fiber_audit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.fiber-audit.example.yml +19 -0
  3. data/ARCHITECTURE.md +726 -0
  4. data/CHANGELOG.md +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +66 -8
  7. data/lib/fiber_audit/cli.rb +87 -2
  8. data/lib/fiber_audit/configuration.rb +106 -6
  9. data/lib/fiber_audit/errors.rb +2 -0
  10. data/lib/fiber_audit/operation_vocabulary.rb +42 -0
  11. data/lib/fiber_audit/reporters/text.rb +1 -1
  12. data/lib/fiber_audit/runtime/active_operations.rb +146 -0
  13. data/lib/fiber_audit/runtime/boot.rb +83 -0
  14. data/lib/fiber_audit/runtime/clock.rb +35 -0
  15. data/lib/fiber_audit/runtime/environment.rb +289 -0
  16. data/lib/fiber_audit/runtime/event.rb +86 -0
  17. data/lib/fiber_audit/runtime/execution_context.rb +89 -0
  18. data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
  19. data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
  20. data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
  21. data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
  22. data/lib/fiber_audit/runtime/limits.rb +102 -0
  23. data/lib/fiber_audit/runtime/location.rb +44 -0
  24. data/lib/fiber_audit/runtime/policy.rb +121 -0
  25. data/lib/fiber_audit/runtime/probes/base.rb +333 -0
  26. data/lib/fiber_audit/runtime/probes/http.rb +80 -0
  27. data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
  28. data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
  29. data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
  30. data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
  31. data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
  32. data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
  33. data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
  34. data/lib/fiber_audit/runtime/rails_integration.rb +279 -0
  35. data/lib/fiber_audit/runtime/recorder.rb +333 -0
  36. data/lib/fiber_audit/runtime/redactor.rb +102 -0
  37. data/lib/fiber_audit/runtime/sampler.rb +26 -0
  38. data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
  39. data/lib/fiber_audit/runtime/session.rb +112 -0
  40. data/lib/fiber_audit/runtime/supervisor.rb +114 -0
  41. data/lib/fiber_audit/runtime/validation.rb +68 -0
  42. data/lib/fiber_audit/runtime/watchdog.rb +479 -0
  43. data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
  44. data/lib/fiber_audit/runtime.rb +37 -0
  45. data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
  46. data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
  47. data/lib/fiber_audit/static/rules/io_select.rb +2 -4
  48. data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
  49. data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
  50. data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
  51. data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
  52. data/lib/fiber_audit/version.rb +1 -1
  53. data/lib/fiber_audit.rb +1 -0
  54. metadata +38 -2
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_operations'
4
+ require_relative 'clock'
5
+ require_relative 'event'
6
+ require_relative 'heartbeat'
7
+ require_relative 'recorder'
8
+ require_relative 'redactor'
9
+ require_relative 'watchdog_policy'
10
+
11
+ module FiberAudit
12
+ module Runtime
13
+ # Monitors scheduler-owned heartbeat fibers from one dedicated process thread.
14
+ # rubocop:disable Metrics/ClassLength
15
+ class Watchdog
16
+ SOURCE = :scheduler_watchdog
17
+ STOP_TIMEOUT_SECONDS = 1
18
+
19
+ Stall = Data.define(:sequence, :progress_sequence, :began_monotonic_ns)
20
+ Channel = Struct.new(:thread, :heartbeat, :active, :unsupported, :stall, keyword_init: true)
21
+
22
+ attr_reader :policy, :recorder, :active_operations
23
+
24
+ def initialize(
25
+ policy:,
26
+ recorder:,
27
+ redactor:,
28
+ active_operations:,
29
+ clock: Clock.new,
30
+ thread_factory: ->(&block) { Thread.new(&block) }
31
+ )
32
+ validate_dependencies!(policy, recorder, redactor, active_operations, clock, thread_factory)
33
+ @policy = policy
34
+ @recorder = recorder
35
+ @redactor = redactor
36
+ @active_operations = active_operations
37
+ @clock = clock
38
+ @thread_factory = thread_factory
39
+ @mutex = Mutex.new
40
+ @wait_mutex = Mutex.new
41
+ @condition = ConditionVariable.new
42
+ @channels = {}.compare_by_identity
43
+ @stall_sequence = 0
44
+ @unsupported_seen = false
45
+ @monitor_thread = nil
46
+ @stopping = false
47
+ @stopped = false
48
+ emit_state(policy.enabled? ? :watchdog_absent : :watchdog_disabled)
49
+ end
50
+
51
+ def enabled?
52
+ policy.enabled?
53
+ end
54
+
55
+ def fail_open?
56
+ recorder.session.policy.fail_open?
57
+ end
58
+
59
+ def state
60
+ return :disabled unless enabled?
61
+
62
+ @mutex.synchronize do
63
+ return :active if @channels.values.any?(&:active)
64
+ return :unsupported if @unsupported_seen || @channels.values.any?(&:unsupported)
65
+
66
+ :absent
67
+ end
68
+ end
69
+
70
+ def scheduler_installed(
71
+ thread: Thread.current,
72
+ schedule: Fiber.method(:schedule),
73
+ sleeper: Kernel.method(:sleep)
74
+ )
75
+ return self unless enabled?
76
+ raise RuntimeContractError, 'scheduler thread must be a Thread' unless thread.is_a?(Thread)
77
+
78
+ heartbeat = Heartbeat.new(
79
+ clock: @clock,
80
+ interval_ns: policy.heartbeat_interval_ns,
81
+ owner_thread: thread,
82
+ on_tick: method(:heartbeat_ticked),
83
+ on_error: method(:heartbeat_failed)
84
+ )
85
+ previous = @mutex.synchronize do
86
+ prior = @channels[thread]
87
+ @channels[thread] = Channel.new(
88
+ thread: thread,
89
+ heartbeat: heartbeat,
90
+ active: false,
91
+ unsupported: false,
92
+ stall: nil
93
+ )
94
+ prior
95
+ end
96
+ previous&.heartbeat&.request_stop
97
+ heartbeat.start(schedule: schedule, sleeper: sleeper)
98
+ self
99
+ rescue StandardError => e
100
+ channel_failure(thread, e)
101
+ raise e unless fail_open?
102
+
103
+ self
104
+ end
105
+
106
+ def scheduler_unsupported(thread: Thread.current)
107
+ return self unless enabled?
108
+
109
+ channel_failure(thread, nil)
110
+ self
111
+ end
112
+
113
+ def scheduler_closing(thread: Thread.current)
114
+ return self unless enabled?
115
+
116
+ now_ns = safe_monotonic_ns
117
+ @mutex.synchronize do
118
+ channel = @channels.delete(thread)
119
+ next unless channel
120
+
121
+ channel.heartbeat.request_stop
122
+ complete_stall(channel, now_ns: now_ns, resumed: false) if channel.stall
123
+ unless channel.active || channel.unsupported
124
+ @unsupported_seen = true
125
+ emit_state(:watchdog_unsupported, thread: thread)
126
+ end
127
+ end
128
+ wake_monitor
129
+ self
130
+ rescue StandardError => e
131
+ handle_failure(e)
132
+ raise e unless fail_open?
133
+
134
+ self
135
+ end
136
+
137
+ def poll(now_ns: nil)
138
+ return state unless enabled?
139
+
140
+ observed_now = now_ns.nil? ? @clock.monotonic_ns : Validation.integer(now_ns, 'watchdog poll time')
141
+ @mutex.synchronize do
142
+ @channels.each_value { |channel| poll_channel(channel, observed_now) if channel.active }
143
+ end
144
+ state
145
+ rescue StandardError => e
146
+ handle_failure(e)
147
+ raise e unless fail_open?
148
+
149
+ :unsupported
150
+ end
151
+
152
+ def stop
153
+ return self if @stopped
154
+
155
+ @wait_mutex.synchronize do
156
+ @stopping = true
157
+ @condition.broadcast
158
+ end
159
+ now_ns = safe_monotonic_ns
160
+ @mutex.synchronize do
161
+ @channels.each_value do |channel|
162
+ channel.heartbeat.request_stop
163
+ complete_stall(channel, now_ns: now_ns, resumed: false) if channel.stall
164
+ if !channel.active && !channel.unsupported
165
+ @unsupported_seen = true
166
+ emit_state(:watchdog_unsupported, thread: channel.thread)
167
+ end
168
+ end
169
+ @channels.clear
170
+ end
171
+ stop_monitor_thread
172
+ @stopped = true
173
+ self
174
+ rescue StandardError => e
175
+ handle_failure(e)
176
+ @stopped = true
177
+ raise e unless fail_open?
178
+
179
+ self
180
+ end
181
+
182
+ private
183
+
184
+ def validate_dependencies!(watchdog_policy, candidate_recorder, redactor, operations, clock, threads)
185
+ unless watchdog_policy.is_a?(WatchdogPolicy)
186
+ raise RuntimeContractError, 'policy must be a FiberAudit::Runtime::WatchdogPolicy'
187
+ end
188
+ unless candidate_recorder.is_a?(Recorder)
189
+ raise RuntimeContractError, 'recorder must be a FiberAudit::Runtime::Recorder'
190
+ end
191
+ raise RuntimeContractError, 'redactor must be a FiberAudit::Runtime::Redactor' unless redactor.is_a?(Redactor)
192
+ unless operations.is_a?(ActiveOperations)
193
+ raise RuntimeContractError, 'active_operations must be FiberAudit::Runtime::ActiveOperations'
194
+ end
195
+ raise RuntimeContractError, 'clock must be a FiberAudit::Runtime::Clock' unless clock.is_a?(Clock)
196
+ raise RuntimeContractError, 'thread_factory must respond to call' unless threads.respond_to?(:call)
197
+ end
198
+
199
+ def heartbeat_ticked(heartbeat)
200
+ activate = false
201
+ @mutex.synchronize do
202
+ channel = @channels[heartbeat.owner_thread]
203
+ return unless channel&.heartbeat.equal?(heartbeat)
204
+
205
+ unless channel.active
206
+ channel.active = true
207
+ activate = true
208
+ end
209
+ end
210
+ if activate
211
+ snapshot = heartbeat.snapshot
212
+ emit_state(
213
+ :watchdog_active,
214
+ thread: heartbeat.owner_thread,
215
+ fiber_id: snapshot.fiber_id,
216
+ measurements: policy_measurements
217
+ )
218
+ start_monitor_thread
219
+ end
220
+ wake_monitor
221
+ rescue StandardError => e
222
+ heartbeat_failed(heartbeat, e)
223
+ end
224
+
225
+ def heartbeat_failed(heartbeat, error)
226
+ channel_failure(heartbeat.owner_thread, error)
227
+ raise error unless fail_open?
228
+ end
229
+
230
+ def channel_failure(thread, error)
231
+ already_unsupported = false
232
+ @mutex.synchronize do
233
+ entry = @channels[thread]
234
+ if entry
235
+ already_unsupported = entry.unsupported
236
+ entry.heartbeat.request_stop
237
+ entry.unsupported = true
238
+ entry.active = false
239
+ else
240
+ already_unsupported = @unsupported_seen
241
+ end
242
+ @unsupported_seen = true
243
+ end
244
+ account_internal_error if error
245
+ emit_state(:watchdog_unsupported, thread: thread) unless already_unsupported
246
+ end
247
+
248
+ def poll_channel(channel, now_ns)
249
+ snapshot = channel.heartbeat.snapshot
250
+ return unless snapshot.started && snapshot.last_progress_ns
251
+ raise RuntimeSafetyError, 'watchdog monotonic clock moved backwards' if now_ns < snapshot.last_progress_ns
252
+
253
+ if channel.stall
254
+ complete_stall(channel, now_ns: now_ns, resumed: true) if snapshot.sequence > channel.stall.progress_sequence
255
+ return
256
+ end
257
+
258
+ age_ns = now_ns - snapshot.last_progress_ns
259
+ start_stall(channel, snapshot, age_ns, now_ns) if policy.stalled?(age_ns: age_ns)
260
+ end
261
+
262
+ def start_stall(channel, snapshot, age_ns, now_ns)
263
+ @stall_sequence += 1
264
+ channel.stall = Stall.new(
265
+ sequence: @stall_sequence,
266
+ progress_sequence: snapshot.sequence,
267
+ began_monotonic_ns: snapshot.last_progress_ns
268
+ )
269
+ operations = active_operations.snapshot(thread_id: snapshot.thread_id)
270
+ frames = safe_frames(channel.thread)
271
+ measurements = {
272
+ stall_sequence: @stall_sequence,
273
+ progress_sequence: snapshot.sequence,
274
+ observed_age_ns: age_ns,
275
+ stall_threshold_ns: policy.stall_threshold_ns,
276
+ heartbeat_interval_ns: policy.heartbeat_interval_ns,
277
+ frame_count: frames.size,
278
+ active_operation_count: operations.size,
279
+ active_operation_first_sequence: operations.first&.sequence,
280
+ active_operation_last_sequence: operations.last&.sequence
281
+ }
282
+ emit_event(
283
+ kind: :scheduler_stall_started,
284
+ monotonic_ns: now_ns,
285
+ thread_id: snapshot.thread_id,
286
+ fiber_id: snapshot.fiber_id,
287
+ measurements: measurements
288
+ )
289
+ frames.each_with_index do |location, index|
290
+ emit_event(
291
+ kind: :scheduler_stall_frame,
292
+ monotonic_ns: now_ns,
293
+ location: location,
294
+ thread_id: snapshot.thread_id,
295
+ fiber_id: snapshot.fiber_id,
296
+ measurements: { stall_sequence: @stall_sequence, frame_index: index }
297
+ )
298
+ end
299
+ end
300
+
301
+ def complete_stall(channel, now_ns:, resumed:)
302
+ stall = channel.stall
303
+ return unless stall
304
+
305
+ snapshot = channel.heartbeat.snapshot
306
+ duration = [now_ns - stall.began_monotonic_ns, 0].max
307
+ emit_event(
308
+ kind: :scheduler_stall_completed,
309
+ monotonic_ns: now_ns,
310
+ duration_ns: duration,
311
+ thread_id: snapshot.thread_id,
312
+ fiber_id: snapshot.fiber_id,
313
+ measurements: {
314
+ stall_sequence: stall.sequence,
315
+ progress_sequence: snapshot.sequence,
316
+ resumed: resumed
317
+ }
318
+ )
319
+ channel.stall = nil
320
+ end
321
+
322
+ def safe_frames(thread)
323
+ return [] if policy.max_frames.zero?
324
+
325
+ frames = thread.backtrace_locations(0, policy.max_frames) || []
326
+ frames.first(policy.max_frames).filter_map do |frame|
327
+ location = safe_frame_location(frame)
328
+ location unless location.nil? || Location::SENTINELS.include?(location.path)
329
+ rescue StandardError
330
+ nil
331
+ end.freeze
332
+ rescue StandardError => e
333
+ account_internal_error
334
+ raise e unless fail_open?
335
+
336
+ [].freeze
337
+ end
338
+
339
+ def safe_frame_location(frame)
340
+ path = frame.absolute_path
341
+ unless path
342
+ relative = frame.path
343
+ return unless relative.is_a?(String) && !relative.start_with?('-', '<')
344
+
345
+ path = File.join(@redactor.root, relative)
346
+ end
347
+ @redactor.location(path: path, line: frame.lineno, column: nil)
348
+ end
349
+
350
+ def emit_state(kind, thread: nil, fiber_id: nil, measurements: {})
351
+ state_measurements = measurements.empty? ? policy_measurements : measurements
352
+ emit_event(
353
+ kind: kind,
354
+ monotonic_ns: @clock.monotonic_ns,
355
+ thread_id: thread&.object_id,
356
+ fiber_id: fiber_id,
357
+ measurements: state_measurements
358
+ )
359
+ end
360
+
361
+ def emit_event(kind:, monotonic_ns:, duration_ns: nil, location: nil, thread_id: nil, fiber_id: nil, measurements: {})
362
+ recorder.record_control do
363
+ Event.new(
364
+ kind: kind,
365
+ source: SOURCE,
366
+ occurred_at: @clock.wall_time,
367
+ monotonic_ns: monotonic_ns,
368
+ duration_ns: duration_ns,
369
+ location: location,
370
+ execution_context: :unknown,
371
+ thread_id: thread_id,
372
+ fiber_id: fiber_id,
373
+ measurements: measurements
374
+ )
375
+ end
376
+ rescue StandardError => e
377
+ account_internal_error unless recorder.disabled?
378
+ raise e unless fail_open?
379
+
380
+ :internal_error
381
+ end
382
+
383
+ def policy_measurements
384
+ {
385
+ heartbeat_interval_ns: policy.heartbeat_interval_ns,
386
+ stall_threshold_ns: policy.stall_threshold_ns,
387
+ max_frames: policy.max_frames
388
+ }
389
+ end
390
+
391
+ def start_monitor_thread
392
+ @wait_mutex.synchronize do
393
+ return if @monitor_thread || @stopping
394
+
395
+ @monitor_thread = @thread_factory.call { monitor_loop }
396
+ raise RuntimeContractError, 'thread_factory must return a Thread' unless @monitor_thread.is_a?(Thread)
397
+
398
+ @monitor_thread.report_on_exception = false
399
+ @monitor_thread.abort_on_exception = !fail_open?
400
+ @monitor_thread.name = 'fiber-audit-watchdog' if @monitor_thread.respond_to?(:name=)
401
+ end
402
+ end
403
+
404
+ def monitor_loop
405
+ loop do
406
+ should_stop = @wait_mutex.synchronize do
407
+ @condition.wait(@wait_mutex, policy.heartbeat_interval_ms.fdiv(1_000)) unless @stopping
408
+ @stopping
409
+ end
410
+ break if should_stop
411
+
412
+ poll
413
+ end
414
+ rescue StandardError => e
415
+ handle_failure(e)
416
+ raise e unless fail_open?
417
+ end
418
+
419
+ def wake_monitor
420
+ @wait_mutex.synchronize { @condition.broadcast }
421
+ end
422
+
423
+ def stop_monitor_thread
424
+ thread = @monitor_thread
425
+ return unless thread && thread != Thread.current
426
+
427
+ return if thread.join(STOP_TIMEOUT_SECONDS)
428
+
429
+ account_internal_error
430
+ thread.kill
431
+ thread.join
432
+ ensure
433
+ @monitor_thread = nil
434
+ end
435
+
436
+ def handle_failure(error)
437
+ first_failure = false
438
+ failed_thread = nil
439
+ unless @mutex.owned?
440
+ @mutex.synchronize do
441
+ first_failure = !@unsupported_seen
442
+ @unsupported_seen = true
443
+ @channels.each_value do |channel|
444
+ failed_thread ||= channel.thread
445
+ channel.heartbeat.request_stop
446
+ channel.active = false
447
+ channel.unsupported = true
448
+ end
449
+ end
450
+ end
451
+ if first_failure
452
+ account_internal_error
453
+ begin
454
+ emit_state(:watchdog_unsupported, thread: failed_thread)
455
+ rescue StandardError
456
+ nil
457
+ end
458
+ end
459
+ error
460
+ end
461
+
462
+ def account_internal_error
463
+ recorder.internal_error!
464
+ rescue StandardError
465
+ nil
466
+ end
467
+
468
+ def safe_monotonic_ns
469
+ @clock.monotonic_ns
470
+ rescue StandardError => e
471
+ account_internal_error
472
+ raise e unless fail_open?
473
+
474
+ recorder.session.started_monotonic_ns
475
+ end
476
+ end
477
+ # rubocop:enable Metrics/ClassLength
478
+ end
479
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'validation'
4
+
5
+ module FiberAudit
6
+ module Runtime
7
+ WatchdogPolicy = Data.define(
8
+ :enabled,
9
+ :heartbeat_interval_ms,
10
+ :stall_threshold_ms,
11
+ :max_frames
12
+ ) do
13
+ def initialize(**values)
14
+ unknown = values.keys - WatchdogPolicy::DEFAULTS.keys
15
+ raise RuntimeContractError, "unknown watchdog policy field: #{unknown.first}" unless unknown.empty?
16
+
17
+ fields = WatchdogPolicy::DEFAULTS.merge(values)
18
+ raise RuntimeContractError, 'enabled must be a Boolean' unless [true, false].include?(fields[:enabled])
19
+
20
+ limits = WatchdogPolicy::LIMITS.to_h do |name, range|
21
+ value = fields.fetch(name)
22
+ unless value.is_a?(Integer) && range.cover?(value)
23
+ raise RuntimeContractError, "#{name} must be an Integer in #{range}"
24
+ end
25
+
26
+ [name, value]
27
+ end
28
+
29
+ super(enabled: fields[:enabled], **limits)
30
+ end
31
+
32
+ def enabled?
33
+ enabled
34
+ end
35
+
36
+ def heartbeat_interval_ns
37
+ heartbeat_interval_ms * WatchdogPolicy::NANOSECONDS_PER_MILLISECOND
38
+ end
39
+
40
+ def stall_threshold_ns
41
+ stall_threshold_ms * WatchdogPolicy::NANOSECONDS_PER_MILLISECOND
42
+ end
43
+
44
+ def stalled?(age_ns:)
45
+ age = Validation.integer(age_ns, 'heartbeat age')
46
+ age > stall_threshold_ns
47
+ end
48
+ end
49
+
50
+ WatchdogPolicy.const_set(:NANOSECONDS_PER_MILLISECOND, 1_000_000)
51
+ WatchdogPolicy.const_set(:DEFAULTS, {
52
+ enabled: true,
53
+ heartbeat_interval_ms: 25,
54
+ stall_threshold_ms: 100,
55
+ max_frames: 20
56
+ }.freeze)
57
+ WatchdogPolicy.const_set(:LIMITS, {
58
+ heartbeat_interval_ms: 1..60_000,
59
+ stall_threshold_ms: 1..600_000,
60
+ max_frames: 0..100
61
+ }.freeze)
62
+ WatchdogPolicy.const_set(:DISABLED, WatchdogPolicy.new(enabled: false))
63
+ end
64
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'errors'
5
+ require_relative 'operation_vocabulary'
6
+ require_relative 'execution_context'
7
+ require_relative 'runtime/validation'
8
+ require_relative 'runtime/policy'
9
+ require_relative 'runtime/watchdog_policy'
10
+ require_relative 'runtime/location'
11
+ require_relative 'runtime/event'
12
+ require_relative 'runtime/session'
13
+ require_relative 'runtime/redactor'
14
+ require_relative 'runtime/clock'
15
+ require_relative 'runtime/sampler'
16
+ require_relative 'runtime/limits'
17
+ require_relative 'runtime/jsonl/schema'
18
+ require_relative 'runtime/jsonl/writer'
19
+ require_relative 'runtime/recorder'
20
+ require_relative 'runtime/execution_context'
21
+ require_relative 'runtime/rails_integration'
22
+ require_relative 'runtime/environment'
23
+ require_relative 'runtime/active_operations'
24
+ require_relative 'runtime/heartbeat'
25
+ require_relative 'runtime/watchdog'
26
+ require_relative 'runtime/scheduler_observer'
27
+ require_relative 'runtime/probes/base'
28
+ require_relative 'runtime/probes/subprocess'
29
+ require_relative 'runtime/probes/thread_wait'
30
+ require_relative 'runtime/probes/synchronization'
31
+ require_relative 'runtime/probes/thread_state'
32
+ require_relative 'runtime/probes/io_select'
33
+ require_relative 'runtime/probes/socket'
34
+ require_relative 'runtime/probes/http'
35
+ require_relative 'runtime/probes/registry'
36
+ require_relative 'runtime/lifecycle'
37
+ require_relative 'runtime/supervisor'
@@ -4,6 +4,7 @@ require_relative 'base'
4
4
  require_relative '../../findings/evidence'
