fiber_audit 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +26 -24
- data/CHANGELOG.md +23 -0
- data/README.md +42 -18
- data/lib/fiber_audit/cli.rb +16 -4
- data/lib/fiber_audit/operation_vocabulary.rb +2 -1
- data/lib/fiber_audit/runtime/active_operations.rb +19 -5
- data/lib/fiber_audit/runtime/execution_context.rb +55 -43
- data/lib/fiber_audit/runtime/probes/base.rb +24 -3
- data/lib/fiber_audit/runtime/probes/subprocess.rb +89 -8
- data/lib/fiber_audit/runtime/probes/thread_state.rb +0 -14
- data/lib/fiber_audit/runtime/scheduler_observer.rb +54 -18
- data/lib/fiber_audit/runtime/scheduler_snapshot.rb +113 -0
- data/lib/fiber_audit/runtime/watchdog.rb +44 -2
- data/lib/fiber_audit/runtime.rb +1 -0
- data/lib/fiber_audit/static/call_site_extractor.rb +1 -0
- data/lib/fiber_audit/static/rules/base.rb +19 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +90 -10
- data/lib/fiber_audit/static/rules/direct_socket.rb +8 -9
- data/lib/fiber_audit/static/rules/io_select.rb +6 -6
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +11 -8
- data/lib/fiber_audit/static/rules/synchronization.rb +13 -8
- data/lib/fiber_audit/static/rules/thread_current_state.rb +18 -20
- data/lib/fiber_audit/static/rules/thread_join.rb +9 -7
- data/lib/fiber_audit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64afd3fe0ced532129e0d6e55508edca15e7a613ca30c064a61b9a868b155488
|
|
4
|
+
data.tar.gz: a4db073387d08a250c8c79e95251ebc39ddb414a2a830d5ddb91c85d673d9989
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a632c52483c6b266bbbe1d99ca7e5dfb3c35d5556d364da7bd3adaab44308c551eda6db22d77c8b15871158386015ac2fe9c0fe69d0336a31d4094df5f4d5c4d
|
|
7
|
+
data.tar.gz: 638caf18b5c0273abe010457b74e33bc79082a857d3196c6020394b8d771d5a3479d27d83f1cc97abdcc4ace7dbc6eee01285fa7013b8a59019ac4bc53646a2b
|
data/ARCHITECTURE.md
CHANGED
|
@@ -20,13 +20,10 @@ produces hypotheses, while observational runtime sessions provide bounded eviden
|
|
|
20
20
|
without establishing complete coverage. FiberAudit never claims unconditional
|
|
21
21
|
`PASS`.
|
|
22
22
|
|
|
23
|
-
> **Repository status:** v0.
|
|
24
|
-
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
> child-process boot, lifecycle, supervising command, bounded scheduler
|
|
28
|
-
> watchdog, targeted FA1001–FA1007 operation probes, and Rails runtime execution
|
|
29
|
-
> contexts are implemented; static/runtime correlation remains future work.
|
|
23
|
+
> **Repository status:** v0.3.0 includes the static pipeline end to end,
|
|
24
|
+
> observational runtime probes, propagated Rails execution context,
|
|
25
|
+
> scheduler-capability snapshots, and bounded operation/stall overlap events.
|
|
26
|
+
> Combined static/runtime reporting remains future work.
|
|
30
27
|
|
|
31
28
|
## 2. Scope
|
|
32
29
|
|
|
@@ -60,8 +57,8 @@ Current code and specs define implementation truth. This document records the
|
|
|
60
57
|
supported architecture and dependency boundaries; README and CHANGELOG record
|
|
61
58
|
the user-facing release contract.
|
|
62
59
|
|
|
63
|
-
The
|
|
64
|
-
and
|
|
60
|
+
The platform target is Ruby `>= 3.3` with CI configured for Ruby 3.3, 3.4,
|
|
61
|
+
and 4.0. Ruby 3.2 is excluded because it is end-of-life.
|
|
65
62
|
|
|
66
63
|
## 4. Architectural Principles
|
|
67
64
|
|
|
@@ -363,17 +360,17 @@ explicit and can lower confidence.
|
|
|
363
360
|
Rules consume FiberAudit `CallSite` values and emit `Finding` values. They do
|
|
364
361
|
not parse files and do not access Rubydex directly.
|
|
365
362
|
|
|
366
|
-
Shipped
|
|
363
|
+
Shipped rules:
|
|
367
364
|
|
|
368
365
|
| ID | Concern | Default severity |
|
|
369
366
|
|---|---|---:|
|
|
370
|
-
| FA1001 |
|
|
371
|
-
| FA1002 |
|
|
372
|
-
| FA1003 |
|
|
373
|
-
| FA1004 | Thread
|
|
374
|
-
| FA1005 |
|
|
375
|
-
| FA1006 |
|
|
376
|
-
| FA1007 |
|
|
367
|
+
| FA1001 | Subprocess lifecycle and process-wait cooperation | info/medium |
|
|
368
|
+
| FA1002 | Thread-wait scheduler coordination | low |
|
|
369
|
+
| FA1003 | Synchronization scheduler coordination | low/info |
|
|
370
|
+
| FA1004 | True Thread variables shared across sibling Fibers | high |
|
|
371
|
+
| FA1005 | `IO.select` scheduler capability requirement | medium |
|
|
372
|
+
| FA1006 | Socket/DNS/I/O scheduler cooperation | low |
|
|
373
|
+
| FA1007 | HTTP scheduler cooperation in request-like contexts | medium |
|
|
377
374
|
|
|
378
375
|
A rule registry owns registration, enumeration, configuration enablement, and
|
|
379
376
|
metadata used by `list-rules` and `explain`.
|
|
@@ -595,7 +592,8 @@ spec/fixtures/apps/rails_contexts
|
|
|
595
592
|
spec/fixtures/reports/rails_blockers_v0.1.json
|
|
596
593
|
```
|
|
597
594
|
|
|
598
|
-
CI is configured to run linting, specs, and gem packaging on Ruby 3.3
|
|
595
|
+
CI is configured to run linting, specs, and gem packaging on Ruby 3.3, 3.4,
|
|
596
|
+
and 4.0.
|
|
599
597
|
The workflow configuration does not itself prove that remote CI has passed.
|
|
600
598
|
|
|
601
599
|
## 13. Runtime Architecture Beyond v0.1.0
|
|
@@ -666,11 +664,11 @@ stall, and only then closes the recorder. Fork rebinding discards inherited prob
|
|
|
666
664
|
and watchdog references before touching their locks and creates process-local
|
|
667
665
|
replacements.
|
|
668
666
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
667
|
+
Rails execution context detection uses a bounded immutable frame chain in Ruby
|
|
668
|
+
Fiber storage. Child Fibers inherit the current logical context snapshot, while
|
|
669
|
+
child overrides and `clear!` remain local to that Fiber. Frames carry PID and
|
|
670
|
+
Thread ownership, validate against `Context::ALL`, enforce `MAX_DEPTH = 32`, and
|
|
671
|
+
reset on fork. A process-local `RailsIntegration` class hooks into Rails boundaries
|
|
674
672
|
via `Module#prepend`: Rack middleware (`:middleware`), `ActionController::Metal#process_action`
|
|
675
673
|
(`:request`), `ActiveJob::Base#perform_now` (`:job`), and `ActionCable::Channel::Base#dispatch_action`
|
|
676
674
|
(`:websocket`). Wrappers consult the active integration before setting context and become
|
|
@@ -680,7 +678,11 @@ even after runtime boot. Probe observations snapshot the context once at start a
|
|
|
680
678
|
propagate it through active operations and events. Lifecycle wires context store and
|
|
681
679
|
Rails integration ownership, shutdown deactivates Rails integration before probes,
|
|
682
680
|
and fork rebinding resets context and rebuilds integration. JSONL schema 1.0 and
|
|
683
|
-
privacy requirements are preserved; no new schema fields are added.
|
|
681
|
+
privacy requirements are preserved; no new schema fields are added. Runtime
|
|
682
|
+
operation events also record scheduler presence, blocking-Fiber state, and
|
|
683
|
+
optional hook support. Watchdog stalls emit bounded
|
|
684
|
+
`scheduler_stall_operation_overlap` events for operations active on the same
|
|
685
|
+
Thread. These events establish temporal overlap, not causality.
|
|
684
686
|
|
|
685
687
|
Loading the gem normally performs no instrumentation, fibers, threads, or file
|
|
686
688
|
I/O. The observer is activated only by explicit runtime boot. A native operation
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.0 (2026-08-12)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Reframed static rules as scheduler-cooperation requirements. Advisory rules no
|
|
10
|
+
longer escalate solely because they appear in request-like execution contexts.
|
|
11
|
+
- Split FA1001 under its existing rule ID into subprocess creation, replacement,
|
|
12
|
+
waiting, detach, and stream lifecycle semantics; added Process `spawn`, `exec`,
|
|
13
|
+
`wait`, `wait2`, `waitpid`, `waitpid2`, and `Process::Status.wait` coverage.
|
|
14
|
+
- Corrected FA1004 to report only true Thread variables. `Thread.current[]` and
|
|
15
|
+
`Thread.current[]=` are Fiber-local and are no longer static or runtime findings.
|
|
16
|
+
- Propagated Rails/runtime execution context into child Fibers through immutable
|
|
17
|
+
Ruby Fiber storage, with explicit current-Fiber `clear!`/`reset!` behavior.
|
|
18
|
+
- Added scheduler presence, blocking-Fiber state, and optional scheduler-hook
|
|
19
|
+
capability measurements to targeted operation events.
|
|
20
|
+
- Added bounded `scheduler_stall_operation_overlap` JSONL 1.0 events. They record
|
|
21
|
+
temporal overlap between a watchdog stall and active operations, not causality.
|
|
22
|
+
- Reconciled scheduler observer state only after `Fiber.set_scheduler` accepts a
|
|
23
|
+
change, so rejected replacement attempts preserve the previous observation.
|
|
24
|
+
- Added Ruby 4.0 to the required CI matrix and executable local scheduler-semantic
|
|
25
|
+
reproductions under `script/scheduler-semantics`, including Ruby 4's
|
|
26
|
+
`fiber_interrupt` scheduler protocol hook.
|
|
27
|
+
|
|
5
28
|
## 0.2.1 (2026-08-12)
|
|
6
29
|
|
|
7
30
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# FiberAudit
|
|
2
2
|
|
|
3
|
-
FiberAudit audits Ruby and Rails code for operations that
|
|
4
|
-
|
|
5
|
-
observational runtime audit.
|
|
3
|
+
FiberAudit audits Ruby and Rails code for operations that require cooperation
|
|
4
|
+
from a Fiber scheduler. Version 0.3.0 includes static analysis and an explicit,
|
|
5
|
+
observational runtime audit with scheduler-capability and stall-overlap evidence.
|
|
6
6
|
|
|
7
7
|
> **Safety disclaimer:** FiberAudit does not prove that an application is
|
|
8
8
|
> fiber-safe. Static findings are hypotheses, and absence of runtime events does
|
|
@@ -10,7 +10,7 @@ observational runtime audit.
|
|
|
10
10
|
|
|
11
11
|
## Requirements and installation
|
|
12
12
|
|
|
13
|
-
FiberAudit v0.
|
|
13
|
+
FiberAudit v0.3.0 supports Ruby 3.3, 3.4, and 4.0.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
gem install fiber_audit
|
|
@@ -52,13 +52,13 @@ Explicit `--format` always wins.
|
|
|
52
52
|
|
|
53
53
|
| ID | Detects | Default severity |
|
|
54
54
|
|---|---|---|
|
|
55
|
-
| FA1001 |
|
|
56
|
-
| FA1002 |
|
|
57
|
-
| FA1003 |
|
|
58
|
-
| FA1004 | Thread-
|
|
59
|
-
| FA1005 |
|
|
60
|
-
| FA1006 |
|
|
61
|
-
| FA1007 |
|
|
55
|
+
| FA1001 | Subprocess creation, replacement, waiting, and streams | info/medium |
|
|
56
|
+
| FA1002 | Thread-wait scheduler coordination | low |
|
|
57
|
+
| FA1003 | Synchronization scheduler coordination | low/info |
|
|
58
|
+
| FA1004 | True Thread-variable state shared by sibling Fibers | high |
|
|
59
|
+
| FA1005 | `IO.select` scheduler capability requirement | medium |
|
|
60
|
+
| FA1006 | Socket/DNS/I/O scheduler cooperation | low |
|
|
61
|
+
| FA1007 | HTTP scheduler cooperation in request-like contexts | medium |
|
|
62
62
|
|
|
63
63
|
Use `fiber-audit explain <RULE_ID>` for exact targets and remediation.
|
|
64
64
|
|
|
@@ -82,13 +82,17 @@ Libraries such as Open3, Monitor, Socket, Net::HTTP, and OpenURI may be loaded
|
|
|
82
82
|
after runtime boot; FiberAudit rescans only these known targets after `require`.
|
|
83
83
|
|
|
84
84
|
Rails execution contexts (`:request`, `:middleware`, `:job`, `:websocket`) are
|
|
85
|
-
captured automatically when Rails integration is active. A bounded, PID-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
captured automatically when Rails integration is active. A bounded, PID- and
|
|
86
|
+
Thread-aware immutable context chain uses inheritable Ruby Fiber storage, so
|
|
87
|
+
child Fibers receive a snapshot of their parent's logical context. FiberAudit's
|
|
88
|
+
`ExecutionContext.clear!` explicitly detaches the current Fiber without allowing
|
|
89
|
+
enclosing scopes to restore stale context. Rails boundaries use prepend hooks that become inert
|
|
88
90
|
after deactivation or fork, preserving application semantics without interfering
|
|
89
91
|
with normal Rails operation. The integration supports late loading: hooks are
|
|
90
92
|
installed when Rails components become available, even after runtime boot.
|
|
91
|
-
|
|
93
|
+
Raw runtime JSONL now records bounded `scheduler_stall_operation_overlap`
|
|
94
|
+
events linking active operation sequences to watchdog stalls. This establishes
|
|
95
|
+
temporal overlap, not causality. Combined static/runtime reporting remains future work.
|
|
92
96
|
|
|
93
97
|
The scheduler watchdog records one bounded start/completion pair when its
|
|
94
98
|
scheduler-owned heartbeat stops progressing past the configured threshold.
|
|
@@ -190,7 +194,7 @@ Missing reasons and invalid configuration return exit code 2.
|
|
|
190
194
|
- `PASS_WITH_WARNINGS` — only low or informational findings.
|
|
191
195
|
- `NO_FINDINGS` — no findings at the configured threshold.
|
|
192
196
|
|
|
193
|
-
FiberAudit never emits unconditional `PASS` in v0.
|
|
197
|
+
FiberAudit never emits unconditional `PASS` in v0.3.0.
|
|
194
198
|
|
|
195
199
|
## Exit codes
|
|
196
200
|
|
|
@@ -199,10 +203,30 @@ FiberAudit never emits unconditional `PASS` in v0.2.1.
|
|
|
199
203
|
| 0 | No active finding at or above the configured threshold |
|
|
200
204
|
| 1 | One or more active findings at or above the threshold |
|
|
201
205
|
| 2 | Invalid options, configuration, analysis, or report output |
|
|
202
|
-
| 3 | Reserved; never emitted by v0.
|
|
206
|
+
| 3 | Reserved; never emitted by v0.3.0 |
|
|
203
207
|
|
|
204
208
|
Source parse errors are included in report data while analysis continues on
|
|
205
209
|
other files.
|
|
206
210
|
|
|
211
|
+
## Development and semantic verification
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
bundle exec rspec
|
|
215
|
+
bundle exec rubocop
|
|
216
|
+
bundle exec ruby script/scheduler-semantics
|
|
217
|
+
gem build fiber_audit.gemspec
|
|
218
|
+
bundle exec rake release:sanity
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The semantic probe uses only local Threads, pipes, and child processes. It checks
|
|
222
|
+
scheduler coordination, Fiber-storage inheritance, Thread/Fiber state semantics,
|
|
223
|
+
and rejected scheduler replacement against each supported Ruby in CI.
|
|
224
|
+
|
|
225
|
+
Relevant Ruby contracts:
|
|
226
|
+
|
|
227
|
+
- [Fiber and inheritable storage](https://docs.ruby-lang.org/en/3.4/Fiber.html)
|
|
228
|
+
- [Fiber::Scheduler hooks](https://docs.ruby-lang.org/en/3.4/Fiber/Scheduler.html)
|
|
229
|
+
- [Thread Fiber-local and Thread-wide storage](https://docs.ruby-lang.org/en/3.4/Thread.html#method-i-5B-5D)
|
|
230
|
+
|
|
207
231
|
See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation boundaries, runtime
|
|
208
|
-
architecture, and
|
|
232
|
+
architecture, raw stall-overlap evidence, and deferred combined reporting.
|
data/lib/fiber_audit/cli.rb
CHANGED
|
@@ -274,12 +274,25 @@ module FiberAudit
|
|
|
274
274
|
stdout.puts "Description: #{rule_class.description}"
|
|
275
275
|
stdout.puts 'Targets:'
|
|
276
276
|
rule_targets(rule_class).each { |target| stdout.puts " - #{target}" }
|
|
277
|
-
stdout
|
|
277
|
+
print_rule_remediations(rule_class, stdout)
|
|
278
278
|
0
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
+
def print_rule_remediations(rule_class, stdout)
|
|
282
|
+
values = if rule_class.const_defined?(:REMEDIATION, false)
|
|
283
|
+
[rule_class.const_get(:REMEDIATION)]
|
|
284
|
+
elsif rule_class.const_defined?(:CATEGORY_METADATA, false)
|
|
285
|
+
rule_class.const_get(:CATEGORY_METADATA).values.map { |metadata| metadata.fetch(:remediation) }.uniq
|
|
286
|
+
else
|
|
287
|
+
[]
|
|
288
|
+
end
|
|
289
|
+
values.each_with_index do |remediation, index|
|
|
290
|
+
stdout.puts "#{index.zero? ? 'Remediation:' : ' '} #{remediation}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
281
294
|
def rule_title(rule_class)
|
|
282
|
-
return '
|
|
295
|
+
return 'Subprocess lifecycle operations' if rule_class.id == 'FA1001'
|
|
283
296
|
|
|
284
297
|
%i[TITLE RULE_TITLE].each do |name|
|
|
285
298
|
return rule_class.const_get(name) if rule_class.const_defined?(name, false)
|
|
@@ -296,8 +309,7 @@ module FiberAudit
|
|
|
296
309
|
when 'FA1003'
|
|
297
310
|
expand_target_map(rule_class::TARGETS, '#')
|
|
298
311
|
when 'FA1004'
|
|
299
|
-
rule_class::THREAD_VARIABLE_METHODS.map { |method| "Thread##{method}" }
|
|
300
|
-
rule_class::INDEX_METHODS.map { |method| "Thread.current.#{method}" }
|
|
312
|
+
rule_class::THREAD_VARIABLE_METHODS.map { |method| "Thread##{method}" }
|
|
301
313
|
when 'FA1005'
|
|
302
314
|
expand_target_map(rule_class::TARGETS.transform_values { |method| [method] }, '.')
|
|
303
315
|
when 'FA1006'
|
|
@@ -7,7 +7,8 @@ module FiberAudit
|
|
|
7
7
|
'Kernel' => %i[system exec spawn].freeze,
|
|
8
8
|
'Open3' => %i[capture2 capture2e capture3 pipeline].freeze,
|
|
9
9
|
'IO' => %i[popen].freeze,
|
|
10
|
-
'Process' => %i[waitall detach].freeze
|
|
10
|
+
'Process' => %i[spawn exec wait wait2 waitpid waitpid2 waitall detach].freeze,
|
|
11
|
+
'Process::Status' => %i[wait].freeze
|
|
11
12
|
}.freeze
|
|
12
13
|
FA1001_KERNEL_METHODS = %i[system exec spawn].freeze
|
|
13
14
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'event'
|
|
4
4
|
require_relative 'location'
|
|
5
|
+
require_relative 'scheduler_snapshot'
|
|
5
6
|
require_relative 'validation'
|
|
6
7
|
|
|
7
8
|
module FiberAudit
|
|
@@ -19,7 +20,8 @@ module FiberAudit
|
|
|
19
20
|
:operation,
|
|
20
21
|
:location,
|
|
21
22
|
:execution_context,
|
|
22
|
-
:started_monotonic_ns
|
|
23
|
+
:started_monotonic_ns,
|
|
24
|
+
:scheduler_snapshot
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
def initialize(pid_source: Process.method(:pid), capacity: MAX_ENTRIES, snapshot_limit: MAX_SNAPSHOT)
|
|
@@ -38,7 +40,8 @@ module FiberAudit
|
|
|
38
40
|
location: nil,
|
|
39
41
|
execution_context: :unknown,
|
|
40
42
|
thread: Thread.current,
|
|
41
|
-
fiber: Fiber.current
|
|
43
|
+
fiber: Fiber.current,
|
|
44
|
+
scheduler_snapshot: nil
|
|
42
45
|
)
|
|
43
46
|
ensure_current_process!
|
|
44
47
|
values = normalize_entry(
|
|
@@ -47,7 +50,8 @@ module FiberAudit
|
|
|
47
50
|
execution_context: execution_context,
|
|
48
51
|
monotonic_ns: monotonic_ns,
|
|
49
52
|
thread: thread,
|
|
50
|
-
fiber: fiber
|
|
53
|
+
fiber: fiber,
|
|
54
|
+
scheduler_snapshot: scheduler_snapshot
|
|
51
55
|
)
|
|
52
56
|
|
|
53
57
|
@mutex.synchronize do
|
|
@@ -90,7 +94,7 @@ module FiberAudit
|
|
|
90
94
|
|
|
91
95
|
private
|
|
92
96
|
|
|
93
|
-
def normalize_entry(operation:, location:, execution_context:, monotonic_ns:, thread:, fiber:)
|
|
97
|
+
def normalize_entry(operation:, location:, execution_context:, monotonic_ns:, thread:, fiber:, scheduler_snapshot: nil)
|
|
94
98
|
canonical_operation = Validation.operation(operation)
|
|
95
99
|
unless location.nil? || location.is_a?(Location)
|
|
96
100
|
raise RuntimeContractError, 'location must be a FiberAudit::Runtime::Location or nil'
|
|
@@ -103,16 +107,26 @@ module FiberAudit
|
|
|
103
107
|
raise RuntimeContractError, 'thread and fiber identities are invalid'
|
|
104
108
|
end
|
|
105
109
|
|
|
110
|
+
normalized_snapshot = normalize_scheduler_snapshot(scheduler_snapshot)
|
|
111
|
+
|
|
106
112
|
{
|
|
107
113
|
thread_id: Validation.integer(thread.object_id, 'thread_id'),
|
|
108
114
|
fiber_id: Validation.integer(fiber.object_id, 'fiber_id'),
|
|
109
115
|
operation: canonical_operation,
|
|
110
116
|
location: location,
|
|
111
117
|
execution_context: normalized_context,
|
|
112
|
-
started_monotonic_ns: Validation.integer(monotonic_ns, 'monotonic_ns')
|
|
118
|
+
started_monotonic_ns: Validation.integer(monotonic_ns, 'monotonic_ns'),
|
|
119
|
+
scheduler_snapshot: normalized_snapshot
|
|
113
120
|
}
|
|
114
121
|
end
|
|
115
122
|
|
|
123
|
+
def normalize_scheduler_snapshot(value)
|
|
124
|
+
return value if value.nil?
|
|
125
|
+
return value if value.is_a?(SchedulerSnapshot)
|
|
126
|
+
|
|
127
|
+
raise RuntimeContractError, 'scheduler_snapshot must be a FiberAudit::Runtime::SchedulerSnapshot or nil'
|
|
128
|
+
end
|
|
129
|
+
|
|
116
130
|
def ensure_current_process!
|
|
117
131
|
pid = current_pid
|
|
118
132
|
reset_for_process!(pid) unless pid == @owner_pid
|
|
@@ -4,73 +4,85 @@ require_relative '../execution_context'
|
|
|
4
4
|
|
|
5
5
|
module FiberAudit
|
|
6
6
|
module Runtime
|
|
7
|
-
# Fiber-local execution context
|
|
8
|
-
# Uses
|
|
9
|
-
#
|
|
7
|
+
# Fiber-local execution context with propagation to child fibers.
|
|
8
|
+
# Uses Ruby Fiber storage (Fiber[]) for fiber-local state that is
|
|
9
|
+
# inherited by child fibers at creation, enabling automatic context
|
|
10
|
+
# propagation across Fiber boundaries.
|
|
11
|
+
#
|
|
12
|
+
# Frames are immutable (frozen Data objects) forming a linked list.
|
|
13
|
+
# Each +with+ call creates a new frame pointing to the parent frame.
|
|
14
|
+
#
|
|
15
|
+
# Semantics:
|
|
16
|
+
# - Child fibers inherit the parent fiber's current context at creation
|
|
17
|
+
# - Child fiber overrides do not alter parent context
|
|
18
|
+
# - +clear!+ removes context for the current fiber only
|
|
19
|
+
# - +clear!+ inside nested +with+ is not undone by the enclosing ensure
|
|
20
|
+
# (ensure restores only when its own frame is still the active one)
|
|
21
|
+
# - Thread isolation is maintained even though Ruby copies Fiber storage to a
|
|
22
|
+
# newly created Thread's root Fiber
|
|
23
|
+
# - PID mismatch (after fork) is treated as empty context
|
|
24
|
+
# - MAX_DEPTH overflow exposes :unknown rather than stale outer context
|
|
10
25
|
module ExecutionContext
|
|
11
26
|
MAX_DEPTH = 32
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
FRAME_KEY = :__fiber_audit_execution_context_frame__
|
|
28
|
+
|
|
29
|
+
# Immutable frame in the context chain. Process and Thread ownership keep
|
|
30
|
+
# inherited storage from crossing fork or Thread boundaries.
|
|
31
|
+
Frame = Data.define(:context, :parent, :depth, :pid, :thread_id)
|
|
32
|
+
private_constant :Frame
|
|
14
33
|
|
|
15
34
|
class << self
|
|
16
35
|
def current
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
state[:stack].last || Context::UNKNOWN
|
|
36
|
+
frame = current_frame
|
|
37
|
+
frame ? frame.context : Context::UNKNOWN
|
|
21
38
|
end
|
|
22
39
|
|
|
23
40
|
def with(context)
|
|
24
41
|
normalized = validate_context(context)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
parent = current_frame
|
|
43
|
+
|
|
44
|
+
new_depth = parent ? parent.depth + 1 : 1
|
|
45
|
+
effective = new_depth > MAX_DEPTH ? Context::UNKNOWN : normalized
|
|
46
|
+
frame = Frame.new(
|
|
47
|
+
context: effective,
|
|
48
|
+
parent: parent,
|
|
49
|
+
depth: new_depth,
|
|
50
|
+
pid: Process.pid,
|
|
51
|
+
thread_id: Thread.current.object_id
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
Fiber[FRAME_KEY] = frame
|
|
29
55
|
begin
|
|
30
56
|
yield
|
|
31
57
|
ensure
|
|
32
|
-
|
|
58
|
+
# Only restore if our frame is still the active one.
|
|
59
|
+
# If clear! was called (or another with replaced it), skip restore.
|
|
60
|
+
Fiber[FRAME_KEY] = parent if Fiber[FRAME_KEY].equal?(frame)
|
|
33
61
|
end
|
|
34
62
|
end
|
|
35
63
|
|
|
64
|
+
def clear!
|
|
65
|
+
Fiber[FRAME_KEY] = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Compatibility alias for clear!.
|
|
36
69
|
def reset!
|
|
37
|
-
|
|
38
|
-
fiber.remove_instance_variable(IVAR_KEY) if fiber.instance_variable_defined?(IVAR_KEY)
|
|
39
|
-
fiber.remove_instance_variable(IVAR_PID_KEY) if fiber.instance_variable_defined?(IVAR_PID_KEY)
|
|
70
|
+
clear!
|
|
40
71
|
end
|
|
41
72
|
|
|
73
|
+
# Clear context after fork.
|
|
42
74
|
def after_fork!
|
|
43
|
-
|
|
75
|
+
clear!
|
|
44
76
|
end
|
|
45
77
|
|
|
46
78
|
private
|
|
47
79
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
return nil unless
|
|
51
|
-
|
|
52
|
-
pid = fiber.instance_variable_defined?(IVAR_PID_KEY) ? fiber.instance_variable_get(IVAR_PID_KEY) : nil
|
|
53
|
-
return nil unless pid == Process.pid
|
|
54
|
-
|
|
55
|
-
{ stack: fiber.instance_variable_get(IVAR_KEY), pid: pid }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def ensure_state
|
|
59
|
-
fiber = Fiber.current
|
|
60
|
-
pid = Process.pid
|
|
61
|
-
|
|
62
|
-
if fiber.instance_variable_defined?(IVAR_KEY)
|
|
63
|
-
stored_pid = fiber.instance_variable_defined?(IVAR_PID_KEY) ? fiber.instance_variable_get(IVAR_PID_KEY) : nil
|
|
64
|
-
return { stack: fiber.instance_variable_get(IVAR_KEY), pid: pid } if stored_pid == pid
|
|
65
|
-
|
|
66
|
-
# PID mismatch - reset
|
|
67
|
-
reset!
|
|
68
|
-
end
|
|
80
|
+
def current_frame
|
|
81
|
+
frame = Fiber[FRAME_KEY]
|
|
82
|
+
return nil unless frame&.pid == Process.pid
|
|
83
|
+
return nil unless frame.thread_id == Thread.current.object_id
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
fiber.instance_variable_set(IVAR_KEY, stack)
|
|
72
|
-
fiber.instance_variable_set(IVAR_PID_KEY, pid)
|
|
73
|
-
{ stack: stack, pid: pid }
|
|
85
|
+
frame
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
def validate_context(value)
|
|
@@ -7,6 +7,7 @@ require_relative '../execution_context'
|
|
|
7
7
|
require_relative '../rails_integration'
|
|
8
8
|
require_relative '../recorder'
|
|
9
9
|
require_relative '../redactor'
|
|
10
|
+
require_relative '../scheduler_snapshot'
|
|
10
11
|
|
|
11
12
|
module FiberAudit
|
|
12
13
|
module Runtime
|
|
@@ -27,7 +28,8 @@ module FiberAudit
|
|
|
27
28
|
:thread_id,
|
|
28
29
|
:fiber_id,
|
|
29
30
|
:measurements,
|
|
30
|
-
:execution_context
|
|
31
|
+
:execution_context,
|
|
32
|
+
:scheduler_snapshot
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
attr_reader :recorder, :clock, :redactor, :active_operations, :owner_pid, :execution_context_store
|
|
@@ -125,13 +127,15 @@ module FiberAudit
|
|
|
125
127
|
thread = Thread.current
|
|
126
128
|
fiber = Fiber.current
|
|
127
129
|
captured_context = capture_execution_context
|
|
130
|
+
captured_scheduler_snapshot = capture_scheduler_snapshot
|
|
128
131
|
handle = active_operations.register(
|
|
129
132
|
operation: canonical_operation,
|
|
130
133
|
monotonic_ns: started_ns,
|
|
131
134
|
location: location,
|
|
132
135
|
execution_context: captured_context,
|
|
133
136
|
thread: thread,
|
|
134
|
-
fiber: fiber
|
|
137
|
+
fiber: fiber,
|
|
138
|
+
scheduler_snapshot: captured_scheduler_snapshot
|
|
135
139
|
)
|
|
136
140
|
Observation.new(
|
|
137
141
|
operation: canonical_operation,
|
|
@@ -141,7 +145,8 @@ module FiberAudit
|
|
|
141
145
|
thread_id: thread.object_id,
|
|
142
146
|
fiber_id: fiber.object_id,
|
|
143
147
|
measurements: normalized_measurements,
|
|
144
|
-
execution_context: captured_context
|
|
148
|
+
execution_context: captured_context,
|
|
149
|
+
scheduler_snapshot: captured_scheduler_snapshot
|
|
145
150
|
)
|
|
146
151
|
end
|
|
147
152
|
# rubocop:enable Metrics/BlockLength
|
|
@@ -155,6 +160,10 @@ module FiberAudit
|
|
|
155
160
|
Context::UNKNOWN
|
|
156
161
|
end
|
|
157
162
|
|
|
163
|
+
def capture_scheduler_snapshot
|
|
164
|
+
SchedulerSnapshotCapture.capture
|
|
165
|
+
end
|
|
166
|
+
|
|
158
167
|
def emit_start_observation(observation)
|
|
159
168
|
emit_observation(:operation_started, observation, monotonic_ns: observation.started_monotonic_ns)
|
|
160
169
|
rescue StandardError => e
|
|
@@ -207,11 +216,14 @@ module FiberAudit
|
|
|
207
216
|
values.merge!(generated)
|
|
208
217
|
end
|
|
209
218
|
values[:operation_sequence] = observation.handle&.sequence
|
|
219
|
+
# Include scheduler snapshot measurements (immutable, captured at operation start)
|
|
220
|
+
values.merge!(observation.scheduler_snapshot.to_measurements) if observation.scheduler_snapshot
|
|
210
221
|
values
|
|
211
222
|
end
|
|
212
223
|
|
|
213
224
|
def emit_observation(kind, observation, monotonic_ns:, duration_ns: nil, measurements: nil)
|
|
214
225
|
values = measurements || observation.measurements.merge(operation_sequence: observation.handle&.sequence)
|
|
226
|
+
values = merge_scheduler_measurements_for_emit(values.dup, observation.scheduler_snapshot)
|
|
215
227
|
recorder.record do
|
|
216
228
|
Event.new(
|
|
217
229
|
kind: kind,
|
|
@@ -229,6 +241,15 @@ module FiberAudit
|
|
|
229
241
|
end
|
|
230
242
|
end
|
|
231
243
|
|
|
244
|
+
def merge_scheduler_measurements_for_emit(values, scheduler_snapshot)
|
|
245
|
+
return values unless scheduler_snapshot
|
|
246
|
+
|
|
247
|
+
scheduler_snapshot.to_measurements.each do |key, value|
|
|
248
|
+
values[key.to_sym] = value unless values.key?(key.to_sym)
|
|
249
|
+
end
|
|
250
|
+
values
|
|
251
|
+
end
|
|
252
|
+
|
|
232
253
|
def normalize_measurements(value)
|
|
233
254
|
raise RuntimeContractError, 'probe measurements must be a Hash' unless value.is_a?(Hash)
|
|
234
255
|
|