Skip to content

Commit c18f098

Browse files
author
Richard Jones
committed
node ids are now generated from a lockable store - no more race conditions
We're using the portalocker code by Jonathan Feinberg that was contributed to the ASPN Python cookbook. This gives us locking across Unix and Windows.
1 parent 25e9ed6 commit c18f098

File tree

9 files changed

+275
-11
lines changed

9 files changed

+275
-11
lines changed

.cvsignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
*.pyc
22
localconfig.py
33
build
4+
MANIFEST

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Feature:
2323
Fixed:
2424
. stop sending blank (whitespace-only) notes
2525
. cleanup of serialisation for database storage
26+
. node ids are now generated from a lockable store - no more race conditions
2627

2728

2829
2002-03-25 - 0.4.1

doc/.cvsignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ implementation.html
66
index.html
77
installation.html
88
user_guide.html
9+
FAQ.html

roundup/backends/back_anydbm.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
#$Id: back_anydbm.py,v 1.31 2002-04-03 05:54:31 richard Exp $
18+
#$Id: back_anydbm.py,v 1.32 2002-04-15 23:25:15 richard Exp $
1919
'''
2020
This module defines a backend that saves the hyperdatabase in a database
2121
chosen by anydbm. It is guaranteed to always be available in python
@@ -26,6 +26,7 @@
2626
import whichdb, anydbm, os, marshal
2727
from roundup import hyperdb, date
2828
from blobfiles import FileStorage
29+
from locking import acquire_lock, release_lock
2930

3031
#
3132
# Now the database
@@ -130,6 +131,7 @@ def _opendb(self, name, mode):
130131
'''
131132
if hyperdb.DEBUG:
132133
print '_opendb', (self, name, mode)
134+
133135
# determine which DB wrote the class file
134136
db_type = ''
135137
path = os.path.join(os.getcwd(), self.dir, name)
@@ -159,6 +161,31 @@ def _opendb(self, name, mode):
159161
print "_opendb %r.open(%r, %r)"%(db_type, path, mode)
160162
return dbm.open(path, mode)
161163

164+
def _lockdb(self, name):
165+
''' Lock a database file
166+
'''
167+
path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
168+
return acquire_lock(path)
169+
170+
#
171+
# Node IDs
172+
#
173+
def newid(self, classname):
174+
''' Generate a new id for the given class
175+
'''
176+
# open the ids DB - create if if doesn't exist
177+
lock = self._lockdb('_ids')
178+
db = self._opendb('_ids', 'c')
179+
if db.has_key(classname):
180+
newid = db[classname] = str(int(db[classname]) + 1)
181+
else:
182+
# the count() bit is transitional - older dbs won't start at 1
183+
newid = str(self.getclass(classname).count()+1)
184+
db[classname] = newid
185+
db.close()
186+
release_lock(lock)
187+
return newid
188+
162189
#
163190
# Nodes
164191
#
@@ -442,6 +469,14 @@ def rollback(self):
442469

443470
#
444471
#$Log: not supported by cvs2svn $
472+
#Revision 1.31 2002/04/03 05:54:31 richard
473+
#Fixed serialisation problem by moving the serialisation step out of the
474+
#hyperdb.Class (get, set) into the hyperdb.Database.
475+
#
476+
#Also fixed htmltemplate after the showid changes I made yesterday.
477+
#
478+
#Unit tests for all of the above written.
479+
#
445480
#Revision 1.30 2002/02/27 03:40:59 richard
446481
#Ran it through pychecker, made fixes
447482
#

roundup/backends/locking.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#! /usr/bin/env python
2+
# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/)
3+
#
4+
# Permission is hereby granted, free of charge, to any person obtaining a copy
5+
# of this software and associated documentation files (the "Software"), to deal
6+
# in the Software without restriction, including without limitation the rights
7+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
# copies of the Software, and to permit persons to whom the Software is
9+
# furnished to do so, subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be included in
12+
# all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
# SOFTWARE.
21+
22+
# $Id: locking.py,v 1.1 2002-04-15 23:25:15 richard Exp $
23+
24+
'''This module provides a generic interface to acquire and release
25+
exclusive access to a file.
26+
27+
It should work on Unix and Windows.
28+
'''
29+
30+
import portalocker
31+
32+
def acquire_lock(path, block=1):
33+
'''Acquire a lock for the given path
34+
'''
35+
import portalocker
36+
file = open(path, 'w')
37+
if block:
38+
portalocker.lock(file, portalocker.LOCK_EX)
39+
else:
40+
portalocker.lock(file, portalocker.LOCK_EX|portalocker.LOCK_NB)
41+
return file
42+
43+
def release_lock(file):
44+
'''Release our lock on the given path
45+
'''
46+
portalocker.unlock(file)
47+
48+
#
49+
# $Log: not supported by cvs2svn $
50+
#
51+
#

