Coverage for portality/lock.py: 93%
98 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-22 15:59 +0100
« 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
9class Locked(Exception):
10 def __init__(self, message, lock):
11 self.message = message
12 self.lock = lock
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)
24 l = _retrieve_latest_with_cleanup(type, id)
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
35 indate = not l.is_expired()
36 yours = l.username == username
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
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)
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
56 # shouldn't ever get here - if we do something is bust
57 raise Locked("Unable to resolve lock state", None)
60def unlock(type, id, username):
61 l = _retrieve_latest_with_cleanup(type, id)
63 if l is None:
64 return True
66 if l.username == username:
67 l.delete()
68 return True
70 return False
73def has_lock(type, id, username):
74 l = _retrieve_latest_with_cleanup(type, id)
76 if l is None:
77 return False
79 indate = not l.is_expired()
80 yours = l.username == username
82 if indate and yours:
83 return True
85 return False
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.
93 Works by attempting to lock everything, and then backing out, unlocking already locked
94 resources, when it first encounters a locked record
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
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)
120 return locks
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.
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)
142 return {"success": success, "fail" : fail}
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
156 q = LockQuery(type, id)
157 try:
158 ls = models.Lock.q2obj(q=q.query())
159 except ESMappingMissingError:
160 return l
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()
171 return l
174class LockQuery(object):
175 """
176 ~~Lock:Query->Elasticsearch:Technology~~
177 """
178 def __init__(self, type, about):
179 self.about = about
180 self.type = type
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 }