Class: Wurk::Swarm::ChildBoot

Inherits:
Object
  • Object
show all
Includes:
Component
Defined in:
lib/wurk/swarm/child_boot.rb

Overview

Step 5 of the boot ordering. Runs inside each forked child:

* reset signal traps inherited from the parent,
* open a fresh Redis pool (ActiveRecord reconnects by its own fork hook),
* apply the slot's queues + concurrency to the default capsule,
* install child signal handlers (TERM/INT drain, TSTP quiet,
USR2 reopen logs),
* launch the Wurk::Launcher and block until shutdown.

Kept separate from Wurk::Swarm so the parent supervisor stays focused on PID supervision (SRP).

Constant Summary collapse

STORE_NOT_SERVING =

TTIN is trapped because its default disposition STOPS the process: an operator's kill -TTIN <child> asking for a thread dump would suspend the worker instead.

/\A(?:(?:LOADING|BUSY|CLUSTERDOWN|MASTERDOWN|READONLY|OOM|MISCONF)\b|ERR Out of memory)/
CHILD_SIGNALS =
{ 'TERM' => :term, 'INT' => :term, 'TSTP' => :tstp, 'USR2' => :usr2, 'TTIN' => :ttin }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config, slot, index, parent_pid: ::Process.ppid, start_quiet: false, fleet_size: nil, restart_counters: nil) ⇒ ChildBoot

parent_pid is captured by the swarm before it forks (race-free) and threaded through so OrphanGuard can tell "still supervised" from "reparented after the supervisor died". Defaults to the live parent for the non-swarm callers (tests) that construct a ChildBoot directly. start_quiet: — the swarm was TSTP-quieted before this child was forked (respawn/recycle during maintenance); boot the launcher already quieted so the replacement doesn't resume fetching. Delivered as a constructor flag, not a post-fork TSTP: a signal landing before this child resets its traps hits the parent's inherited (inert) handler and is dropped — the supervisor's TSTP re-relay is only the backstop. fleet_size: — slots in the swarm's topology, published to Health.fleet_size so this child's health listener (if it wins the port) can judge /ready on the whole swarm, not just itself. restart_counters: — the parent's Swarm::RestartCounters, read by this child's health listener for wurk_swarm_child_restarts_total.



47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/wurk/swarm/child_boot.rb', line 47

def initialize(config, slot, index, parent_pid: ::Process.ppid, start_quiet: false, fleet_size: nil,
               restart_counters: nil)
  @config = config
  @restart_counters = restart_counters
  @slot = slot
  @index = index
  @fleet_size = fleet_size
  @parent_pid = parent_pid
  @start_quiet = start_quiet
  @signal_read = nil
  @signal_write = nil
  @pending_tstp = false
end

Instance Attribute Details

#config ⇒ Object (readonly) Originally defined in module Component

Returns the value of attribute config.

Instance Method Details

#default_tag(dir = Dir.pwd) ⇒ Object Originally defined in module Component

#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object Originally defined in module Component

Invokes lifecycle hooks for event. Hooks run in registration order (or LIFO when reverse: true, used for teardown). A raise in one hook is reported via handle_exception and does NOT stop the next hook unless reraise: true (used in tests / fail-fast boot). oneshot: true clears the bucket after dispatch so the event can't fire twice.

#handle_exception(ex, ctx = {}) ⇒ Object Originally defined in module Component

#hostname ⇒ Object Originally defined in module Component

#identity ⇒ Object Originally defined in module Component

#leader? ⇒ Boolean Originally defined in module Component

True iff this process currently holds the cluster dear-leader lock. Cached per Component instance for LEADER_CACHE_TTL_MS (~5s): cron and the metrics rollups call this every tick, and an uncached GET would double their Redis traffic at short intervals for no benefit — the lock's own renewal cadence (60s+, spec §6.1) easily tolerates a few-second-stale read. Returns false unconditionally when WURK_LEADER=false (or SIDEKIQ_LEADER=false) is set on the process (opt-out hot-standby). Any Redis error is swallowed → false, so a transient partition can't propagate as an exception into user code.

Spec: docs/target/sidekiq-ent.md §6.1.

Returns:

  • (Boolean)

#logger ⇒ Object Originally defined in module Component

--- delegated to config -------------------------------------------

#mono_ms ⇒ Object Originally defined in module Component

#process_nonce ⇒ Object Originally defined in module Component

#real_ms ⇒ Object Originally defined in module Component

--- clocks ---------------------------------------------------------

#redis(idempotent: false) ⇒ Object Originally defined in module Component

#run ⇒ Object

rubocop:disable Metrics/AbcSize



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/wurk/swarm/child_boot.rb', line 61

def run # rubocop:disable Metrics/AbcSize
  reset_inherited_signals
  reconnect_after_fork
  @config.reopen_in_forked_child
  Wurk.server = true
  TestModeGuard.enforce!(@config.logger)
  Wurk::Health.fleet_size = @fleet_size
  Wurk::Health.swarm_counters = @restart_counters
  # :fork runs in each child after our internal AR/Redis reconnect and
  # before fetching, so apps can reopen sockets / restart threads /
  # reconnect non-fork-safe libs (Ent §7.4). It never fires in the parent
  # (which only forks + supervises). Like :startup, the child's forked
  # copy of the bucket is cleared after dispatch, so siblings fire theirs.
  fire_boot_event(:fork)
  apply_slot_to_config
  # :startup must fire in each worker child before its managers spin up
  # (Sidekiq contract, reraise: true). The parent supervisor never runs
  # jobs, so the non-swarm CLI path fires it once per process — for the
  # swarm, each child fires it here. Its own forked copy of the bucket is
  # cleared after, so siblings still fire their own.
  fire_boot_event(:startup, reraise: true)
  run_launcher
  exit 0
rescue StandardError, ::Wurk::Shutdown => e
  @config.logger.error { "swarm child ##{@index} (#{::Process.pid}) crashed: #{e.class}: #{e.message}" }
  report_crash(e)
  exit(redis_unavailable?(e) ? Swarm::REDIS_UNAVAILABLE_EXIT_STATUS : 1)
end

#safe_thread(name, priority: nil, &block) ⇒ Object Originally defined in module Component

Spawns a named thread that runs block under watchdog(name). The parent must retain the returned Thread; otherwise GC may not, but report_on_exception is disabled so we don't double-log on death.

Priority resolution matches Sidekiq (component.rb:44-48): explicit argument, then config.thread_priority, then -1. Ruby's default of 0 buys a 100ms timeslice; each negative step halves it, so -1 keeps a CPU-heavy capsule from starving its siblings for a whole tick.

#tid ⇒ Object Originally defined in module Component

#watchdog(last_words) ⇒ Object Originally defined in module Component

Wraps a block at a thread boundary: any unhandled exception is reported via handle_exception (so it lands in error_handlers / the log) and then re-raised. last_words is the component label included in the context.