roundup/backends/portalocker.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# portalocker.py - Cross-platform (posix/nt) API for flock-style file locking.
2+
# Requires python 1.5.2 or better.
3+
4+
# ID line added by richard for Roundup file tracking
5+
# $Id: portalocker.py,v 1.1 2002-04-15 23:25:15 richard Exp $
6+
7+
"""Cross-platform (posix/nt) API for flock-style file locking.
8+
9+
Synopsis:
10+
11+
import portalocker
12+
file = open("somefile", "r+")
13+
portalocker.lock(file, portalocker.LOCK_EX)
14+
file.seek(12)
15+
file.write("foo")
16+
file.close()
17+
18+
If you know what you're doing, you may choose to
19+
20+
portalocker.unlock(file)
21+
22+
before closing the file, but why?
23+
24+
Methods:
25+
26+
lock( file, flags )
27+
unlock( file )
28+
29+
Constants:
30+
31+
LOCK_EX
32+
LOCK_SH
33+
LOCK_NB
34+
35+
I learned the win32 technique for locking files from sample code
36+
provided by John Nielsen <[email protected]> in the documentation
37+
that accompanies the win32 modules.
38+
39+
Author: Jonathan Feinberg <[email protected]>
40+
Version: Id: portalocker.py,v 1.3 2001/05/29 18:47:55 Administrator Exp
41+
**un-cvsified by richard so the version doesn't change**
42+
"""
43+
import os
44+
45+
if os.name == 'nt':
46+
import win32con
47+
import win32file
48+
import pywintypes
49+
LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
50+
LOCK_SH = 0 # the default
51+
LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
52+
# is there any reason not to reuse the following structure?
53+
__overlapped = pywintypes.OVERLAPPED()
54+
elif os.name == 'posix':
55+
import fcntl
56+
LOCK_EX = fcntl.LOCK_EX
57+
LOCK_SH = fcntl.LOCK_SH
58+
LOCK_NB = fcntl.LOCK_NB
59+
else:
60+
raise RuntimeError("PortaLocker only defined for nt and posix platforms")
61+
62+
if os.name == 'nt':
63+
def lock(file, flags):
64+
hfile = win32file._get_osfhandle(file.fileno())
65+
win32file.LockFileEx(hfile, flags, 0, 0xffff0000, __overlapped)
66+
67+
def unlock(file):
68+
hfile = win32file._get_osfhandle(file.fileno())
69+
win32file.UnlockFileEx(hfile, 0, 0xffff0000, __overlapped)
70+
71+
elif os.name =='posix':
72+
def lock(file, flags):
73+
fcntl.flock(file.fileno(), flags)
74+
75+
def unlock(file):
76+
fcntl.flock(file.fileno(), fcntl.LOCK_UN)
77+
78+
if __name__ == '__main__':
79+
from time import time, strftime, localtime
80+
import sys
81+
import portalocker
82+
83+
log = open('log.txt', "a+")
84+
portalocker.lock(log, portalocker.LOCK_EX)
85+
86+
timestamp = strftime("%m/%d/%Y %H:%M:%S\n", localtime(time()))
87+
log.write( timestamp )
88+
89+
print "Wrote lines. Hit enter to release lock."
90+
dummy = sys.stdin.readline()
91+
92+
log.close()
93+

roundup/hyperdb.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: hyperdb.py,v 1.62 2002-04-03 07:05:50 richard Exp $
18+
# $Id: hyperdb.py,v 1.63 2002-04-15 23:25:15 richard Exp $
1919

