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"
32class LockException(util.SMException):
33 pass
36class Lock(object):
37 """Simple file-based lock on a local FS. With shared reader/writer
38 attributes."""
40 BASE_DIR = "/var/lock/sm"
42 INSTANCES: Dict[str, 'LockImplementation'] = {}
43 BASE_INSTANCES: Dict[str, 'LockImplementation'] = {}
45 def __new__(cls, name, ns=None, *args, **kwargs):
46 if ns:
47 if ns not in Lock.INSTANCES:
48 Lock.INSTANCES[ns] = {}
49 instances = Lock.INSTANCES[ns]
50 else:
51 instances = Lock.BASE_INSTANCES
53 if name not in instances:
54 instances[name] = LockImplementation(name, ns)
55 return instances[name]
57 def acquire(self):
58 raise NotImplementedError("Lock methods implemented in LockImplementation")
60 def acquireNoblock(self):
61 raise NotImplementedError("Lock methods implemented in LockImplementation")
63 def release(self):
64 raise NotImplementedError("Lock methods implemented in LockImplementation")
66 def held(self):
67 raise NotImplementedError("Lock methods implemented in LockImplementation")
69 @staticmethod
70 def _mknamespace(ns):
72 if ns is None:
73 return ".nil"
75 assert not ns.startswith(".")
76 assert ns.find(os.path.sep) < 0
77 return ns
79 @staticmethod
80 def clearAll():
81 """
82 Drop all lock instances, to be used when forking, but not execing
83 """
84 Lock.INSTANCES = {}
85 Lock.BASE_INSTANCES = {}
87 @staticmethod
88 def cleanup(name, ns=None):
89 if ns: 89 ↛ 95line 89 didn't jump to line 95, because the condition on line 89 was never false
90 if ns in Lock.INSTANCES: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 if name in Lock.INSTANCES[ns]:
92 del Lock.INSTANCES[ns][name]
93 if len(Lock.INSTANCES[ns]) == 0:
94 del Lock.INSTANCES[ns]
95 elif name in Lock.BASE_INSTANCES:
96 del Lock.BASE_INSTANCES[name]
98 ns = Lock._mknamespace(ns)
99 path = os.path.join(Lock.BASE_DIR, ns, name)
100 if os.path.exists(path): 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true
101 Lock._unlink(path)
103 @staticmethod
104 def cleanupAll(ns=None):
105 ns = Lock._mknamespace(ns)
106 nspath = os.path.join(Lock.BASE_DIR, ns)
108 if not os.path.exists(nspath): 108 ↛ 111line 108 didn't jump to line 111, because the condition on line 108 was never false
109 return
111 for file in os.listdir(nspath):
112 path = os.path.join(nspath, file)
113 Lock._unlink(path)
115 Lock._rmdir(nspath)
117 #
118 # Lock and attribute file management
119 #
121 @staticmethod
122 def _mkdirs(path):
123 """Concurrent makedirs() catching EEXIST."""
124 if os.path.exists(path):
125 return
126 try:
127 os.makedirs(path)
128 except OSError as e:
129 if e.errno != errno.EEXIST: 129 ↛ exitline 129 didn't return from function '_mkdirs', because the condition on line 129 was never false
130 raise LockException("Failed to makedirs(%s)" % path)
132 @staticmethod
133 def _unlink(path):
134 """Non-raising unlink()."""
135 util.SMlog("lock: unlinking lock file %s" % path)
136 try:
137 os.unlink(path)
138 except Exception as e:
139 util.SMlog("Failed to unlink(%s): %s" % (path, e))
141 @staticmethod
142 def _rmdir(path):
143 """Non-raising rmdir()."""
144 util.SMlog("lock: removing lock dir %s" % path)
145 try:
146 os.rmdir(path)
147 except Exception as e:
148 util.SMlog("Failed to rmdir(%s): %s" % (path, e))
151class LockImplementation(object):
153 def __init__(self, name, ns=None):
154 self.lockfile = None
156 self.ns = Lock._mknamespace(ns)
158 assert not name.startswith(".")
159 assert name.find(os.path.sep) < 0
160 self.name = name
162 self.count = 0
164 self._open()
166 def _open(self):
167 """Create and open the lockable attribute base, if it doesn't exist.
168 (But don't lock it yet.)"""
170 # one directory per namespace
171 self.nspath = os.path.join(Lock.BASE_DIR, self.ns)
173 # the lockfile inside that namespace directory per namespace
174 self.lockpath = os.path.join(self.nspath, self.name)
176 number_of_enoent_retries = 10
178 while True:
179 Lock._mkdirs(self.nspath)
181 try:
182 self._open_lockfile()
183 except IOError as e:
184 # If another lock within the namespace has already
185 # cleaned up the namespace by removing the directory,
186 # _open_lockfile raises an ENOENT, in this case we retry.
187 if e.errno == errno.ENOENT: 187 ↛ 191line 187 didn't jump to line 191, because the condition on line 187 was never false
188 if number_of_enoent_retries > 0: 188 ↛ 191line 188 didn't jump to line 191, because the condition on line 188 was never false
189 number_of_enoent_retries -= 1
190 continue
191 raise
192 break
194 fd = self.lockfile.fileno()
195 self.lock = flock.WriteLock(fd)
197 def _open_lockfile(self) -> None:
198 """Provide a seam, so extreme situations could be tested"""
199 util.SMlog("lock: opening lock file %s" % self.lockpath)
200 self.lockfile = open(self.lockpath, "w+")
202 def _close(self):
203 """Close the lock, which implies releasing the lock."""
204 if self.lockfile is not None:
205 if self.held(): 205 ↛ 207line 205 didn't jump to line 207, because the condition on line 205 was never true
206 # drop all reference counts
207 self.count = 0
208 self.release()
209 self.lockfile.close()
210 util.SMlog("lock: closed %s" % self.lockpath)
211 self.lockfile = None
213 __del__ = _close
215 def cleanup(self, name, ns=None):
216 Lock.cleanup(name, ns)
218 def cleanupAll(self, ns=None):
219 Lock.cleanupAll(ns)
220 #
221 # Actual Locking
222 #
224 def acquire(self):
225 """Blocking lock aquisition, with warnings. We don't expect to lock a
226 lot. If so, not to collide. Coarse log statements should be ok
227 and aid debugging."""
228 if not self.held():
229 if not self.lock.trylock(): 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true
230 util.SMlog("Failed to lock %s on first attempt, " % self.lockpath
231 + "blocked by PID %d" % self.lock.test())
232 self.lock.lock()
233 if VERBOSE: 233 ↛ 235line 233 didn't jump to line 235, because the condition on line 233 was never false
234 util.SMlog("lock: acquired %s" % self.lockpath)
235 self.count += 1
237 def acquireNoblock(self):
238 """Acquire lock if possible, or return false if lock already held"""
239 if not self.held():
240 exists = os.path.exists(self.lockpath)
241 ret = self.lock.trylock()
242 if VERBOSE: 242 ↛ 248line 242 didn't jump to line 248, because the condition on line 242 was never false
243 util.SMlog("lock: tried lock %s, acquired: %s (exists: %s)" % \
244 (self.lockpath, ret, exists))
245 else:
246 ret = True
248 if ret: 248 ↛ 251line 248 didn't jump to line 251, because the condition on line 248 was never false
249 self.count += 1
251 return ret
253 def held(self):
254 """True if @self acquired the lock, False otherwise."""
255 return self.lock.held()
257 def release(self):
258 """Release a previously acquired lock."""
259 if self.count >= 1: 259 ↛ 262line 259 didn't jump to line 262, because the condition on line 259 was never false
260 self.count -= 1
262 if self.count > 0:
263 return
265 self.lock.unlock()
266 if VERBOSE: 266 ↛ exitline 266 didn't return from function 'release', because the condition on line 266 was never false
267 util.SMlog("lock: released %s" % self.lockpath)