| """Task replacement and execution share a lock; queued old actions are rejected.""" | |
| from threading import RLock | |
| from uuid import uuid4 | |
| from .actions import Ticket | |
| class Rejected(ValueError): | |
| pass | |
| class Authority: | |
| def __init__(self): | |
| self.lock = RLock() | |
| self.session_id = uuid4().hex | |
| self.task_id = "" | |
| self.epoch = 0 | |
| self.sequence = 0 | |
| self.goal = "" | |
| def replace(self, goal): | |
| if not isinstance(goal, str) or not goal.strip(): | |
| raise ValueError("task needs a nonempty goal") | |
| with self.lock: | |
| self.task_id = uuid4().hex | |
| self.epoch += 1 | |
| self.sequence = 0 | |
| self.goal = goal | |
| def ticket(self, state): | |
| with self.lock: | |
| if not self.task_id: | |
| raise Rejected("NO_TASK") | |
| return Ticket(self.session_id, self.task_id, self.epoch, self.sequence, | |
| state.document_id, state.revision, state.state_hash) | |
| def validate(self, ticket, state): | |
| if ticket != self.ticket(state): | |
| raise Rejected("STALE_AUTHORITY_OR_OBSERVATION") | |
| def consume(self): | |
| self.sequence += 1 | |