5
5
  require_relative '../../correlation/fingerprint'
6
6
  require_relative '../../findings/finding'
7
+ require_relative '../../operation_vocabulary'
7
8
 
8
9
  module FiberAudit
9
10
  module Static
@@ -14,14 +15,8 @@ module FiberAudit
14
15
  default_confidence :high
15
16
  description 'Blocking subprocess call in the fiber scheduler path'
16
17
 
17
- TARGETS = {
18
- 'Kernel' => %i[system exec spawn].freeze,
19
- 'Open3' => %i[capture2 capture2e capture3 pipeline].freeze,
20
- 'IO' => %i[popen].freeze,
21
- 'Process' => %i[waitall detach].freeze
22
- }.freeze
23
-
24
- BARE_KERNEL_METHODS = %i[system exec spawn].freeze
18
+ TARGETS = OperationVocabulary::FA1001_TARGETS
19
+ BARE_KERNEL_METHODS = OperationVocabulary::FA1001_KERNEL_METHODS
25
20
 
26
21
  MESSAGE = 'Subprocess operation may block the thread running the fiber scheduler.'
27
22
  REMEDIATION = 'Move long-running subprocess work outside the request path, or verify scheduler behaviour under load.'
@@ -4,6 +4,7 @@ require_relative 'base'
4
4
  require_relative '../../findings/evidence'
