Module: RoundhouseUi::Cancellation

Defined in:
lib/roundhouse_ui/cancellation.rb

Overview

Cooperative job cancellation — pure OSS, no preemption (Ruby can't safely kill a running thread). Cancelled JIDs live in a Redis set:

* RoundhouseUi::CancelMiddleware skips a job whose JID is cancelled when it
is *about* to run (covers queued/scheduled/retry jobs).
* A long-running job can call RoundhouseUi.cancelled?(jid) and bail out.

The set expires so stale flags clean themselves up.

Constant Summary collapse

KEY =
"roundhouse:cancelled"
TTL =

seconds

86_400
CHECK_EVERY =

seconds — max staleness of the "nothing is cancelled" gate

2.0

Class Method Summary collapse

Class Method Details

.any?Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/roundhouse_ui/cancellation.rb', line 66

def any?
  Sidekiq.redis { |conn| conn.call("EXISTS", KEY) } == 1
end

.cancel!(jid) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/roundhouse_ui/cancellation.rb', line 21

def cancel!(jid)
  Sidekiq.redis do |conn|
    conn.call("SADD", KEY, jid.to_s)
    conn.call("EXPIRE", KEY, TTL)
  end
  # Bust the local gate: the cancelling process sees its own cancel
  # immediately; other processes converge within CHECK_EVERY.
  @gate.synchronize do
    @pending = true
    @checked_at = monotonic_now
  end
end

.cancelled?(jid) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/roundhouse_ui/cancellation.rb', line 34

def cancelled?(jid)
  Sidekiq.redis { |conn| conn.call("SISMEMBER", KEY, jid.to_s) } == 1
end

.cancelled_jidsObject



42
43
44
# File 'lib/roundhouse_ui/cancellation.rb', line 42

def cancelled_jids
  Sidekiq.redis { |conn| conn.call("SMEMBERS", KEY) }
end

.clear!(jid) ⇒ Object



38
39
40
# File 'lib/roundhouse_ui/cancellation.rb', line 38

def clear!(jid)
  Sidekiq.redis { |conn| conn.call("SREM", KEY, jid.to_s) }
end

.monotonic_nowObject



78
# File 'lib/roundhouse_ui/cancellation.rb', line 78

def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)

.pending?Boolean

The middleware's hot-path gate: "is anything cancelled at all?" — almost always no, so answering it with one EXISTS per process per CHECK_EVERY (instead of a SISMEMBER per job) takes the common case to zero Redis round-trips. While cancellations are pending, the middleware still does the exact per-job SISMEMBER. A cancel issued by another process is invisible here for up to CHECK_EVERY — acceptable because cancellation is cooperative and racy by design (a job may already be running when the flag lands), and cancel! busts the local gate so the cancelling process itself is always exact.

Returns:

  • (Boolean)


55
56
57
58
59
60
61
62
63
64
# File 'lib/roundhouse_ui/cancellation.rb', line 55

def pending?
  now = monotonic_now
  @gate.synchronize do
    if @checked_at.nil? || now - @checked_at >= CHECK_EVERY
      @pending = any?
      @checked_at = now
    end
    @pending
  end
end

.reset_gate!Object

Forget the cached gate state (tests; or after flushing Redis by hand).



71
72
73
74
75
76
# File 'lib/roundhouse_ui/cancellation.rb', line 71

def reset_gate!
  @gate.synchronize do
    @pending = nil
    @checked_at = nil
  end
end