Module: ConvertSdk::ForkGuard
- Defined in:
- lib/convert_sdk/fork_guard.rb
Overview
The SDK's single fork-detection authority and the ONLY Process._fork
prepend in the gem — the SDK's only global mutation (NFR15, architecture
Decision 6). It follows the Rails ForkTracker pattern: a module prepended
onto Process.singleton_class whose _fork wraps super and, when it
returns 0 (the child), runs the re-arm path. The prepend is installed once
at SDK load (it must exist before any fork; installing it is cheap and
thread-free, so it does not violate the NFR4 zero-threads-until-use rule —
that rule concerns THREADS, not this hook).
Fork detection elsewhere uses ForkGuard.forked? — a free integer comparison (+Process.pid != owner_pid+, the Datadog idiom) safe to call on every boundary, including JRuby (where it is always false).
On JRuby (no fork, no Process._fork) the prepend is a no-op by
construction: ForkGuard.install! skips it, so ForkGuard.forked? stays false forever.
Consumers register their thread-owning timers via ForkGuard.register_timer and any
child-side cleanup (e.g. ApiManager's queue-ownership clear in Story 4.2) via
ForkGuard.register_child_callback, keeping ForkGuard decoupled from its callers.
ForkGuard.rearm! is the shared re-arm path (also invoked by Client#postfork in
Epic 4): it marks every registered timer dead, then fires every registered
child-callback in registration order, then resets owner_pid.
The child hook path is LOCK-MINIMAL — mutexes held by other threads at the
moment of fork are a classic deadlock source. It resets owner_pid first,
then iterates a SNAPSHOT of the timer registry and a SNAPSHOT of the
child-callback registry taken under the registry mutex.
Defined Under Namespace
Modules: ForkHook
Class Attribute Summary collapse
-
.logger ⇒ ConvertSdk::LogManager?
Module-level logger, settable at wiring time (Client wires it in 2.7).
-
.owner_pid ⇒ Integer
readonly
The pid that currently owns the SDK's threads.
Class Method Summary collapse
-
.forked? ⇒ Boolean
True iff the current process differs from the owner (i.e. we are in a forked child).
-
.install! ⇒ void
Install the
Process._forkprepend. -
.rearm! ⇒ void
The shared re-arm path: reset owner_pid, mark every registered timer dead, then fire every child-callback in registration order.
-
.register_child_callback(callable) ⇒ void
Register a child-side callback fired after timers are marked dead (e.g. queue-ownership clear).
-
.register_timer(timer) ⇒ void
Register a thread-owning timer to be marked dead in a forked child.
-
.reset_for_tests! ⇒ void
private
Test-only reset so the singleton-state module is order-independent under RSpec.
-
.stop_all_timers! ⇒ void
private
Test-only reap that STOPS (signals exit + joins) every registered timer so NO BackgroundTimer thread can survive into the next example.
Class Attribute Details
.logger ⇒ ConvertSdk::LogManager?
Module-level logger, settable at wiring time (Client wires it in 2.7). nil-safe before wiring — the hook never assumes a logger is present.
51 52 53 |
# File 'lib/convert_sdk/fork_guard.rb', line 51 def logger @logger end |
.owner_pid ⇒ Integer (readonly)
Returns the pid that currently owns the SDK's threads.
46 47 48 |
# File 'lib/convert_sdk/fork_guard.rb', line 46 def owner_pid @owner_pid end |
Class Method Details
.forked? ⇒ Boolean
Returns true iff the current process differs from the owner (i.e. we are in a forked child). A free comparison; false on JRuby.
68 69 70 |
# File 'lib/convert_sdk/fork_guard.rb', line 68 def forked? Process.pid != @owner_pid end |
.install! ⇒ void
This method returns an undefined value.
Install the Process._fork prepend. Idempotent (double-install guarded)
and a no-op when fork is unsupported (JRuby) — the prepend never lands,
so the hook is a no-op by construction. Safe to call repeatedly.
57 58 59 60 61 62 63 64 |
# File 'lib/convert_sdk/fork_guard.rb', line 57 def install! return unless Process.respond_to?(:_fork) && Process.respond_to?(:fork) return if @installed Process.singleton_class.prepend(ForkHook) @installed = true @owner_pid = Process.pid end |
.rearm! ⇒ void
This method returns an undefined value.
The shared re-arm path: reset owner_pid, mark every registered timer dead, then fire every child-callback in registration order. Lock-minimal: owner_pid is reset first, then SNAPSHOTS of the registries are iterated outside the registry mutex (deadlock-safe in the fork hook).
92 93 94 95 96 97 98 |
# File 'lib/convert_sdk/fork_guard.rb', line 92 def rearm! @owner_pid = Process.pid timers, callbacks = @registry_mutex.synchronize { [@timers.dup, @child_callbacks.dup] } @logger&.debug("ForkGuard#rearm!: fork detected, re-arming #{timers.size} timer(s) in pid #{Process.pid}") timers.each(&:mark_dead) callbacks.each(&:call) end |
.register_child_callback(callable) ⇒ void
This method returns an undefined value.
Register a child-side callback fired after timers are marked dead (e.g. queue-ownership clear). Keeps ForkGuard decoupled from its callers.
83 84 85 |
# File 'lib/convert_sdk/fork_guard.rb', line 83 def register_child_callback(callable) @registry_mutex.synchronize { @child_callbacks << callable } end |
.register_timer(timer) ⇒ void
This method returns an undefined value.
Register a thread-owning timer to be marked dead in a forked child.
75 76 77 |
# File 'lib/convert_sdk/fork_guard.rb', line 75 def register_timer(timer) @registry_mutex.synchronize { @timers << timer } end |
.reset_for_tests! ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Test-only reset so the singleton-state module is order-independent under RSpec. Clears registries, resets owner_pid, drops the logger. Does NOT uninstall the prepend (it is harmless and global).
121 122 123 124 125 126 127 128 |
# File 'lib/convert_sdk/fork_guard.rb', line 121 def reset_for_tests! @registry_mutex.synchronize do @timers = [] @child_callbacks = [] end @owner_pid = Process.pid @logger = nil end |
.stop_all_timers! ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Test-only reap that STOPS (signals exit + joins) every registered timer
so NO BackgroundTimer thread can survive into the next example. Distinct
from reset_for_tests!, which only clears the registry (it leaves any
live thread running). A leaked flush/refresh timer thread firing a real
POST/GET after its example ends pollutes a later example's zero-HTTP
assertion under WebMock (intermittent on JRuby's thread scheduling) — a
global after(:each) reap closes that window deterministically. Iterates
a SNAPSHOT taken under the registry mutex; #stop is idempotent so this
is a cheap no-op for already-stopped timers.
111 112 113 114 |
# File 'lib/convert_sdk/fork_guard.rb', line 111 def stop_all_timers! timers = @registry_mutex.synchronize { @timers.dup } timers.each(&:stop) end |