def test_open_takes_exclusive_lock(self): config_file = os.path.join(self.make_dir(), "config") config_lock = RunLock(config_file) self.assertFalse(config_lock.is_locked()) with ConfigurationFile.open_for_update(config_file): self.assertTrue(config_lock.is_locked()) self.assertFalse(config_lock.is_locked())
def test_byte_path(self): filename = b"/foo/bar/123:456.txt" expected = get_tentative_data_path( "/run/lock/maas@foo:bar:123::456.txt" ) observed = RunLock(filename).path self.assertEqual(expected, observed)
def open_for_update(cls, path: str): """Open a configuration file. Locks are taken so that there can only be *one* reader or writer for a configuration file at a time. Where configuration files can be read by multiple concurrent processes it follows that each process should hold the file open for the shortest time possible. **Note** that this returns a context manager which will SAVE changes to the configuration on a clean exit. """ time_opened = None try: # Only one reader or writer at a time. with RunLock(path).wait(timeout=5.0): time_opened = time() # Ensure `path` exists... touch(path) # before loading it in. configfile = cls(path, mutable=True) configfile.load() try: yield configfile except Exception: raise else: if configfile.dirty: configfile.save() finally: if time_opened is not None: time_open = time() - time_opened if time_open >= 2.5: mini_stack = ", from ".join( "%s:%d" % (fn, lineno) for fn, lineno, _, _ in islice( reversed(traceback.extract_stack()), 2, 5 ) ) logger.warn( "Configuration file %s locked for %.1f seconds; this " "may starve other processes. Called from %s.", path, time_open, mini_stack, )
def test__string_path(self): filename = '/foo/bar/123:456.txt' expected = get_tentative_data_path( '/run/lock/maas@foo:bar:123::456.txt') observed = RunLock(filename).path self.assertEqual(expected, observed)