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"""
19from sm_typing import Dict
21import os
22import errno
23import flock
24import util
26VERBOSE = True
28# Still just called "running" for backwards compatibility
29LOCK_TYPE_GC_RUNNING = "running"
30LOCK_TYPE_ISCSIADM_RUNNING = "isciadm_running"
31LOCK_TYPE_SR = "sr"
33class LockException(util.SMException):
34 pass
37class Lock(object):
38 """Simple file-based lock on a local FS. With shared reader/writer
39 attributes."""
41 BASE_DIR = "/var/lock/sm"
43 INSTANCES: Dict[str, 'LockImplementation'] = {}
44 BASE_INSTANCES: Dict[str, 'LockImplementation'] = {}
46 def __new__(cls, name, ns=None, *args, **kwargs):
47 if ns:
48 if ns not in Lock.INSTANCES:
49 Lock.INSTANCES[ns] = {}
50 instances = Lock.INSTANCES[ns]
51 else:
52 instances = Lock.BASE_INSTANCES
54 if name not in instances:
55 instances[name] = LockImplementation(name, ns)
56 return instances[name]
58 def acquire(self):
59 raise NotImplementedError("Lock methods implemented in LockImplementation")
61 def acquireNoblock(self):
62 raise NotImplementedError("Lock methods implemented in LockImplementation")
64 def release(self):
65 raise NotImplementedError("Lock methods implemented in LockImplementation")
67 def held(self):
68 raise NotImplementedError("Lock methods implemented in LockImplementation")
70 @staticmethod
71 def _mknamespace(ns):
73 if ns is None:
74 return ".nil"
76 assert not ns.startswith(".")
77 assert ns.find(os.path.sep) < 0
78 return ns
80 @staticmethod
81 def clearAll():
82 """
83 Drop all lock instances, to be used when forking, but not execing
84 """
85 Lock.INSTANCES = {}
86 Lock.BASE_INSTANCES = {}
88 @staticmethod
89 def cleanup(name, ns=None):
90 if ns: 90 ↛ 96line 90 didn't jump to line 96, because the condition on line 90 was never false
91 if ns in Lock.INSTANCES: 91 ↛ 92line 91 didn't jump to line 92, because the condition on line 91 was never true
92 if name in Lock.INSTANCES[ns]:
93 del Lock.INSTANCES[ns][name]
94 if len(Lock.INSTANCES[ns]) == 0:
95 del Lock.INSTANCES[ns]
96 elif name in Lock.BASE_INSTANCES:
97 del Lock.BASE_INSTANCES[name]
99 ns = Lock._mknamespace(ns)
100 path = os.path.join(Lock.BASE_DIR, ns, name)
101 if os.path.exists(path): 101 ↛ 102line 101 didn't jump to line 102, because the condition on line 101 was never true
102 Lock._unlink(path)
104 @staticmethod
105 def cleanupAll(ns=None):
106 ns = Lock._mknamespace(ns)
107 nspath = os.path.join(Lock.BASE_DIR, ns)
109 if not os.path.exists(nspath): 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false
110 return
112 for file in os.listdir(nspath):
113 path = os.path.join(nspath, file)
114 Lock._unlink(path)
116 Lock._rmdir(nspath)
118 #
119 # Lock and attribute file management
120 #
122 @staticmethod
123 def _mkdirs(path):
124 """Concurrent makedirs() catching EEXIST."""
125 if os.path.exists(path):
126 return
127 try:
128 os.makedirs(path)
129 except OSError as e:
130 if e.errno != errno.EEXIST: 130 ↛ exitline 130 didn't return from function '_mkdirs', because the condition on line 130 was never false
131 raise LockException("Failed to makedirs(%s)" % path)
133 @staticmethod
134 def _unlink(path):
135 """Non-raising unlink()."""
136 util.SMlog("lock: unlinking lock file %s" % path)
137 try:
138 os.unlink(path)
139 except Exception as e:
140 util.SMlog("Failed to unlink(%s): %s" % (path, e))
142 @staticmethod
143 def _rmdir(path):
144 """Non-raising rmdir()."""
145 util.SMlog("lock: removing lock dir %s" % path)
146 try:
147 os.rmdir(path)
148 except Exception as e:
149 util.SMlog("Failed to rmdir(%s): %s" % (path, e))
152class LockImplementation(object):
154 def __init__(self, name, ns=None):
155 self.lockfile = None
157 self.ns = Lock._mknamespace(ns)
159 assert not name.startswith(".")
160 assert name.find(os.path.sep) < 0
161 self.name = name
163 self.count = 0
165 self._open()
167 def _open(self):
168 """Create and open the lockable attribute base, if it doesn't exist.
169 (But don't lock it yet.)"""
171 # one directory per namespace
172 self.nspath = os.path.join(Lock.BASE_DIR, self.ns)
174 # the lockfile inside that namespace directory per namespace
175 self.lockpath = os.path.join(self.nspath, self.name)
177 number_of_enoent_retries = 10
179 while True:
180 Lock._mkdirs(self.nspath)
182 try:
183 self._open_lockfile()
184 except IOError as e:
185 # If another lock within the namespace has already
186 # cleaned up the namespace by removing the directory,
187 # _open_lockfile raises an ENOENT, in this case we retry.
188 if e.errno == errno.ENOENT: 188 ↛ 192line 188 didn't jump to line 192, because the condition on line 188 was never false
189 if number_of_enoent_retries > 0: 189 ↛ 192line 189 didn't jump to line 192, because the condition on line 189 was never false
190 number_of_enoent_retries -= 1
191 continue
192 raise
193 break
195 fd = self.lockfile.fileno()
196 self.lock = flock.WriteLock(fd)
198 def _open_lockfile(self) -> None:
199 """Provide a seam, so extreme situations could be tested"""
200 util.SMlog("lock: opening lock file %s" % self.lockpath)
201 self.lockfile = open(self.lockpath, "w+")
203 def _close(self):
204 """Close the lock, which implies releasing the lock."""
205 if self.lockfile is not None:
206 if self.held(): 206 ↛ 208line 206 didn't jump to line 208, because the condition on line 206 was never true
207 # drop all reference counts
208 self.count = 0
209 self.release()
210 self.lockfile.close()
211 util.SMlog("lock: closed %s" % self.lockpath)
212 self.lockfile = None
214 __del__ = _close
216 def cleanup(self, name, ns=None):
217 Lock.cleanup(name, ns)
219 def cleanupAll(self, ns=None):
220 Lock.cleanupAll(ns)
221 #
222 # Actual Locking
223 #
225 def acquire(self):
226 """Blocking lock aquisition, with warnings. We don't expect to lock a
227 lot. If so, not to collide. Coarse log statements should be ok
228 and aid debugging."""
229 if not self.held():
230 if not self.lock.trylock(): 230 ↛ 231line 230 didn't jump to line 231, because the condition on line 230 was never true
231 util.SMlog("Failed to lock %s on first attempt, " % self.lockpath
232 + "blocked by PID %d" % self.lock.test())
233 self.lock.lock()
234 if VERBOSE: 234 ↛ 236line 234 didn't jump to line 236, because the condition on line 234 was never false
235 util.SMlog("lock: acquired %s" % self.lockpath)
236 self.count += 1
238 def acquireNoblock(self):
239 """Acquire lock if possible, or return false if lock already held"""
240 if not self.held():
241 exists = os.path.exists(self.lockpath)
242 ret = self.lock.trylock()
243 if VERBOSE: 243 ↛ 249line 243 didn't jump to line 249, because the condition on line 243 was never false
244 util.SMlog("lock: tried lock %s, acquired: %s (exists: %s)" % \
245 (self.lockpath, ret, exists))
246 else:
247 ret = True
249 if ret: 249 ↛ 252line 249 didn't jump to line 252, because the condition on line 249 was never false
250 self.count += 1
252 return ret
254 def held(self):
255 """True if @self acquired the lock, False otherwise."""
256 return self.lock.held()
258 def release(self):
259 """Release a previously acquired lock."""
260 if self.count >= 1: 260 ↛ 263line 260 didn't jump to line 263, because the condition on line 260 was never false
261 self.count -= 1
263 if self.count > 0:
264 return
266 self.lock.unlock()
267 if VERBOSE: 267 ↛ exitline 267 didn't return from function 'release', because the condition on line 267 was never false
268 util.SMlog("lock: released %s" % self.lockpath)