Module: Clacky::ThreadRegistry
- Defined in:
- lib/clacky/thread_registry.rb
Overview
Process-wide registry of managed threads.
Every long-lived worker thread should be created through ThreadRegistry.spawn so shutdown can wait for it, force-stop it, and report any thread that refuses to die — including the backtrace of its creation site for tracing stray Thread.new calls.
Threads created with daemon: true are exempt from force_stop!; they are terminated automatically when the process exits.
Constant Summary collapse
- MUTEX =
Mutex.new
- ENTRIES =
Thread => { name:, daemon:, killable:, created_at:, backtrace: }
{}
Class Method Summary collapse
-
.force_stop!(grace: 5.0) ⇒ Object
Wait up to grace seconds for all killable threads to exit cooperatively, then Thread#kill the stragglers.
-
.register(thread, name:, daemon: false, killable: true, backtrace: nil) ⇒ Object
Register an already-created thread (for code that cannot switch to spawn).
-
.report_leaks! ⇒ Object
Log WARN for every managed thread still alive (with its creation site).
-
.spawn(name:, daemon: false, killable: true, &block) ⇒ Thread
Create a managed thread.
- .unregister(thread) ⇒ Object
Class Method Details
.force_stop!(grace: 5.0) ⇒ Object
Wait up to grace seconds for all killable threads to exit cooperatively, then Thread#kill the stragglers. Returns the threads that were killed.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/clacky/thread_registry.rb', line 65 def force_stop!(grace: 5.0) snapshot = MUTEX.synchronize do ENTRIES.keys.each { |t| ENTRIES.delete(t) unless t.alive? } ENTRIES.map { |t, | [t, .dup] } end killable = snapshot.select { |_t, | [:killable] } deadline = Time.now + grace while Time.now < deadline && killable.any? { |t, | t.alive? } sleep 0.05 end stragglers = killable.select { |t, | t.alive? }.map(&:first) stragglers.each(&:kill) # Thread#kill is asynchronous — give stragglers a moment to run their # ensure (unregister) so report_leaks! doesn't flag threads that did exit. stragglers.each { |t| t.join(0.5) rescue nil } stragglers end |
.register(thread, name:, daemon: false, killable: true, backtrace: nil) ⇒ Object
Register an already-created thread (for code that cannot switch to spawn).
49 50 51 52 53 54 55 56 57 |
# File 'lib/clacky/thread_registry.rb', line 49 def register(thread, name:, daemon: false, killable: true, backtrace: nil) MUTEX.synchronize do ENTRIES[thread] = { name: name, daemon: daemon, killable: killable, created_at: Time.now, backtrace: backtrace } end thread end |
.report_leaks! ⇒ Object
Log WARN for every managed thread still alive (with its creation site).
86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/clacky/thread_registry.rb', line 86 def report_leaks! snapshot = MUTEX.synchronize do ENTRIES.keys.each { |t| ENTRIES.delete(t) unless t.alive? } ENTRIES.map { |t, | [t, .dup] } end snapshot.each do |_thread, | Clacky::Logger.warn( "thread leaked after shutdown", thread: [:name] || "unnamed", created_from: [:backtrace] ? [:backtrace].join(" | ") : "unknown" ) end end |
.spawn(name:, daemon: false, killable: true, &block) ⇒ Thread
Create a managed thread.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/clacky/thread_registry.rb', line 26 def spawn(name:, daemon: false, killable: true, &block) raise ArgumentError, "block required" unless block creation_backtrace = caller(1, 5) thread = Thread.new do begin Thread.current.name = name if Thread.current.respond_to?(:name=) block.call rescue Clacky::AgentInterrupted # Cooperative shutdown signal (Ctrl+C / server drain). A managed # thread reaching its top with this exception is exiting by design, # not failing — swallow it so the thread terminates cleanly and # Ruby doesn't print a "terminated with exception" backtrace. ensure unregister(Thread.current) end end register(thread, name: name, daemon: daemon, killable: killable, backtrace: creation_backtrace) thread end |