Hide keyboard shortcuts

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 

16 

17"""Serialization for concurrent operations""" 

18 

19from sm_typing import Dict 

20 

21import os 

22import errno 

23import flock 

24import util 

25 

26VERBOSE = True 

27 

28# Still just called "running" for backwards compatibility 

29LOCK_TYPE_GC_RUNNING = "running" 

30LOCK_TYPE_ISCSIADM_RUNNING = "isciadm_running" 

31LOCK_TYPE_SR = "sr" 

32 

33class LockException(util.SMException): 

34 pass 

35 

36 

37class Lock(object): 

38 """Simple file-based lock on a local FS. With shared reader/writer 

39 attributes.""" 

40 

41 BASE_DIR = "/var/lock/sm" 

42 

43 INSTANCES: Dict[str, 'LockImplementation'] = {} 

44 BASE_INSTANCES: Dict[str, 'LockImplementation'] = {} 

45 

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 

53 

54 if name not in instances: 

55 instances[name] = LockImplementation(name, ns) 

56 return instances[name] 

57 

58 def acquire(self): 

59 raise NotImplementedError("Lock methods implemented in LockImplementation") 

60 

61 def acquireNoblock(self): 

62 raise NotImplementedError("Lock methods implemented in LockImplementation") 

63 

64 def release(self): 

65 raise NotImplementedError("Lock methods implemented in LockImplementation") 

66 

67 def held(self): 

68 raise NotImplementedError("Lock methods implemented in LockImplementation") 

69 

70 @staticmethod 

71 def _mknamespace(ns): 

72 

73 if ns is None: 

74 return ".nil" 

75 

76 assert not ns.startswith(".") 

77 assert ns.find(os.path.sep) < 0 

78 return ns 

79 

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 = {} 

87 

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] 

98 

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) 

103 

104 @staticmethod 

105 def cleanupAll(ns=None): 

106 ns = Lock._mknamespace(ns) 

107 nspath = os.path.join(Lock.BASE_DIR, ns) 

108 

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 

111 

112 for file in os.listdir(nspath): 

113 path = os.path.join(nspath, file) 

114 Lock._unlink(path) 

115 

116 Lock._rmdir(nspath) 

117 

118 # 

119 # Lock and attribute file management 

120 # 

121 

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) 

132 

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)) 

141 

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)) 

150 

151 

152class LockImplementation(object): 

153 

154 def __init__(self, name, ns=None): 

155 self.lockfile = None 

156 

157 self.ns = Lock._mknamespace(ns) 

158 

159 assert not name.startswith(".") 

160 assert name.find(os.path.sep) < 0 

161 self.name = name 

162 

163 self.count = 0 

164 

165 self._open() 

166 

167 def _open(self): 

168 """Create and open the lockable attribute base, if it doesn't exist. 

169 (But don't lock it yet.)""" 

170 

171 # one directory per namespace 

172 self.nspath = os.path.join(Lock.BASE_DIR, self.ns) 

173 

174 # the lockfile inside that namespace directory per namespace 

175 self.lockpath = os.path.join(self.nspath, self.name) 

176 

177 number_of_enoent_retries = 10 

178 

179 while True: 

180 Lock._mkdirs(self.nspath) 

181 

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 

194 

195 fd = self.lockfile.fileno() 

196 self.lock = flock.WriteLock(fd) 

197 

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+") 

202 

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 

213 

214 __del__ = _close 

215 

216 def cleanup(self, name, ns=None): 

217 Lock.cleanup(name, ns) 

218 

219 def cleanupAll(self, ns=None): 

220 Lock.cleanupAll(ns) 

221 # 

222 # Actual Locking 

223 # 

224 

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 

237 

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 

248 

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 

251 

252 return ret 

253 

254 def held(self): 

255 """True if @self acquired the lock, False otherwise.""" 

256 return self.lock.held() 

257 

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 

262 

263 if self.count > 0: 

264 return 

265 

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)