-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlockbydir.py
443 lines (319 loc) · 15.1 KB
/
lockbydir.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
'''
lockbydir.py - Locking across processes. Using lockdir existence and age.
@contact: python (at) AndreasKrueger (dot) de
@since: 23 Jan 2015
@license: Never remove my name, nor the examples - and send me job offers.
@bitcoin: I am poor, send bitcoins: 1MT9gazTyodKVU3XFEUgR5aCwG7rXXiuWC Thx!
@requires: lockbydir_OS.py # OS level routines for lockdir
@requires: lockbydir_concurrent.py # for multiprocess example
@call: L = DLock( "name" ) # see howToUse() for details
@return: class with .LoopWhileLocked_ThenLocking() and .unlocking()
@summary
An easy to use lock mechanism, with lock timeout, and limited waiting patience.
I want locking of the django DB, across concurrent uwsgi processes, for any
underlying DB. See posting: http://stackoverflow.com/q/28030536/3693375
When no one could really help me to the end, I created this class 'DLock'.
Inner workings:
A lock is represented by a lockdir, in the current directory (or a RAMDISK).
While it exists, and its filedate is recent, the lock is 'locked'.
If the dir does not exist, or is timed out, the lock is 'unlocked'.
Users need not care about the lockdir, but access the DLock class
by two functions: .LoopWhileLocked_ThenLocking() and .unlocking()
Parameters:
TIMEOUT: Seconds after which a lock opens automatically.
PATIENCE: Seconds after which no more hope to acquire the lock.
Default TIMEOUT, and PATIENCE can be changed in each DLock instance,
or (better) by subclassing DLock.
Shortest possible usage is in howToUse().
The inner workings are well explained in testDLocks().
Parallel processes are shown in 2 examples in 'lockbydir_concurrent.py'.
@todo: Possible future improvement:
TODO 1: how to make this atomic ?
if self.timedOut():
self.breakLock()
I have never seen it happen (as the first step is very fast), but it is not
impossible that 2 processes both arrive at .timedOut() = True at the same time.
And when one has done '.breakLock()' AND later already created a new lock,
only then the second one reaches the '.breakLock()'.
Very improbable, but not impossible. Then two could both be locking.
I have no idea how to solve this. What is the equivalent of a successful mkdir,
when successful non-existence of a directory is the criterion? While at the same
time exactly that is the starting point for the opposite process. A riddle.
TODO 2: Put timeout into lock dir
At the moment all processes using DLock with same name must make sure
that they use the same TIMEOUT! See 'lockbydir_concurrent.FastDLock'
for an elegant way to guarantee that. (Subclassing DLock)
New idea: Put timeout into lockdir, then each lockdir creator can decide
about his own timeout - and all waiting consumers have to obey to that.
Would really put PATIENCE into the waiting one, and TIMEOUT into the lock.
Considerable overhead because all waiting processes will check often
for timeout, so lots of read-access to the disk. Or all waiting loops
get a timer each, and only come back to checking for timeout as soon
as the lock's time is up. Quite a bit of work - perhaps?
Still, a good idea, I guess. But as all my processes have the same timeout,
not necessary for my purposes. Fork this GIT if you want to implement it.
(Or pay me to code it. See @bitcoin.)
And please: Contact me. Send a postcard. In any case. Thanks a lot.
This was a very interesting task! :-) Learnt a lot! Python is great!
See my github For feature requests, ideas, suggestions, appraisal, criticism:
@issues https://github.com/drandreaskrueger/lockbydir/issues
@wiki https://github.com/drandreaskrueger/lockbydir/wiki
'''
# default values:
# (Overwrite in your DLock() instance, not here!)
# for looping:
CHECKEVERYXSECONDS = 0.03 # How frequently check 'isUnlocked' in while loop.
# Locked resource MUST be freed after this timeout!
# Old lockdir is treated as if it didn't exist at all.
# ! After timeout, the lock does not protect anymore !
TIMEOUT = 10 # for some of the examples, 10 is optimal.
# When trying to get the resource, don't wait longer than this.
# Between first attempt to lock, and finally giving up:
PATIENCE = 30
# do not change:
REMOVETIMEDOUT = True # default: remove old locks when tested by 'isLocked'
import time
from lockbydir_OS import LOCKDIREXTENSION, pathExists, pathAgeInSeconds
from lockbydir_OS import mkdir_ReturnWhetherSuccessful, rmdir_ReturnWhetherSuccessfullyRemoved
class DLock:
"""Locking by directory existence, and age. With 2 auto-timeouts:
TIMEOUT: After 'timeout' seconds a lockdir is like no lockdir.
PATIENCE: A waiting instance gives up after 'patience' seconds.
Only the lock 'name' is relevant for the state, i.e.:
Several instances with identical 'name's share the same lock!"""
# begin PUBLIC
# the following three are the only functions which you need to access:
def __init__(self, name):
self.name = name
self.lockingTime = None
self.startedWaitingTime = None
# default values. Overwrite in your instance if other values wanted.
# explanations: See top of this code file.
self.TIMEOUT = TIMEOUT
self.PATIENCE = PATIENCE
self.CHECKEVERYXSECONDS = CHECKEVERYXSECONDS
self.REMOVETIMEDOUT = REMOVETIMEDOUT
self.LOCKDIREXTENSION = LOCKDIREXTENSION
def LoopWhileLocked_ThenLocking(self):
"""THIS is the correct way to acquire a lock.
It waits for lock to open or time out, then tries locking.
(If many are waiting,) repeat only until PATIENCE is gone.
Returns False if locking failed.
Returns True if locking succeeded.
"""
self.startedWaitingTime = time.time()
acquired = False
while (not acquired and self.stillPatience()):
_ = self.loopWhileLocked()
acquired = self.locking()
return acquired
def unlocking( self ):
"""Try remove lock if owned by me & not timed-out yet.
If removal happened, return true.
If not, return false.
N.B.: Only the rightful lock can use unlocking.
N.B.: Unlocking is only possible before timeout.
"""
if self.lockingTime == None: # cannot be the owner
return False
# never unlock after timeout,
# because it might already be owned by other process!
elif (time.time() - self.lockingTime) < self.TIMEOUT:
self.lockingTime = None
return self.breakLock()
else:
return False # so it had already timed out
# end PUBLIC functions.
# begin PRIVATE functions. Usually no need to call them:
def dirname (self):
"lockname plus extension = lock dir name"
return self.name + LOCKDIREXTENSION
def breakLock(self):
"""Low level routine, do not call directly unless you really know why!
If lock owner, just use 'unlocking'.
Or: Just let it time out, old dir is like no dir.
returns True if there was a lockdir, and rmdir was successful.
returns False if not existed, or rmdir failed."""
try:
return rmdir_ReturnWhetherSuccessfullyRemoved ( self.dirname() )
except:
return False
def exists (self):
"Does a lockdir for this lockname exist?"
return pathExists ( self.dirname() )
def age(self):
"""total seconds since last modification.
N.B.: Can return ERROR=-1 if simultaneous file access."""
return pathAgeInSeconds( self.dirname() )
def timedOut (self):
"is last modification longer ago than 'timeout' seconds?"
return self.age() > self.TIMEOUT
def existsAndNotTimedOut(self):
".exists() and not .timedOut()"
return (self.exists() and not self.timedOut())
def startWaiting(self):
"store first moment of trying to acquire lock, for patience condition"
if self.startedWaitingTime == None:
self.startedWaitingTime = time.time()
def locking(self):
"""Try to create a lockdir for this lockname, by mkdir
... then return true.
If already existed and not timed out yet,
(or mkdir fails for other reasons), then return false."""
self.startWaiting()
if self.existsAndNotTimedOut():
return False
acquired = mkdir_ReturnWhetherSuccessful ( self.dirname() )
if acquired:
self.lockingTime = time.time()
self.startedWaitingTime = None
return acquired
def stillPatience(self):
"Waiting instance has limited patience. Return whether patience left."
nervousness = time.time() - self.startedWaitingTime
return (nervousness < self.PATIENCE)
def loopWhileLocked(self):
"""Returns False if it was not locked anyway.
Returns True if it had to wait.
If locked, then test in 'checkeveryXseconds' intervals.
(But only until patience is gone.)
Then, when not locked anymore, return True.
Only a helper function, used in 'LoopWhileLocked_ThenLocking'
"""
if not self.isLocked():
return False
self.startWaiting()
while self.isLocked() and self.stillPatience():
time.sleep (self.CHECKEVERYXSECONDS)
return True
def removeIfTimedOut (self):
"delete the lockfile after timeout"
# TODO: how to make this atomic ??????????????
if self.timedOut():
self.breakLock()
def isLocked( self ):
"""If locked, return True.
If not locked or timed out, return False.
Default is to delete a timedOut lockfile."
"""
if not self.exists():
return False
if self.timedOut():
if self.REMOVETIMEDOUT: self.removeIfTimedOut()
return False
return True # in all other cases it is locked
def getInfoLogger(ID = ""):
"nice printing with timestamp and choosable IDs"
# reload to redefine basicConfig:
import sys, logging; reload(sys.modules['logging']); import logging
FORMAT = '[%(asctime)s.%(msecs).03d] ' + ID + ' %(message)s'
logging.basicConfig(format=FORMAT, datefmt='%I:%M:%S', level=logging.DEBUG)
logger = logging.getLogger(__name__)
return logger.info
# logger = getLogger('[%(asctime)s.%(msecs).03d] ' + ID + ' %(message)s')
def testDLock():
"Present the inner workings, and all relevant functions."
Log = getInfoLogger()
name = "example"
Log("INITIALIZE DLock instance:")
L = DLock (name)
Log("L = %s" % L )
Log("L.dirname = %s (pathname of the lockdir)", L.dirname() )
Log("A hard delete; usually do not use, only at beginning perhaps:")
Log("L.breakLock = %s (from earlier runs perhaps)" % L.breakLock() )
Log("L.exists = %s (no lockdir because of above breakLock)", L.exists() )
Log("")
Log("EXAMPLE: locking, and see what it does:")
Log("locking = %s", L.locking () )
Log("sleep a bit ...")
time.sleep(1)
Log("L.age = %s" % L.age ( ))
Log("L.isLocked = %s" % L.isLocked())
Log("L.exists = %s" % L.exists() )
Log("")
Log("EXAMPLE: Show effect of timeout on 'isLocked'")
L.TIMEOUT = 1
Log("L.TIMEOUT = 1")
Log("L.isLocked = %s" % L.isLocked())
Log("L.exists = %s (as 'isLocked' removed the timedout dir)" % L.exists())
Log("")
Log("EXAMPLE: Demonstrate loopWhileLocked()")
L.TIMEOUT = 3
Log("L.TIMEOUT = 3")
Log("L.locking = %s", L.locking () )
Log("L.loopWhileLocked() ... ")
Log(" = %s", L.loopWhileLocked())
Log("end loop (Hint: Study the timestamps on the left)")
Log("L.isLocked = %s" % L.isLocked())
Log("")
Log("EXAMPLE: Try locking when already locked:")
Log("L.locking = %s", L.locking () )
Log("L.locking = %s (cannot because already locked)", L.locking () )
Log("")
Log("EXAMPLE: Not the instance, but 'name' of lock is deciding:")
Log("Create new DLock instance L2:, using SAME lock-name i.e. lock-dir:" )
L2 = DLock (name)
Log("L2 = %s" % L2)
Log("(L.dirname == L2.dirname) = %s" % (L.dirname() == L2.dirname()))
res = L2.locking ()
Log("L2.locking = %s (Cannot. Already locked by L, 'name' decides)" % res)
Log("L2.LoopWhileLocked_ThenLocking() ... (waiting for default timeout)")
res = L2.LoopWhileLocked_ThenLocking()
Log("= %s (could acquire lock after old locking timed out)", res )
Log("L2.isLocked = %s" % L2.isLocked())
Log("L.isLocked = %s (yes, also. Only the 'name' decides.)" % L.isLocked())
Log("")
Log("EXAMPLE: create situation where lack of patience ends the waiting.")
Log("(usually this happens when many threads/process are competing,")
Log(" but here artificially created by TIMEOUT larger than PATIENCE)")
Log("L.breakLock = %s (to reset the situation)" % L.breakLock())
Log("")
L.TIMEOUT = 10
Log("L.TIMEOUT = 10")
Log("L.locking = %s (acquired by L)" % L.locking())
Log("")
Log("L2.TIMEOUT = 10")
L2.TIMEOUT = 10
Log("L2.PATIENCE = 4")
L2.PATIENCE = 4
Log("L2.LoopWhileLocked_ThenLocking() ... ")
res = L2.LoopWhileLocked_ThenLocking()
Log("= %s (gave up due to lack of patience)", res )
Log("L.isLocked = %s" % L.isLocked())
time.sleep(6)
Log("L.isLocked = %s" % L.isLocked())
Log("End. More examples in the other files.")
def print_Ramdisk_Manual():
"""Measured:
500 threads waiting for 1 DLock - overhead by threading, print, and locking:
Machine 1
Windows with HD: min=0.0063 max=0.0704 median=0.0279 mean=0.0274 stdv=0.0131
Linux (in VirtualBox):
with HD: min=0.0004 max=0.0475 median=0.0030 mean=0.0045 stdv=0.0047
with RAMDISK: min=0.0004 max=0.0422 median=0.0023 mean=0.0038 stdv=0.0042
Machine 2 Linux:
with SSD: min=0.0005 max=0.0043 median=0.0008 mean=0.0009 stdv=0.0004
with RAMDISK: min=0.0003 max=0.0018 median=0.0007 mean=0.0008 stdv=0.0003
So: When time critical, put DLock in RAM! And lower the 'CHECKEVERYXSECONDS'.
"""
print "\nN.B.:"
print"""A (tiny!) ramdisk reduces the overhead of frequent read/writes. On Linux:
modprobe rd
mkfs -q /dev/ram1 100
mkdir -p /ramcache
mount /dev/ram1 /ramcache
df -H | grep ramcache"""
print "Then you simple use lockname = '/ramcache/lockname'"
print
def howToUse(secs = 9):
"""short version how to use DLocks: A, B, C (, D)."""
L = DLock( "LOCKNAME" ) # step A: instantiate, with name
acquired = L.LoopWhileLocked_ThenLocking() # step B: waitThenTryLocking
if acquired:
time.sleep(secs) # Step C: Use scarce resource (max TIMEOUT secs!)
L.unlocking() # Step D: unlocking = resource free for next user
return acquired # Optional: Tell the caller.
if __name__ == '__main__':
# print_Ramdisk_Manual()
testDLock()
howToUse(1)