Coverage for drivers/lock.py : 73%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# Copyright (C) Citrix Systems Inc.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published
6# by the Free Software Foundation; version 2.1 only.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17"""Serialization for concurrent operations"""
19import os
20import errno
21import flock
22import util
24VERBOSE = True
27class LockException(util.SMException):
28 pass
31class Lock(object):
32 """Simple file-based lock on a local FS. With shared reader/writer
33 attributes."""
35 BASE_DIR = "/var/lock/sm"
37 INSTANCES = {}
38 BASE_INSTANCES = {}
40 def __new__(cls, name, ns=None, *args, **kwargs):
41 if ns:
42 if ns not in Lock.INSTANCES:
43 Lock.INSTANCES[ns] = {}
44 instances = Lock.INSTANCES[ns]
45 else:
46 instances = Lock.BASE_INSTANCES
48 if name not in instances:
49 instances[name] = LockImplementation(name, ns)
50 return instances[name]
52 # These are required to pacify pylint as it doesn't understand the __new__
53 def acquire(self):
54 raise NotImplementedError("Lock methods implemented in LockImplementation")
56 def acquireNoblock(self):
57 raise NotImplementedError("Lock methods implemented in LockImplementation")
59 def release(self):
60 raise NotImplementedError("Lock methods implemented in LockImplementation")
62 def held(self):
63 raise NotImplementedError("Lock methods implemented in LockImplementation")
65 def _mknamespace(ns):
67 if ns is None:
68 return ".nil"
70 assert not ns.startswith(".")
71 assert ns.find(os.path.sep) < 0
72 return ns
73 _mknamespace = staticmethod(_mknamespace)
75 @staticmethod
76 def clearAll():
77 """
78 Drop all lock instances, to be used when forking, but not execing
79 """
80 Lock.INSTANCES = {}
81 Lock.BASE_INSTANCES = {}
83 def cleanup(name, ns=None):
84 if ns: 84 ↛ 90line 84 didn't jump to line 90, because the condition on line 84 was never false
85 if ns in Lock.INSTANCES: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true
86 if name in Lock.INSTANCES[ns]:
87 del Lock.INSTANCES[ns][name]
88 if len(Lock.INSTANCES[ns]) == 0:
89 del Lock.INSTANCES[ns]
90 elif name in Lock.BASE_INSTANCES:
91 del Lock.BASE_INSTANCES[name]
93 ns = Lock._mknamespace(ns)
94 path = os.path.join(Lock.BASE_DIR, ns, name)
95 if os.path.exists(path): 95 ↛ 96line 95 didn't jump to line 96, because the condition on line 95 was never true
96 Lock._unlink(path)
98 cleanup = staticmethod(cleanup)
100 def cleanupAll(ns=None):
101 ns = Lock._mknamespace(ns)
102 nspath = os.path.join(Lock.BASE_DIR, ns)
104 if not os.path.exists(nspath): 104 ↛ 107line 104 didn't jump to line 107, because the condition on line 104 was never false
105 return
107 for file in os.listdir(nspath):
108 path = os.path.join(nspath, file)
109 Lock._unlink(path)
111 Lock._rmdir(nspath)
113 cleanupAll = staticmethod(cleanupAll)
114 #
115 # Lock and attribute file management
116 #
118 def _mkdirs(path):
119 """Concurrent makedirs() catching EEXIST."""
120 if os.path.exists(path):
121 return
122 try:
123 os.makedirs(path)
124 except OSError as e:
125 if e.errno != errno.EEXIST: 125 ↛ exitline 125 didn't return from function '_mkdirs', because the condition on line 125 was never false
126 raise LockException("Failed to makedirs(%s)" % path)
127 _mkdirs = staticmethod(_mkdirs)
129 def _unlink(path):
130 """Non-raising unlink()."""
131 util.SMlog("lock: unlinking lock file %s" % path)
132 try:
133 os.unlink(path)
134 except Exception as e:
135 util.SMlog("Failed to unlink(%s): %s" % (path, e))
136 _unlink = staticmethod(_unlink)
138 def _rmdir(path):
139 """Non-raising rmdir()."""
140 util.SMlog("lock: removing lock dir %s" % path)
141 try:
142 os.rmdir(path)
143 except Exception as e:
144 util.SMlog("Failed to rmdir(%s): %s" % (path, e))
145 _rmdir = staticmethod(_rmdir)
148class LockImplementation(object):
150 def __init__(self, name, ns=None):
151 self.lockfile = None
153 self.ns = Lock._mknamespace(ns)
155 assert not name.startswith(".")
156 assert name.find(os.path.sep) < 0
157 self.name = name
159 self.count = 0
161 self._open()
163 def _open(self):
164 """Create and open the lockable attribute base, if it doesn't exist.
165 (But don't lock it yet.)"""
167 # one directory per namespace
168 self.nspath = os.path.join(Lock.BASE_DIR, self.ns)
170 # the lockfile inside that namespace directory per namespace
171 self.lockpath = os.path.join(self.nspath, self.name)
173 number_of_enoent_retries = 10
175 while True:
176 Lock._mkdirs(self.nspath)
178 try:
179 self._open_lockfile()
180 except IOError as e:
181 # If another lock within the namespace has already
182 # cleaned up the namespace by removing the directory,
183 # _open_lockfile raises an ENOENT, in this case we retry.
184 if e.errno == errno.ENOENT: 184 ↛ 188line 184 didn't jump to line 188, because the condition on line 184 was never false
185 if number_of_enoent_retries > 0: 185 ↛ 188line 185 didn't jump to line 188, because the condition on line 185 was never false
186 number_of_enoent_retries -= 1
187 continue
188 raise
189 break
191 fd = self.lockfile.fileno()
192 self.lock = flock.WriteLock(fd)
194 def _open_lockfile(self):
195 """Provide a seam, so extreme situations could be tested"""
196 util.SMlog("lock: opening lock file %s" % self.lockpath)
197 self.lockfile = open(self.lockpath, "w+")
199 def _close(self):
200 """Close the lock, which implies releasing the lock."""
201 if self.lockfile is not None:
202 if self.held(): 202 ↛ 204line 202 didn't jump to line 204, because the condition on line 202 was never true
203 # drop all reference counts
204 self.count = 0
205 self.release()
206 self.lockfile.close()
207 util.SMlog("lock: closed %s" % self.lockpath)
208 self.lockfile = None
210 __del__ = _close
212 def cleanup(self, name, ns=None):
213 Lock.cleanup(name, ns)
215 def cleanupAll(self, ns=None):
216 Lock.cleanupAll(ns)
217 #
218 # Actual Locking
219 #
221 def acquire(self):
222 """Blocking lock aquisition, with warnings. We don't expect to lock a
223 lot. If so, not to collide. Coarse log statements should be ok
224 and aid debugging."""
225 if not self.held():
226 if not self.lock.trylock(): 226 ↛ 227line 226 didn't jump to line 227, because the condition on line 226 was never true
227 util.SMlog("Failed to lock %s on first attempt, " % self.lockpath
228 + "blocked by PID %d" % self.lock.test())
229 self.lock.lock()
230 if VERBOSE: 230 ↛ 232line 230 didn't jump to line 232, because the condition on line 230 was never false
231 util.SMlog("lock: acquired %s" % self.lockpath)
232 self.count += 1
234 def acquireNoblock(self):
235 """Acquire lock if possible, or return false if lock already held"""
236 if not self.held():
237 exists = os.path.exists(self.lockpath)
238 ret = self.lock.trylock()
239 if VERBOSE: 239 ↛ 245line 239 didn't jump to line 245, because the condition on line 239 was never false
240 util.SMlog("lock: tried lock %s, acquired: %s (exists: %s)" % \
241 (self.lockpath, ret, exists))
242 else:
243 ret = True
245 if ret: 245 ↛ 248line 245 didn't jump to line 248, because the condition on line 245 was never false
246 self.count += 1
248 return ret
250 def held(self):
251 """True if @self acquired the lock, False otherwise."""
252 return self.lock.held()
254 def release(self):
255 """Release a previously acquired lock."""
256 if self.count >= 1: 256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false
257 self.count -= 1
259 if self.count > 0:
260 return
262 self.lock.unlock()
263 if VERBOSE: 263 ↛ exitline 263 didn't return from function 'release', because the condition on line 263 was never false
264 util.SMlog("lock: released %s" % self.lockpath)