@@ -23,55 +23,78 @@ def os_touch(path: str):
23
23
os .close (fd )
24
24
25
25
26
- def get_lock_file_names (ddb_dir : str , db_name : str , * , id : str = None , time_ns : int = None , stage : str = None , mode : str = None ) -> list [str ]:
27
- """
28
- Returns a list of lock file names in the configured storage directory as
29
- strings. The directory is not include, only the file names are returned.
30
- If any of the arguments are None, they are treated as a wildcard.
26
+ class LockFileMeta :
31
27
32
- Do not use glob, because it is slow. Compiling the regex pattern
33
- takes a long time, and it is called many times.
34
- """
35
- res = []
36
- for x in os .listdir (ddb_dir ):
37
- if not x .endswith (".lock" ):
38
- continue
39
- f_name , f_id , f_time_ns , f_stage , f_mode , _ = x .split ("." )
40
- if f_name != db_name :
41
- continue
42
- if id is not None and f_id != id :
43
- continue
44
- if time_ns is not None and f_time_ns != str (time_ns ):
45
- continue
46
- if stage is not None and f_stage != stage :
47
- continue
48
- if mode is not None and f_mode != mode :
49
- continue
50
- res .append (x )
51
- return res
52
-
53
-
54
- def any_lock_files (ddb_dir : str , db_name : str , * , id : str = None , time_ns : int = None , stage : str = None , mode : str = None ) -> bool :
55
- """
56
- Like get_lock_file_names, but returns True if there is at least one
57
- lock file with the given arguments.
58
- """
59
- for x in os .listdir (ddb_dir ):
60
- if not x .endswith (".lock" ):
61
- continue
62
- f_name , f_id , f_time_ns , f_stage , f_mode , _ = x .split ("." )
63
- if f_name != db_name :
64
- continue
65
- if id is not None and f_id != id :
66
- continue
67
- if time_ns is not None and f_time_ns != str (time_ns ):
68
- continue
69
- if stage is not None and f_stage != stage :
70
- continue
71
- if mode is not None and f_mode != mode :
72
- continue
73
- return True
74
- return False
28
+ __slots__ = ("ddb_dir" , "name" , "id" , "time_ns" , "stage" , "mode" )
29
+
30
+ ddb_dir : str
31
+ name : str
32
+ id : str
33
+ time_ns : str
34
+ stage : str
35
+ mode : str
36
+
37
+ def __init__ (self , ddb_dir , name , id , time_ns , stage , mode ):
38
+ self .ddb_dir = ddb_dir
39
+ self .name = name
40
+ self .id = id
41
+ self .time_ns = time_ns
42
+ self .stage = stage
43
+ self .mode = mode
44
+
45
+ @property
46
+ def path (self ):
47
+ lock_file = f"{ self .name } .{ self .id } .{ self .time_ns } .{ self .stage } .{ self .mode } .lock"
48
+ return os .path .join (self .ddb_dir , lock_file )
49
+
50
+
51
+ class FileLocksSnapshot :
52
+
53
+ __slots__ = ("any_has_locks" , "any_write_locks" , "any_has_write_locks" , "locks" )
54
+
55
+ any_has_locks : bool
56
+ any_write_locks : bool
57
+ any_has_write_locks : bool
58
+ locks : list [LockFileMeta ]
59
+
60
+ def __init__ (self , ddb_dir , db_name , ignore_during_orphan_check ):
61
+ self .any_has_locks = False
62
+ self .any_write_locks = False
63
+ self .any_has_write_locks = False
64
+ self .locks = []
65
+
66
+ for x in os .listdir (ddb_dir ):
67
+ if not x .endswith (".lock" ):
68
+ continue
69
+ f_name , f_id , f_time_ns , f_stage , f_mode , _ = x .split ("." )
70
+ if f_name != db_name :
71
+ continue
72
+
73
+ lock_meta = LockFileMeta (ddb_dir , f_name , f_id , f_time_ns , f_stage , f_mode )
74
+
75
+ # Remove orphaned locks
76
+ if lock_meta .path != ignore_during_orphan_check :
77
+ lock_age = time .monotonic_ns () - int (lock_meta .time_ns )
78
+ if lock_age > LOCK_TIMEOUT * 1_000_000_000 :
79
+ os .unlink (lock_meta .path )
80
+ print (f"Found orphaned lock ({ lock_meta .path } ). Remove" )
81
+ continue
82
+
83
+ self .locks .append (lock_meta )
84
+
85
+ # Lock existence
86
+ if lock_meta .stage == "has" :
87
+ self .any_has_locks = True
88
+ if lock_meta .mode == "write" :
89
+ self .any_has_write_locks = True
90
+ if lock_meta .mode == "write" :
91
+ self .any_write_locks = True
92
+
93
+ def lock_exists (self , id : str , stage : str , mode : str ) -> bool :
94
+ return any (x .id == id and x .stage == stage and x .mode == mode for x in self .locks )
95
+
96
+ def get_need_locks (self ) -> list [LockFileMeta ]:
97
+ return [l for l in self .locks if l .stage == "need" ]
75
98
76
99
77
100
class AbstractLock :
@@ -80,14 +103,15 @@ class AbstractLock:
80
103
call super().__init__(...) and then only exit __init__ when the lock is aquired.
81
104
"""
82
105
83
- __slots__ = ("id" , "time_ns" , "db_name" , "need_path" , "path" , "ddb_dir" )
106
+ __slots__ = ("id" , "time_ns" , "db_name" , "need_path" , "path" , "ddb_dir" , "snapshot" )
84
107
85
108
id : str
86
109
time_ns : int
87
110
db_name : str
88
111
need_path : str
89
112
path : str
90
113
ddb_dir : str
114
+ snapshot : FileLocksSnapshot
91
115
92
116
def __init__ (self , db_name : str ):
93
117
"""
@@ -98,6 +122,8 @@ def __init__(self, db_name: str):
98
122
self .time_ns = time .monotonic_ns ()
99
123
self .db_name = db_name .replace ("/" , "___" ).replace ("." , "____" )
100
124
self .ddb_dir = os .path .join (config .storage_directory , ".ddb" )
125
+ if not os .path .isdir (self .ddb_dir ):
126
+ os .mkdir (self .ddb_dir )
101
127
102
128
def _lock (self ):
103
129
raise NotImplementedError
@@ -119,35 +145,15 @@ def __exit__(self, exc_type, exc_val, exc_tb):
119
145
self ._unlock ()
120
146
121
147
def make_lock_path (self , stage : str , mode : str ) -> str :
122
- if not os .path .isdir (self .ddb_dir ):
123
- os .mkdir (self .ddb_dir )
124
148
return os .path .join (self .ddb_dir , f"{ self .db_name } .{ self .id } .{ self .time_ns } .{ stage } .{ mode } .lock" )
125
149
126
150
def is_oldest_need_lock (self ) -> bool :
127
- # len(need_locks) is at least 1 since this function is only called if
128
- # there is a need_lock
129
- need_locks = get_lock_file_names (self .ddb_dir , self .db_name , stage = "need" )
130
- # Get need locks id and time_ns
131
- need_locks_id_time = []
132
- for lock in need_locks :
133
- _ , l_id , l_time_ns , _ , _ , _ = lock .split ("." )
134
- need_locks_id_time .append ((l_id , l_time_ns ))
151
+ # len(need_locks) is at least 1 since this function is only called if there is a need_lock
152
+ need_locks = self .snapshot .get_need_locks ()
135
153
# Sort by time_ns. If multiple, the the one with the smaller id is first
136
- need_locks_id_time = sorted (need_locks_id_time , key = lambda x : int (x [0 ])) # Sort by id
137
- need_locks_id_time = sorted (need_locks_id_time , key = lambda x : int (x [1 ])) # Sort by time_ns
138
- return need_locks_id_time [0 ][0 ] == self .id
139
-
140
- def remove_orphaned_locks (self ):
141
- for lock_name in get_lock_file_names (self .ddb_dir , self .db_name ):
142
- lock_path = os .path .join (self .ddb_dir , lock_name )
143
- if self .need_path == lock_path :
144
- continue
145
- _ , _ , time_ns , _ , _ , _ = lock_name .split ("." )
146
- if time .monotonic_ns () - int (time_ns ) > LOCK_TIMEOUT * 1_000_000_000 :
147
- os .unlink (lock_path )
148
- print (f"Found orphaned lock ({ lock_name } ). Remove" )
149
-
150
-
154
+ need_locks = sorted (need_locks , key = lambda l : int (l .id ))
155
+ need_locks = sorted (need_locks , key = lambda l : int (l .time_ns ))
156
+ return need_locks [0 ].id == self .id
151
157
152
158
153
159
class ReadLock (AbstractLock ):
@@ -157,7 +163,8 @@ def _lock(self):
157
163
os_touch (self .need_path )
158
164
159
165
# Except if current thread already has a read lock
160
- if any_lock_files (self .ddb_dir , self .db_name , id = self .id , stage = "has" , mode = "read" ):
166
+ self .snapshot = FileLocksSnapshot (self .ddb_dir , self .db_name , self .need_path )
167
+ if self .snapshot .lock_exists (self .id , "has" , "read" ):
161
168
os .unlink (self .need_path )
162
169
raise RuntimeError ("Thread already has a read lock. Do not try to obtain a read lock twice." )
163
170
@@ -166,40 +173,38 @@ def _lock(self):
166
173
167
174
# Iterate until this is the oldest need* lock and no haswrite locks exist, or no *write locks exist
168
175
while True :
169
- self .remove_orphaned_locks ()
170
176
# If no writing is happening, allow unlimited reading
171
- if not any_lock_files ( self .ddb_dir , self . db_name , mode = "write" ) :
177
+ if not self .snapshot . any_write_locks :
172
178
os_touch (self .path )
173
179
os .unlink (self .need_path )
174
180
return
175
181
# A needwrite or haswrite lock exists
176
- if self . is_oldest_need_lock () and not any_lock_files ( self .ddb_dir , self . db_name , stage = "has" , mode = "write" ):
182
+ if not self .snapshot . any_has_write_locks and self . is_oldest_need_lock ( ):
177
183
os_touch (self .path )
178
184
os .unlink (self .need_path )
179
185
return
180
186
time .sleep (SLEEP_TIMEOUT )
181
-
187
+ self . snapshot = FileLocksSnapshot ( self . ddb_dir , self . db_name , self . need_path )
182
188
183
189
184
190
class WriteLock (AbstractLock ):
185
191
def _lock (self ):
186
192
# Instantly signal that we need to write
193
+ self .path = self .make_lock_path ("has" , "write" )
187
194
self .need_path = self .make_lock_path ("need" , "write" )
188
195
os_touch (self .need_path )
189
196
190
197
# Except if current thread already has a write lock
191
- if any_lock_files (self .ddb_dir , self .db_name , id = self .id , stage = "has" , mode = "write" ):
198
+ self .snapshot = FileLocksSnapshot (self .ddb_dir , self .db_name , self .need_path )
199
+ if self .snapshot .lock_exists (self .id , "has" , "write" ):
192
200
os .unlink (self .need_path )
193
201
raise RuntimeError ("Thread already has a write lock. Do try to obtain a write lock twice." )
194
202
195
- # Make path of the hyptoetical haswrite lock
196
- self .path = self .make_lock_path ("has" , "write" )
197
-
198
203
# Iterate until this is the oldest need* lock and no has* locks exist
199
204
while True :
200
- self .remove_orphaned_locks ()
201
- if self .is_oldest_need_lock () and not any_lock_files (self .ddb_dir , self .db_name , stage = "has" ):
205
+ if not self .snapshot .any_has_locks and self .is_oldest_need_lock ():
202
206
os_touch (self .path )
203
207
os .unlink (self .need_path )
204
208
return
205
209
time .sleep (SLEEP_TIMEOUT )
210
+ self .snapshot = FileLocksSnapshot (self .ddb_dir , self .db_name , self .need_path )
0 commit comments