5
5
  require_relative '../../correlation/fingerprint'
6
6
  require_relative '../../findings/finding'
7
+ require_relative '../../operation_vocabulary'
7
8
 
8
9
  module FiberAudit
9
10
  module Static
@@ -19,9 +20,7 @@ module FiberAudit
19
20
  TITLE = 'Direct socket creation'
20
21
  CATEGORY = :network
21
22
 
22
- EXACT = %w[
23
- TCPSocket TCPServer UDPSocket UNIXSocket UNIXServer Socket IPSocket
24
- ].freeze
23
+ EXACT = OperationVocabulary::FA1006_EXACT
25
24
 
26
25
  MESSAGE = 'Direct socket use may bypass scheduler-aware networking ' \
27
26
  'and block the scheduler thread.'
@@ -4,6 +4,7 @@ require_relative 'base'
4
4
  require_relative '../../findings/evidence'
5
5
  require_relative '../../correlation/fingerprint'
6
6
  require_relative '../../findings/finding'
7
+ require_relative '../../operation_vocabulary'
7
8
 
8
9
  module FiberAudit
9
10
  module Static
@@ -22,10 +23,7 @@ module FiberAudit
22
23
  MESSAGE = 'IO.select may bypass scheduler-aware I/O and block the thread running the fiber scheduler.'
