Class: Agent::Lock::Manager
- Inherits:
-
Object
- Object
- Agent::Lock::Manager
- Defined in:
- lib/agent/lock/manager.rb
Overview
Every decision this gem makes, with nothing printed.
Each verb returns a Result: a status the caller switches on, the records involved, and the exit code the CLI should hand back. Keeping the decisions here and the wording in the commands is what lets the whole lifecycle be tested without capturing output.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_STALE_MINUTES =
120
Instance Attribute Summary collapse
-
#identity ⇒ Object
readonly
Returns the value of attribute identity.
-
#store ⇒ Object
readonly
Returns the value of attribute store.
-
#tree ⇒ Object
readonly
Returns the value of attribute tree.
Instance Method Summary collapse
-
#acquire(path, intent: "unspecified", enforce: false, force: false) ⇒ Result
Claim a scope, unless something overlapping stands in the way.
- #break_lock(path) ⇒ Result
-
#check(path) ⇒ Result
Whether this session may write in a scope.
-
#initialize(tree: Tree.for, identity: Identity.current, stale_minutes: nil, store: nil) ⇒ Manager
constructor
A new instance of Manager.
-
#list ⇒ Result
Every lock in the store, this tree's siblings included.
- #mine ⇒ Result
-
#note(path, text) ⇒ Result
Write a line into a lock this session holds, so that a machine coming back up has something better than the diff to work out where it was.
-
#reap ⇒ Array<Record>
Locks whose holder is provably gone, or, for a holder on another host that cannot be asked, that nobody has touched in so long that nobody can say.
- #release(path) ⇒ Result
- #release_all ⇒ Result
-
#resume(path) ⇒ Result
Take an interrupted lock back, notes and all.
-
#stale_minutes ⇒ Integer
How long a lock whose holder cannot be checked is trusted for, and how long a live one goes untouched before it is reported stale.
Constructor Details
#initialize(tree: Tree.for, identity: Identity.current, stale_minutes: nil, store: nil) ⇒ Manager
Returns a new instance of Manager.
31 32 33 34 35 36 |
# File 'lib/agent/lock/manager.rb', line 31 def initialize(tree: Tree.for, identity: Identity.current, stale_minutes: nil, store: nil) @tree = tree @store = store || Store.for(tree) @identity = identity @stale_minutes = stale_minutes end |
Instance Attribute Details
#identity ⇒ Object (readonly)
Returns the value of attribute identity.
29 30 31 |
# File 'lib/agent/lock/manager.rb', line 29 def identity @identity end |
#store ⇒ Object (readonly)
Returns the value of attribute store.
29 30 31 |
# File 'lib/agent/lock/manager.rb', line 29 def store @store end |
#tree ⇒ Object (readonly)
Returns the value of attribute tree.
29 30 31 |
# File 'lib/agent/lock/manager.rb', line 29 def tree @tree end |
Instance Method Details
#acquire(path, intent: "unspecified", enforce: false, force: false) ⇒ Result
Claim a scope, unless something overlapping stands in the way.
The scan for conflicts and the write it justifies happen inside the
store's mutex. create refuses only an identical scope, so without it
ten sessions claiming lib/** and lib/a.rb at once each saw a clear
field and each wrote a lock, leaving overlapping claims that all
reported success. Freezing stays outside: it walks the tree and runs
chflags, and every other session would be waiting on it.
60 61 62 63 64 65 66 |
# File 'lib/agent/lock/manager.rb', line 60 def acquire(path, intent: "unspecified", enforce: false, force: false) scope = Scope.parse(path, tree: tree) outcome = store.synchronize { take(scope, intent) } return outcome unless enforce && outcome.status == :acquired enforce_on(outcome.record, scope, force: force) end |
#break_lock(path) ⇒ Result
118 119 120 121 122 123 124 125 |
# File 'lib/agent/lock/manager.rb', line 118 def break_lock(path) scope = Scope.parse(path, tree: tree) record = store.find(scope) return result(:not_found, []) if record.nil? drop(record) result(:broken, [record]) end |
#check(path) ⇒ Result
Whether this session may write in a scope. A parent's lock alone does not make it :mine, since the child still has to claim its own corner before its siblings can see it there; to the child, that scope is :free to claim.
87 88 89 90 91 92 93 94 95 |
# File 'lib/agent/lock/manager.rb', line 87 def check(path) scope = Scope.parse(path, tree: tree) reap blocking, family = conflicts(scope).partition { |record| record.blocks?(identity) } return result(:held, blocking) if blocking.any? own = family.select { |record| record.held_by?(identity) } own.any? ? result(:mine, own) : result(:free, []) end |
#list ⇒ Result
Returns every lock in the store, this tree's siblings included.
98 99 100 101 |
# File 'lib/agent/lock/manager.rb', line 98 def list reap result(:listed, store.all) end |
#mine ⇒ Result
104 105 106 107 |
# File 'lib/agent/lock/manager.rb', line 104 def mine reap result(:listed, store.all.select { |record| record.held_by?(identity) }) end |
#note(path, text) ⇒ Result
Write a line into a lock this session holds, so that a machine coming back up has something better than the diff to work out where it was.
153 154 155 156 157 158 159 160 161 162 |
# File 'lib/agent/lock/manager.rb', line 153 def note(path, text) scope = Scope.parse(path, tree: tree) record = store.find(scope) return result(:not_found, []) if record.nil? return result(:refused, [record]) unless record.held_by?(identity) updated = record.note(text) store.update(updated) result(:noted, [updated]) end |
#reap ⇒ Array<Record>
Locks whose holder is provably gone, or, for a holder on another host that cannot be asked, that nobody has touched in so long that nobody can say. A live holder's lock is never among them, however old; see Record#expired?. Reaped before any decision that depends on them, never on a schedule.
A lock with nothing written in it is deleted. One whose holder wrote
down what it was doing is orphaned instead: the process is gone, the
claim is void, but the notes are the only record of work interrupted
halfway, and a reboot is the most likely reason there are any. Whoever
comes next sees them, and resume takes the lock and the notes back.
140 141 142 143 144 145 |
# File 'lib/agent/lock/manager.rb', line 140 def reap store.all.select { |record| record.active? && record.expired?(stale_minutes) }.map do |record| record.notes? ? orphan(record) : drop(record) record end end |
#release(path) ⇒ Result
70 71 72 73 74 75 76 77 78 |
# File 'lib/agent/lock/manager.rb', line 70 def release(path) scope = Scope.parse(path, tree: tree) record = store.find(scope) return result(:not_found, []) if record.nil? return result(:refused, [record]) unless record.held_by?(identity) drop(record) result(:released, [record]) end |
#release_all ⇒ Result
110 111 112 113 114 |
# File 'lib/agent/lock/manager.rb', line 110 def release_all held = store.all.select { |record| record.held_by?(identity) } held.each { |record| drop(record) } result(:released, held) end |
#resume(path) ⇒ Result
Take an interrupted lock back, notes and all.
Inside the store's mutex, because the check and the write are two steps: two sessions resuming one orphan would otherwise both find it interrupted, both write themselves in, and both report :resumed.
172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/agent/lock/manager.rb', line 172 def resume(path) scope = Scope.parse(path, tree: tree) store.synchronize do record = store.find(scope) next result(:not_found, []) if record.nil? || record.active? adopted = record.with(status: Record::ACTIVE, updated_at: Time.now.utc.iso8601, **claim) store.update(adopted) result(:resumed, [adopted]) end end |
#stale_minutes ⇒ Integer
Returns how long a lock whose holder cannot be checked is trusted for, and how long a live one goes untouched before it is reported stale.
41 42 43 |
# File 'lib/agent/lock/manager.rb', line 41 def stale_minutes @stale_minutes ||= Integer(ENV.fetch("AGENT_LOCK_STALE_MINUTES", DEFAULT_STALE_MINUTES)) end |