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" 

31 

32class LockException(util.SMException): 

33 pass 

34 

35 

36class Lock(object): 

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

38 attributes.""" 

39 

40 BASE_DIR = "/var/lock/sm" 

41 

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

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

44 

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 

52 

53 if name not in instances: 

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

55 return instances[name] 

56 

57 def acquire(self): 

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

59 

60 def acquireNoblock(self): 

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

62 

63 def release(self): 

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

65 

66 def held(self): 

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

68 

69 @staticmethod 

70 def _mknamespace(ns): 

71 

72 if ns is None: 

73 return ".nil" 

74 

75 assert not ns.startswith(".") 

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

77 return ns 

78 

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

86 

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] 

97 

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) 

102 

103 @staticmethod 

104 def cleanupAll(ns=None): 

105 ns = Lock._mknamespace(ns) 

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

107 

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 

110 

111 for file in os.listdir(nspath): 

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

113 Lock._unlink(path) 

114 

115 Lock._rmdir(nspath) 

116 

117 # 

118 # Lock and attribute file management 

119 # 

120 

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) 

131 

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

140 

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

149 

150 

151class LockImplementation(object): 

152 

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

154 self.lockfile = None 

155 

156 self.ns = Lock._mknamespace(ns) 

157 

158 assert not name.startswith(".") 

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

160 self.name = name 

161 

162 self.count = 0 

163 

164 self._open() 

165 

166 def _open(self): 

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

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

169 

170 # one directory per namespace 

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

172 

173 # the lockfile inside that namespace directory per namespace 

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

175 

176 number_of_enoent_retries = 10 

177 

178 while True: 

179 Lock._mkdirs(self.nspath) 

180 

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 

193 

194 fd = self.lockfile.fileno() 

195 self.lock = flock.WriteLock(fd) 

196 

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

201 

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 

212 

213 __del__ = _close 

214 

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

216 Lock.cleanup(name, ns) 

217 

218 def cleanupAll(self, ns=None): 

219 Lock.cleanupAll(ns) 

220 # 

221 # Actual Locking 

222 # 

223 

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 

236 

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 

247 

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 

250 

251 return ret 

252 

253 def held(self): 

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

255 return self.lock.held() 

256 

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 

261 

262 if self.count > 0: 

263 return 

264 

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)