23
24
  REMEDIATION = 'Use scheduler-aware I/O APIs or allow the active Fiber scheduler to manage readiness.'
24
25
 
25
- TARGETS = {
26
- 'IO' => :select,
27
- 'Kernel' => :select
28
- }.freeze
26
+ TARGETS = OperationVocabulary::FA1005_TARGETS
29
27
 
30
28
  class << self
31
29
  def title = TITLE
@@ -4,6 +4,7 @@ require_relative 'base'
4
4
  require_relative '../../findings/evidence'
5
5
  require_relative '../../correlation/fingerprint'
6
6
  require_relative '../../findings/finding'
7
+ require_relative '../../operation_vocabulary'
7
8
 
8
9
  module FiberAudit
9
10
  module Static
@@ -22,11 +23,8 @@ module FiberAudit
22
23
  MESSAGE = 'Synchronous HTTP activity in a request-like context may block the thread running the fiber scheduler.'
23
24
  REMEDIATION = 'Use a scheduler-aware HTTP client, or move outbound HTTP work outside the request path.'
24
25
 
25
- NET_HTTP_METHODS = %i[get get_response start request].freeze
26
- URI_METHODS = {
27
- 'URI' => :open,
28
- 'OpenURI' => :open_uri
29
- }.freeze
26
+ NET_HTTP_METHODS = OperationVocabulary::FA1007_NET_HTTP_METHODS
27
+ URI_METHODS = OperationVocabulary::FA1007_URI_METHODS
30
28
  EMIT_CONTEXTS = %i[request middleware websocket callback].freeze
31
29
 
32
30
  def analyze(call_sites:)
@@ -4,6 +4,7 @@ require_relative 'base'
4
4
  require_relative '../../findings/evidence'
5
5
  require_relative '../../correlation/fingerprint'
6
6
  require_relative '../../findings/finding'
7
+ require_relative '../../operation_vocabulary'
7
8
 
8
9
  module FiberAudit
9
10
  module Static
@@ -16,12 +17,7 @@ module FiberAudit
16
17
 
17
18
  RULE_TITLE = 'Thread synchronization'
18
19
  RULE_CATEGORY = :synchronization
19
- TARGETS = {
20
- 'Mutex' => %i[lock synchronize try_lock],
21
- 'ConditionVariable' => %i[wait],
22
- 'Monitor' => %i[synchronize],
23
- 'MonitorMixin' => %i[synchronize]
24
- }.freeze
20
+ TARGETS = OperationVocabulary::FA1003_TARGETS
25
21
  TRY_LOCK_MSG = 'Mutex.try_lock is non-blocking but may indicate ' \
26
22
  'thread-oriented synchronization in fiber-scheduled code.'
27
23
  NORMAL_MSG = 'Synchronization operation may block the thread running the fiber scheduler.'