Coverage for manila/coordination.py: 93%

66 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Licensed under the Apache License, Version 2.0 (the "License"); you may 

2# not use this file except in compliance with the License. You may obtain 

3# a copy of the License at 

4# 

5# http://www.apache.org/licenses/LICENSE-2.0 

6# 

7# Unless required by applicable law or agreed to in writing, software 

8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

10# License for the specific language governing permissions and limitations 

11# under the License. 

12 

13"""Tooz Coordination and locking utilities.""" 

14 

15import inspect 

16 

17import decorator 

18from oslo_config import cfg 

19from oslo_log import log 

20from oslo_utils import uuidutils 

21from tooz import coordination 

22from tooz import locking 

23 

24from manila import exception 

25from manila.i18n import _ 

26 

27 

28LOG = log.getLogger(__name__) 

29 

30 

31coordination_opts = [ 

32 cfg.StrOpt('backend_url', 

33 secret=True, 

34 default='file://$state_path', 

35 help='The back end URL to use for distributed coordination.') 

36] 

37 

38CONF = cfg.CONF 

39CONF.register_opts(coordination_opts, group='coordination') 

40 

41 

42class Coordinator(object): 

43 """Tooz coordination wrapper. 

44 

45 Coordination member id is created from concatenated `prefix` and 

46 `agent_id` parameters. 

47 

48 :param str agent_id: Agent identifier 

49 :param str prefix: Used to provide member identifier with a 

50 meaningful prefix. 

51 """ 

52 

53 def __init__(self, agent_id=None, prefix=''): 

54 self.coordinator = None 

55 self.agent_id = agent_id or uuidutils.generate_uuid() 

56 self.started = False 

57 self.prefix = prefix 

58 

59 def start(self): 

60 """Connect to coordination back end.""" 

61 if self.started: 

62 return 

63 

64 # NOTE(gouthamr): Tooz expects member_id as a byte string. 

65 member_id = (self.prefix + self.agent_id).encode('ascii') 

66 self.coordinator = coordination.get_coordinator( 

67 cfg.CONF.coordination.backend_url, member_id) 

68 self.coordinator.start(start_heart=True) 

69 self.started = True 

70 

71 def stop(self): 

72 """Disconnect from coordination back end.""" 

73 msg = 'Stopped Coordinator (Agent ID: %(agent)s, prefix: %(prefix)s)' 

74 msg_args = {'agent': self.agent_id, 'prefix': self.prefix} 

75 if self.started: 75 ↛ 80line 75 didn't jump to line 80 because the condition on line 75 was always true

76 self.coordinator.stop() 

77 self.coordinator = None 

78 self.started = False 

79 

80 LOG.info(msg, msg_args) 

81 

82 def get_lock(self, name): 

83 """Return a Tooz back end lock. 

84 

85 :param str name: The lock name that is used to identify it 

86 across all nodes. 

87 """ 

88 # NOTE(gouthamr): Tooz expects lock name as a byte string 

89 lock_name = (self.prefix + name).encode('ascii') 

90 if self.started: 90 ↛ 93line 90 didn't jump to line 93 because the condition on line 90 was always true

91 return self.coordinator.get_lock(lock_name) 

92 else: 

93 raise exception.LockCreationFailed(_('Coordinator uninitialized.')) 

94 

95 

96LOCK_COORDINATOR = Coordinator(prefix='manila-') 

97 

98 

99class Lock(locking.Lock): 

100 """Lock with dynamic name. 

101 

102 :param str lock_name: Lock name. 

103 :param dict lock_data: Data for lock name formatting. 

104 :param coordinator: Coordinator object to use when creating lock. 

105 Defaults to the global coordinator. 

106 

107 Using it like so:: 

108 

109 with Lock('mylock'): 

110 ... 

111 

112 ensures that only one process at a time will execute code in context. 

113 Lock name can be formatted using Python format string syntax:: 

114 

115 Lock('foo-{share.id}, {'share': ...,}') 

116 

117 Available field names are keys of lock_data. 

118 """ 

119 def __init__(self, lock_name, lock_data=None, coordinator=None): 

120 super(Lock, self).__init__(str(id(self))) 

121 lock_data = lock_data or {} 

122 self.coordinator = coordinator or LOCK_COORDINATOR 

123 self.blocking = True 

124 self.lock = self._prepare_lock(lock_name, lock_data) 

125 

126 def _prepare_lock(self, lock_name, lock_data): 

127 if not isinstance(lock_name, str): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true

128 raise ValueError(_('Not a valid string: %s') % lock_name) 

129 return self.coordinator.get_lock(lock_name.format(**lock_data)) 

130 

131 def acquire(self, blocking=None): 

132 """Attempts to acquire lock. 

133 

134 :param blocking: If True, blocks until the lock is acquired. If False, 

135 returns right away. Otherwise, the value is used as a timeout 

136 value and the call returns maximum after this number of seconds. 

137 :return: returns true if acquired (false if not) 

138 :rtype: bool 

139 """ 

140 blocking = self.blocking if blocking is None else blocking 

141 return self.lock.acquire(blocking=blocking) 

142 

143 def release(self): 

144 """Attempts to release lock. 

145 

146 The behavior of releasing a lock which was not acquired in the first 

147 place is undefined. 

148 """ 

149 self.lock.release() 

150 

151 

152def synchronized(lock_name, blocking=True, coordinator=None): 

153 """Synchronization decorator. 

154 

155 :param str lock_name: Lock name. 

156 :param blocking: If True, blocks until the lock is acquired. 

157 If False, raises exception when not acquired. Otherwise, 

158 the value is used as a timeout value and if lock is not acquired 

159 after this number of seconds exception is raised. 

160 :param coordinator: Coordinator object to use when creating lock. 

161 Defaults to the global coordinator. 

162 :raises tooz.coordination.LockAcquireFailed: if lock is not acquired 

163 

164 Decorating a method like so:: 

165 

166 @synchronized('mylock') 

167 def foo(self, *args): 

168 ... 

169 

170 ensures that only one process will execute the foo method at a time. 

171 

172 Different methods can share the same lock:: 

173 

174 @synchronized('mylock') 

175 def foo(self, *args): 

176 ... 

177 

178 @synchronized('mylock') 

179 def bar(self, *args): 

180 ... 

181 

182 This way only one of either foo or bar can be executing at a time. 

183 

184 Lock name can be formatted using Python format string syntax:: 

185 

186 @synchronized('{f_name}-{shr.id}-{snap[name]}') 

187 def foo(self, shr, snap): 

188 ... 

189 

190 Available field names are: decorated function parameters and 

191 `f_name` as a decorated function name. 

192 """ 

193 @decorator.decorator 

194 def _synchronized(f, *a, **k): 

195 call_args = inspect.getcallargs(f, *a, **k) 

196 call_args['f_name'] = f.__name__ 

197 lock = Lock(lock_name, call_args, coordinator) 

198 with lock(blocking): 

199 LOG.debug('Lock "%(name)s" acquired by "%(function)s".', 

200 {'name': lock_name, 'function': f.__name__}) 

201 return f(*a, **k) 

202 return _synchronized