Class: Wurk::ShutdownGate

Inherits:
Object
  • Object
show all
Defined in:
lib/wurk/shutdown_gate.rb

Overview

The two rules a process teardown obeys: it runs exactly once, and it never waits on a thread without a bound. Same reason TimerLoop exists — the mutex/clock dance was going to be hand-rolled in the caller, so it lives in one place with a name instead.

A worker process can have several shutdown requests in flight at once — a dashboard-queued TERM, an embedded host calling stop, a Manager that can no longer hold its concurrency — and running the teardown twice double-fires the :exit hooks and tears the Redis pool down under the thread still using it.

@gate = Wurk::ShutdownGate.new
def stop = @gate.run { ...teardown... }

The first caller runs the block; the rest block until it returns and then get nil. Queue#close releases every waiter at once and makes every later call return immediately, so a stop landing after the teardown finished is a no-op rather than a second one.

The lock is only ever held for the bookkeeping, never across the teardown — a caller that must not block (a dying Processor thread, the heartbeat the teardown is about to join) has to be able to ask for a shutdown that is already under way without deadlocking against it.

Instance Method Summary collapse

Constructor Details

#initialize ⇒ ShutdownGate

Returns a new instance of ShutdownGate.



30
31
32
33
34
35
36
37
38
# File 'lib/wurk/shutdown_gate.rb', line 30

def initialize
  # Reentrant: a boot that fails rolls itself back by calling the teardown
  # from inside #boot, on the same thread.
  @boot_lock = ::Monitor.new
  @mutex = ::Mutex.new
  @owner = nil
  @runner = nil
  @done = ::Queue.new
end

Instance Method Details

#boot ⇒ Object

Runs the boot block unless a shutdown has already been claimed, and returns whether it ran. A teardown requested from another thread while the boot is in progress waits for it to finish rather than releasing components the boot is still starting (a leader campaign, a listener) behind its back.



45
46
47
48
49
50
51
52
# File 'lib/wurk/shutdown_gate.rb', line 45

def boot
  @boot_lock.synchronize do
    return false if claimed?

    yield
    true
  end
end

#claimed? ⇒ Boolean

Returns:

  • (Boolean)


54
55
56
# File 'lib/wurk/shutdown_gate.rb', line 54

def claimed?
  @mutex.synchronize { !@owner.nil? }
end

#join_within(threads, budget) ⇒ Object

Join every thread inside one shared monotonic budget (an absolute CLOCK_MONOTONIC instant), so N wedged threads cost the budget once rather than N times. A non-positive remainder polls without waiting, which is the right answer once the budget is spent: the straggler is left to the process exit rather than holding the whole teardown open behind it.



80
81
82
# File 'lib/wurk/shutdown_gate.rb', line 80

def join_within(threads, budget)
  threads.each { |t| t.join([budget - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max) }
end

#run ⇒ Object



58
59
60
61
62
63
64
65
66
# File 'lib/wurk/shutdown_gate.rb', line 58

def run
  return await unless @boot_lock.synchronize { claim }

  begin
    yield
  ensure
    @done.close
  end
end

#runner ⇒ Object

Memoizes the one background thread a caller uses to reach #run from somewhere it cannot block. Spawned once: a second request rides the first teardown instead of leaving another unreferenced one running behind it.



71
72
73
# File 'lib/wurk/shutdown_gate.rb', line 71

def runner
  @mutex.synchronize { @runner ||= yield }
end