2020
__doc__ = """
2121
Hyperdatabase implementation, especially field types.
@@ -353,7 +353,7 @@ def create(self, **propvalues):
353353
raise DatabaseError, 'Database open read-only'
354354

355355
# new node's id
356-
newid = str(self.count() + 1)
356+
newid = self.db.newid(self.classname)
357357

358358
# validate propvalues
359359
num_re = re.compile('^\d+$')
@@ -1127,6 +1127,10 @@ def Choice(name, db, *options):
11271127

11281128
#
11291129
# $Log: not supported by cvs2svn $
1130+
# Revision 1.62 2002/04/03 07:05:50 richard
1131+
# d'oh! killed retirement of nodes :(
1132+
# all better now...
1133+
#
11301134
# Revision 1.61 2002/04/03 06:11:51 richard
11311135
# Fix for old databases that contain properties that don't exist any more.
11321136
#

test/test_db.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: test_db.py,v 1.20 2002-04-03 05:54:31 richard Exp $
18+
# $Id: test_db.py,v 1.21 2002-04-15 23:25:15 richard Exp $
1919

2020
import unittest, os, shutil
2121

@@ -66,6 +66,8 @@ def setUp(self):
6666
os.makedirs(config.DATABASE + '/files')
6767
self.db = anydbm.Database(config, 'test')
6868
setupSchema(self.db, 1)
69+
self.db2 = anydbm.Database(config, 'test')
70+
setupSchema(self.db2, 0)
6971

7072
def testChanges(self):
7173
self.db.issue.create(title="spam", status='1')
@@ -140,8 +142,6 @@ def testTransactions(self):
140142
self.db.rollback()
141143
self.assertNotEqual(num_files, self.db.numfiles())
142144
self.assertEqual(num_files2, self.db.numfiles())
143-
144-
145145

146146
def testExceptions(self):
147147
# this tests the exceptions that should be raised
@@ -192,17 +192,17 @@ def testExceptions(self):
192192
# set up a valid issue for me to work on
193193
self.db.issue.create(title="spam", status='1')
194194
# invalid link index
195-
ar(IndexError, self.db.issue.set, '1', title='foo', status='bar')
195+
ar(IndexError, self.db.issue.set, '6', title='foo', status='bar')
196196
# invalid link value
197-
ar(ValueError, self.db.issue.set, '1', title='foo', status=1)
197+
ar(ValueError, self.db.issue.set, '6', title='foo', status=1)
198198
# invalid multilink type
199-
ar(TypeError, self.db.issue.set, '1', title='foo', status='1',
199+
ar(TypeError, self.db.issue.set, '6', title='foo', status='1',
200200
nosy='hello')
201201
# invalid multilink index type
202-
ar(ValueError, self.db.issue.set, '1', title='foo', status='1',
202+
ar(ValueError, self.db.issue.set, '6', title='foo', status='1',
203203
nosy=[1])
204204
# invalid multilink index
205-
ar(IndexError, self.db.issue.set, '1', title='foo', status='1',
205+
ar(IndexError, self.db.issue.set, '6', title='foo', status='1',
206206
nosy=['10'])
207207

208208
def testJournals(self):
@@ -269,6 +269,11 @@ def testPack(self):
269269
def testRetire(self):
270270
pass
271271

272+
def testIDGeneration(self):
273+
id1 = self.db.issue.create(title="spam", status='1')
274+
id2 = self.db2.issue.create(title="eggs", status='2')
275+
self.assertNotEqual(id1, id2)
276+
272277

273278
class anydbmReadOnlyDBTestCase(MyTestCase):
274279
def setUp(self):
@@ -281,6 +286,8 @@ def setUp(self):
281286
setupSchema(db, 1)
282287
self.db = anydbm.Database(config)
283288
setupSchema(self.db, 0)
289+
self.db2 = anydbm.Database(config, 'test')
290+
setupSchema(self.db2, 0)
284291

285292
def testExceptions(self):
286293
# this tests the exceptions that should be raised
@@ -301,6 +308,8 @@ def setUp(self):
301308
os.makedirs(config.DATABASE + '/files')
302309
self.db = bsddb.Database(config, 'test')
303310
setupSchema(self.db, 1)
311+
self.db2 = bsddb.Database(config, 'test')
312+
setupSchema(self.db2, 0)
304313

305314
class bsddbReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
306315
def setUp(self):
@@ -313,6 +322,8 @@ def setUp(self):
313322
setupSchema(db, 1)
314323
self.db = bsddb.Database(config)
315324
setupSchema(self.db, 0)
325+
self.db2 = bsddb.Database(config, 'test')
326+
setupSchema(self.db2, 0)
316327

317328

318329
class bsddb3DBTestCase(anydbmDBTestCase):
@@ -324,6 +335,8 @@ def setUp(self):
324335
os.makedirs(config.DATABASE + '/files')
325336
self.db = bsddb3.Database(config, 'test')
326337
setupSchema(self.db, 1)
338+
self.db2 = bsddb3.Database(config, 'test')
339+
setupSchema(self.db2, 0)
327340

328341
class bsddb3ReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
329342
def setUp(self):
@@ -336,6 +349,8 @@ def setUp(self):
336349
setupSchema(db, 1)
337350
self.db = bsddb3.Database(config)
338351
setupSchema(self.db, 0)
352+
self.db2 = bsddb3.Database(config, 'test')
353+
setupSchema(self.db2, 0)
339354

340355

341356
def suite():
@@ -362,6 +377,14 @@ def suite():
362377

363378
#
364379
# $Log: not supported by cvs2svn $
380+
# Revision 1.20 2002/04/03 05:54:31 richard
381+
# Fixed serialisation problem by moving the serialisation step out of the
382+
# hyperdb.Class (get, set) into the hyperdb.Database.
383+
#
384+
# Also fixed htmltemplate after the showid changes I made yesterday.
385+
#
386+
# Unit tests for all of the above written.
387+
#
365388
# Revision 1.19 2002/02/25 14:34:31 grubert
366389
# . use blobfiles in back_anydbm which is used in back_bsddb.
367390
# change test_db as dirlist does not work for subdirectories.

0 commit comments

Comments
 (0)