Class: Wurk::Fetcher::Reliable::UnitOfWork
- Inherits:
-
Struct
- Object
- Struct
- Wurk::Fetcher::Reliable::UnitOfWork
- Defined in:
- lib/wurk/fetcher/unit_of_work.rb
Instance Attribute Summary collapse
-
#config ⇒ Object
Returns the value of attribute config.
-
#fetcher ⇒ Object
Returns the value of attribute fetcher.
-
#jid ⇒ Object
Returns the value of attribute jid.
-
#job ⇒ Object
Returns the value of attribute job.
-
#private_queue ⇒ Object
Returns the value of attribute private_queue.
-
#queue ⇒ Object
Returns the value of attribute queue.
-
#queue_name ⇒ Object
Returns the value of attribute queue_name.
-
#slot_key ⇒ Object
Returns the value of attribute slot_key.
-
#slot_token ⇒ Object
Returns the value of attribute slot_token.
Class Method Summary collapse
-
.claimed(queue, queue_name, private_queue, job, config, fetcher) ⇒ Object
The fetch path's constructor.
Instance Method Summary collapse
-
#acknowledge ⇒ Object
Deferred, never skipped: the LREM goes back to the fetcher, which pipelines it in front of the next fetch's LMOVE instead of spending a round trip of its own.
-
#initialize(queue = nil, job = nil, config = nil) ⇒ UnitOfWork
constructor
The positional form is upstream's documented
UnitOfWork.new(queue, job, config), which gem test suites build directly. -
#release_claim ⇒ Object
The Processor has settled this unit, one way or the other: stop vouching for its payload.
-
#release_slot ⇒ Object
The same release for the one path that deliberately does not ACK: Wurk::Shutdown, where the payload stays in the private list to be requeued and re-run somewhere else.
-
#release_slot_in(pipe) ⇒ Object
Give a global-concurrency slot back inside a pipeline the caller already has open — the ACK's, so a capped job costs no round trip more on the way out than an uncapped one does on the way in.
-
#requeue ⇒ Object
Puts a claimed job back without running it — the public half of the Sidekiq UnitOfWork contract that throttling/rate-limiting gems call (sidekiq-throttled's requeue path, anything holding a BasicFetch UoW, which is this one under the
Sidekiq::BasicFetchalias). -
#write_ack(pipe) ⇒ Object
Queue this unit's ACK into an already-open pipeline.
Constructor Details
#initialize(queue = nil, job = nil, config = nil) ⇒ UnitOfWork
The positional form is upstream's documented UnitOfWork.new(queue, job, config), which gem test suites build directly. No fetcher means
no private list, so #acknowledge has nothing to do. Anonymous ** is
the cheap spelling for the fetch path, which always passes keywords:
one allocation and ~230ns per job (a named **fields costs two).
49 50 51 52 53 54 55 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 49 def initialize(queue = nil, job = nil, config = nil, **) if queue super(queue: queue, queue_name: queue.delete_prefix('queue:'), job: job, config: config, **) else super(**) end end |
Instance Attribute Details
#config ⇒ Object
Returns the value of attribute config
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def config @config end |
#fetcher ⇒ Object
Returns the value of attribute fetcher
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def fetcher @fetcher end |
#jid ⇒ Object
Returns the value of attribute jid
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def jid @jid end |
#job ⇒ Object
Returns the value of attribute job
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def job @job end |
#private_queue ⇒ Object
Returns the value of attribute private_queue
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def private_queue @private_queue end |
#queue ⇒ Object
Returns the value of attribute queue
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def queue @queue end |
#queue_name ⇒ Object
Returns the value of attribute queue_name
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def queue_name @queue_name end |
#slot_key ⇒ Object
Returns the value of attribute slot_key
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def slot_key @slot_key end |
#slot_token ⇒ Object
Returns the value of attribute slot_token
42 43 44 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 42 def slot_token @slot_token end |
Class Method Details
.claimed(queue, queue_name, private_queue, job, config, fetcher) ⇒ Object
The fetch path's constructor. Positional, filling the members straight
in: the keyword form costs the caller's Hash, a second one for the
** forward, and Struct's keyword parsing — per job, for six fields
whose names never change.
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 61 def self.claimed(queue, queue_name, private_queue, job, config, fetcher) # rubocop:disable Metrics/ParameterLists uow = allocate uow.queue = queue uow.queue_name = queue_name uow.private_queue = private_queue uow.job = job uow.config = config uow.fetcher = fetcher uow end |
Instance Method Details
#acknowledge ⇒ Object
Deferred, never skipped: the LREM goes back to the fetcher, which pipelines it in front of the next fetch's LMOVE instead of spending a round trip of its own. Ordering against the job is unchanged — the LREM still happens only after success or retry handling (Pro §3.2) — so all that moves is the wall clock. Every path that stops fetching flushes first; see #flush_pending_acks.
78 79 80 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 78 def acknowledge fetcher&.defer_ack(self) end |
#release_claim ⇒ Object
The Processor has settled this unit, one way or the other: stop vouching for its payload. See Fetcher::SelfReconcile.
158 159 160 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 158 def release_claim ::Thread.current.thread_variable_set(CLAIM_KEY, nil) end |
#release_slot ⇒ Object
The same release for the one path that deliberately does not ACK: Wurk::Shutdown, where the payload stays in the private list to be requeued and re-run somewhere else. Whoever re-runs it needs the capacity, so this cannot wait out the TTL — and there is no ACK left to ride, hence the round trip. Bounded to the jobs a hard shutdown kills mid-flight.
122 123 124 125 126 127 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 122 def release_slot return unless slot_key QueueSlot::HELD.drop(slot_token, slot_key) config.redis(idempotent: true) { |conn| conn.call('ZREM', slot_key, slot_token) } end |
#release_slot_in(pipe) ⇒ Object
Give a global-concurrency slot back inside a pipeline the caller already has open — the ACK's, so a capped job costs no round trip more on the way out than an uncapped one does on the way in. The ZREM lands ahead of the fetch it is pipelined with, so a thread re-competing for the queue it just ran releases before it asks, never after.
Idempotent by construction (QueueSlot#release): it names one member, so a replayed ACK frees nothing that has since been handed on. The member names this claim rather than this thread, which is what makes that true of a stale ACK too — one handed back by a failed flush (Fetcher::Reliable#restore_pending_acks) after the thread has already claimed its next slot on the same queue. See QueueSlot.claim_token.
111 112 113 114 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 111 def release_slot_in(pipe) QueueSlot::HELD.drop(slot_token, slot_key) pipe.call('ZREM', slot_key, slot_token) end |
#requeue ⇒ Object
Puts a claimed job back without running it — the public half of the
Sidekiq UnitOfWork contract that throttling/rate-limiting gems call
(sidekiq-throttled's requeue path, anything holding a BasicFetch UoW,
which is this one under the Sidekiq::BasicFetch alias). Sidekiq's
BasicFetch has no private list, so its bare RPUSH is the whole move;
here the private copy has to go in the same step, or the job sits in
both lists and runs twice — once from the public queue, once when the
reaper reclaims this process's private list.
RPUSH then LREM in one script, unconditionally: unlike bulk_requeue (which races a Processor ACKing the same UoW and so guards the push on the private copy), the caller here owns the unit, and a job whose ACK already went out must still be re-queued rather than dropped. A script because a MULTI would apply the LREM after a refused RPUSH, losing the job. Unconditional is also why it goes by call_once: redis-client re-sends on a dropped socket, and the replay would push the job a second time. A raise leaves the outcome unknown, and both outcomes are safe: moved, or still in the private list, where Fetcher::SelfReconcile finds it.
148 149 150 151 152 153 154 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 148 def requeue config.redis do |conn| next conn.call_once('RPUSH', queue, job) unless private_queue Wurk::Lua::Loader.eval_once(conn, :requeue_unit, keys: [private_queue, queue], argv: [job]) end end |
#write_ack(pipe) ⇒ Object
Queue this unit's ACK into an already-open pipeline. The counter DEL rides the same round trip rather than taking one of its own: a per-job call would be a fetch+execute regression for the sake of a key that exists for roughly no jobs. See Middleware::PoisonPill.
Sending it only for the jobs that own one is not available to us: whether a job was reclaimed lives in the counter, and a reclaimed payload is byte-identical to a first-attempt one (the job JSON is wire-frozen, so the reaper cannot flag it). Reading the counter to decide would spend the very round trip the DEL is riding for free.
92 93 94 95 96 97 |
# File 'lib/wurk/fetcher/unit_of_work.rb', line 92 def write_ack(pipe) pipe.call_v(['LREM', private_queue, LREM_COUNT, job]) job_jid = jid.to_s Middleware::PoisonPill.clear_in(pipe, job_jid) unless job_jid.empty? release_slot_in(pipe) if slot_key end |