88which acquires a lock (or at least, permission) on entry and
99releases it on exit.
1010
11+ The model is to prevent concurrency problems if there are multiple
12+ SCons processes running which may try to read/write the same
13+ externally shared resource, such as a cache file. They don't
14+ have to be the same machine in the case of a network share,
15+ so we can't use OS-specific locking schemes like fcntl/msvcrt.
16+
1117Usage::
1218
1319 from SCons.Util.filelock import FileLock
1925
2026# TODO: things to consider.
2127# Is raising an exception the right thing for failing to get lock?
22- # Is a filesystem lockfile scheme sufficient for our needs?
23- # - or is it better to put locks on the actual file (fcntl/windows-based)?
24- # ... Is that even viable in the case of a remote (network) file?
28+ # Is this scheme sufficient for our needs? Or do we need the extra
29+ # level of creating a "claim file" first and then trying to link
30+ # that to the lockfile? Bonus: a claim file can contain details, like
31+ # how long before another process is allowed to break the lock,
32+ # plus identifying the creator.
2533# Is this safe enough? Or do we risk dangling lockfiles?
2634# Permission issues in case of multi-user. This *should* be okay,
2735# the cache usually goes in user's homedir, plus you already have
28- # enough rights for the lockfile if the dir lets you create the cache.
36+ # enough rights for the lockfile if the dir lets you create the cache,
37+ # but manual permission fiddling may be needed for network shares.
2938# Need a forced break-lock method?
3039# The lock attributes could probably be made opaque. Showed one visible
3140# in the example above, but not sure the benefit of that.
3746
3847
3948class SConsLockFailure (Exception ):
40- """Lock failure exception."""
49+ """Generic lock failure exception."""
50+
51+ class AlreadyLockedError (SConsLockFailure ):
52+ """Non-blocking lock attempt on already locked object."""
53+
54+ class LockTimeoutError (SConsLockFailure ):
55+ """Blocking lock attempt timed out."""
4156
4257
4358class FileLock :
44- """Lock a file using a lockfile.
59+ """Lock a resource using a lockfile.
4560
46- Basic locking for when multiple processes may hit an externally
47- shared resource that cannot depend on locking within a single SCons
48- process. SCons does not have a lot of those, but caches come to mind.
61+ Basic locking for when multiple processes may hit an externally shared
62+ resource (i.e. a file) that cannot depend on locking within a single
63+ SCons process. SCons does not have a lot of those, but caches come to
64+ mind. Note that the resource named by *file* does not need to exist.
4965
50- Cross-platform safe, does not use any OS-specific features. Provides
66+ Cross-platform safe, does not use any OS-specific features. Is not
67+ safe from outside interference - a "locked" resource can be messed
68+ with by anyone who doesn't pay attention to this protocol. Provides
5169 context manager support, or can be called with :meth:`acquire_lock`
5270 and :meth:`release_lock`.
5371
5472 Lock can be a write lock, which is held until released, or a read
55- lock, which releases immediately upon aquisition - we want to not
56- read a file which somebody else may be writing, but not create the
57- writers starvation problem of the classic readers/writers lock.
73+ lock, which releases immediately upon acquisition - we want to prevent
74+ reading a file while somebody else may be writing to it, but once we
75+ get that permission, we don't want to block others from reading it
76+ too, or asking for a write lock on it.
5877
5978 TODO: Should default timeout be None (non-blocking), or 0 (block forever),
60- or some arbitrary number?
79+ or some arbitrary number? For now it's non-blocking.
6180
6281 Arguments:
6382 file: name of file to lock. Only used to build the lockfile name.
64- timeout: optional time (sec) to give up trying.
65- If ``None``, quit now if we failed to get the lock ( non-blocking).
83+ timeout: optional time (sec) after which to give up trying.
84+ If ``None``, the lock attempt will be non-blocking (the default ).
6685 If 0, block forever (well, a long time).
6786 delay: optional delay between tries [default 0.05s]
68- writer: if True , obtain the lock for safe writing. If False (default),
69- just wait till the lock is available, give it back right away.
87+ writer: if true , obtain and hold the lock for safe writing.
88+ If false ( the default), release the lock right away.
7089
7190 Raises:
7291 SConsLockFailure: if the operation "timed out", including the
@@ -97,8 +116,10 @@ def __init__(
97116 self .writer = writer
98117
99118 def acquire_lock (self ) -> None :
100- """Acquire the lock, if possible .
119+ """Acquire the lock.
101120
121+ Blocks until the lock is acquired, unless the lock object
122+ was initialized to not block.
102123 If the lock is in use, check again every *delay* seconds.
103124 Continue until lock acquired or *timeout* expires.
104125 """
@@ -108,12 +129,12 @@ def acquire_lock(self) -> None:
108129 self .lock = os .open (self .lockfile , os .O_CREAT | os .O_EXCL | os .O_RDWR )
109130 except (FileExistsError , PermissionError ) as exc :
110131 if self .timeout is None :
111- raise SConsLockFailure (
132+ raise AlreadyLockedError (
112133 f"Could not acquire lock on { self .file !r} "
113134 ) from exc
114135 if (time .perf_counter () - start_time ) > self .timeout :
115- raise SConsLockFailure (
116- f"Timeout waiting for lock on { self .file !r} ."
136+ raise LockTimeoutError (
137+ f"Timeout waiting for lock on { self .file !r} for { self . timeout } seconds ."
117138 ) from exc
118139 time .sleep (self .delay )
119140 else :
@@ -131,21 +152,30 @@ def release_lock(self) -> None:
131152
132153 def __enter__ (self ) -> FileLock :
133154 """Context manager entry: acquire lock if not holding."""
134- if not self .lock :
155+ if self .lock is None :
135156 self .acquire_lock ()
136157 return self
137158
138159 def __exit__ (self , exc_type , exc_value , exc_tb ) -> None :
139160 """Context manager exit: release lock if holding."""
140- if self .lock :
161+ if self .lock is not None :
141162 self .release_lock ()
142163
143164 def __repr__ (self ) -> str :
144165 """Nicer display if someone repr's the lock class."""
145166 return (
146- f"{ self .__class__ .__name__ } ("
167+ f"< { self .__class__ .__name__ } ("
147168 f"file={ self .file !r} , "
148- f"timeout={ self .timeout !r} , "
149- f"delay={ self .delay !r} , "
150- f"writer={ self .writer !r} )"
169+ f"type={ 'writer, ' if self .writer else 'reader, ' } "
170+ f"lock={ 'unlocked' if self .lock is None else 'locked' } , "
171+ f"timeout={ self .timeout } , "
172+ f"delay={ self .delay } , "
173+ f"pid={ os .getpid ()} ) "
174+ f"at 0x{ id (self ):x} >"
151175 )
176+
177+ # Local Variables:
178+ # tab-width:4
179+ # indent-tabs-mode:nil
180+ # End:
181+ # vim: set expandtab tabstop=4 shiftwidth=4:
0 commit comments