11import os
2+ import signal
23import time
4+ import atexit
35from datetime import datetime
6+ """
7+ This module provides a directory-based locking mechanism.
8+ It allows for acquiring and releasing locks using directories,
9+ which can be useful in scenarios where file-based based
10+ operations can not be done atomically (e.g.
11+ distributed or cluster filesystem)
12+
13+ The DirLock class provides methods to acquire and release locks,
14+ and it supports context management for easy usage.
15+
16+ It also handles cleanup of locks on program exit or signal interrupts.
17+ """
18+
19+
20+ # keep a list of currently acquired locks
21+ # so these can be cleaned up on exit
22+ _allActiveLocks = set ()
23+ # we don't want to call the original handler
24+ original_sigint_handler = signal .getsignal (signal .SIGINT )
25+ original_sigterm_handler = signal .getsignal (signal .SIGTERM )
26+
27+
28+ # function to clean up all active locks
29+ def _clean_locks ():
30+ global _allActiveLocks
31+ # Create a copy to avoid "Set changed size during iteration" error
32+ locks_to_release = list (_allActiveLocks )
33+ for dl in locks_to_release :
34+ dl .release ()
35+ _allActiveLocks .remove (dl )
36+
37+
38+ def handle_sigint_cleanup (signum , frame ):
39+ """
40+ Handle SIGINT (Ctrl+C) cleanup.
41+ This function is called when a SIGINT signal is received.
42+ It cleans up all active locks and calls the original signal handler if it exists.
43+ """
44+ global original_sigint_handler
45+ _clean_locks ()
46+ if original_sigint_handler is not None :
47+ original_sigint_handler (signum , frame )
48+
49+
50+ def handle_sigterm_cleanup (signum , frame ):
51+ """
52+ Handle SIGTERM cleanup.
53+ This function is called when a SIGTERM signal is received.
54+ It cleans up all active locks and calls the original signal handler if it exists.
55+ """
56+ global original_sigterm_handler
57+ _clean_locks ()
58+ if original_sigterm_handler is not None :
59+ original_sigterm_handler (signum , frame )
60+
61+
62+ # normal exit clean up
63+ atexit .register (_clean_locks )
64+
65+ # ctrl+c cleanup
66+ signal .signal (signal .SIGINT , handle_sigint_cleanup )
67+ # sigterm cleanup
68+ signal .signal (signal .SIGTERM , handle_sigterm_cleanup )
469
570
671class DirLock :
772 default_retry_interval : float = 0.1
73+
874 def __init__ (self ,
975 lock_dir : str ,
1076 retry_interval : float = None ,
@@ -30,12 +96,14 @@ def acquire(self):
3096 """
3197 Acquire the lock by following the directory-based lock mechanism.
3298 """
99+ global _allActiveLocks
33100 start_time = datetime .now ()
34101
35102 while True :
36103 try :
37104 os .mkdir (self .lock_dir )
38105 self .acquired = True
106+ _allActiveLocks .add (self )
39107 break
40108 except FileExistsError :
41109 pass
@@ -51,11 +119,13 @@ def release(self):
51119 """
52120 Release the lock if it is held by this instance.
53121 """
54- try :
55- os .rmdir (self .lock_dir )
56- except FileNotFoundError :
57- pass
58- self .acquired = False
122+ if self .acquired :
123+ try :
124+ os .rmdir (self .lock_dir )
125+ _allActiveLocks .remove (self )
126+ except FileNotFoundError :
127+ pass
128+ self .acquired = False
59129
60130 def __enter__ (self ):
61131 """
@@ -70,6 +140,8 @@ def __exit__(self, exc_type, exc_value, traceback):
70140 """
71141 self .release ()
72142
143+
73144class LockTimeoutException (Exception ):
74145 def __init__ (self , message = "acquiring lock timed out" ):
75- super ().__init__ (message )
146+ super ().__init__ (message )
147+
0 commit comments