Coverage for portality/lock.py: 93%

98 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-22 15:59 +0100

1""" 

2~~Lock:Feature~~ 

3""" 

4from portality import models 

5from portality.core import app 

6from portality.dao import ESMappingMissingError 

7 

8 

9class Locked(Exception): 

10 def __init__(self, message, lock): 

11 self.message = message 

12 self.lock = lock 

13 

14 

15def lock(type, id, username, timeout=None, blocking=False): 

16 """ 

17 ~~->Lock:Model~~ 

18 Obtain a lock on the object for the given username. If unable to obtain 

19 a lock, raise an exception 

20 """ 

21 if timeout is None: 

22 timeout = app.config.get("EDIT_LOCK_TIMEOUT", 1200) 

23 

24 l = _retrieve_latest_with_cleanup(type, id) 

25 

26 if l is None: 

27 l = models.Lock() 

28 l.set_about(id) 

29 l.set_type(type) 

30 l.set_username(username) 

31 l.expires_in(timeout) 

32 l.save(blocking=blocking) 

33 return l 

34 

35 indate = not l.is_expired() 

36 yours = l.username == username 

37 

38 if not yours and not indate: 

39 # overwrite the old lock with a new one 

40 l.set_username(username) 

41 l.expires_in(timeout) 

42 l.save(blocking=blocking) 

43 return l 

44 

45 if not yours and indate: 

46 # someone else holds the lock, so raise exception 

47 raise Locked("Object is locked by another user", l) 

48 

49 if yours: 

50 # if the lock would expire within the time specified by the timeout, extend it 

51 if l.would_expire_within(timeout): 

52 l.expires_in(timeout) 

53 l.save(blocking=blocking) 

54 return l 

55 

56 # shouldn't ever get here - if we do something is bust 

57 raise Locked("Unable to resolve lock state", None) 

58 

59 

60def unlock(type, id, username): 

61 l = _retrieve_latest_with_cleanup(type, id) 

62 

63 if l is None: 

64 return True 

65 

66 if l.username == username: 

67 l.delete() 

68 return True 

69 

70 return False 

71 

72 

73def has_lock(type, id, username): 

74 l = _retrieve_latest_with_cleanup(type, id) 

75 

76 if l is None: 

77 return False 

78 

79 indate = not l.is_expired() 

80 yours = l.username == username 

81 

82 if indate and yours: 

83 return True 

84 

85 return False 

86 

87 

88def batch_lock(type, ids, username, timeout=None): 

89 """ 

90 Batch lock succeeds and fails as a unit. If locks can't be obtained on everything 

91 then all locks are released. 

92 

93 Works by attempting to lock everything, and then backing out, unlocking already locked 

94 resources, when it first encounters a locked record 

95 

96 :param type: 

97 :param ids: 

98 :param username: 

99 :return: 

100 """ 

101 locked = [] 

102 locks = [] 

103 abort = False 

104 failon = None 

105 for id in ids: 

106 try: 

107 lock(type, id, username, timeout) 

108 locked.append(id) 

109 locks.append(lock) 

110 except Locked as e: 

111 abort = True 

112 failon = id 

113 break 

114 

115 if abort: 

116 for id in locked: 

117 unlock(type, id, username) 

118 raise Locked("Batch lock failed on id {x}".format(x=failon), None) 

119 

120 return locks 

121 

122 

123def batch_unlock(type, ids, username): 

124 """ 

125 Calls unlock on all resources. Unlock may fail on one or more resources 

126 without affecting the others. 

127 

128 :param type: 

129 :param ids: 

130 :param username: 

131 :return: 

132 """ 

133 success = [] 

134 fail = [] 

135 for id in ids: 

136 unlocked = unlock(type, id, username) 

137 if unlocked: 

138 success.append(id) 

139 else: 

140 fail.append(id) 

141 

142 return {"success": success, "fail" : fail} 

143 

144 

145def _retrieve_latest_with_cleanup(type, id): 

146 """ 

147 ~~->Lock:Query~~ 

148 :param type: 

149 :param id: 

150 :return: 

151 """ 

152 # query for any locks on this id. There is a chance there's more than one, if two locks 

153 # are created at the same time 

154 l = None 

155 

156 q = LockQuery(type, id) 

157 try: 

158 ls = models.Lock.q2obj(q=q.query()) 

159 except ESMappingMissingError: 

160 return l 

161 

162 # if there's more than one lock, keep the most recent (the query is sorted) and 

163 # delete all the rest. Code that uses locks should check for a lock before each 

164 # operation, and handle the fact that it may lose its lock. 

165 if len(ls) > 0: 

166 l = ls[0] 

167 if len(ls) > 1: 

168 for i in range(1, len(ls)): 

169 ls[i].delete() 

170 

171 return l 

172 

173 

174class LockQuery(object): 

175 """ 

176 ~~Lock:Query->Elasticsearch:Technology~~ 

177 """ 

178 def __init__(self, type, about): 

179 self.about = about 

180 self.type = type 

181 

182 def query(self): 

183 return { 

184 "track_total_hits" : True, 

185 "query" : { 

186 "bool" : { 

187 "must" : [ 

188 {"term" : {"about.exact" : self.about}}, 

189 {"term" : {"type.exact" : self.type}} 

190 ] 

191 } 

192 }, 

193 "sort" : [{"last_updated" : {"order" : "desc"}}] 

194 }