Skip to content

fix: isolate inherited hubs under hub_isolation_level = :fiber#3036

Open
SeanLF wants to merge 2 commits into
getsentry:masterfrom
SeanLF:fix/fiber-hub-inherited-storage
Open

fix: isolate inherited hubs under hub_isolation_level = :fiber#3036
SeanLF wants to merge 2 commits into
getsentry:masterfrom
SeanLF:fix/fiber-hub-inherited-storage

Conversation

@SeanLF

@SeanLF SeanLF commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Fixes the :fiber isolation level, which did not isolate anything. Reported by @trevorturk in #3018.

The bug

Fiber storage is inherited by child fibers and by threads started from a fiber, and the inherited value is the same object. Once anything seeds a hub in an ancestor fiber, every sibling request fiber and every worker thread shares one Hub, along with its unsynchronized @stack.

Reproduced on master (Ruby 4.0):

  • 4 threads spawned after the seed: 1 hub instead of 4
  • 4 concurrent jobs in with_scope: every one read another job's tags
  • 3 sibling request fibers: 1 hub instead of 3

The || clone fallback in get_current_hub never fires in those contexts, because the inherited value is not nil.

The fix

Tag each stored entry with its owning fiber. A context that reads an entry it did not store gets its own copy instead of sharing.

The copy is a fork, not Hub#clone: private scope stack, but the live span. Without that, Scope#dup deep-dups the span, Transaction#deep_dup builds a fresh SpanRecorder, and every child-fiber span lands on a detached transaction copy that is never sent. Async fan-out inside a request is the workload this mode exists for.

Fiber contexts now behave like thread contexts (own hub, own scope stack) and stay on the same trace.

Behaviour change

The spec asserting a child fiber gets the same hub object is inverted: it now asserts the child gets the parent's context in its own hub. Sharing the object is what caused the leak.

Sentry.last_event_id and the per-hub profiler registry do not cross the boundary, matching existing :thread behaviour.

Testing

  • 1416 examples, 0 failures on Ruby 3.2 (Docker), 3.4 and 4.0
  • The four isolation specs fail without the change. The span spec passes before it (sharing a hub did record the spans) and fails if the fork is replaced with a plain Hub#clone, so it guards the fix rather than the original bug
  • Fiber semantics verified identical on 3.2 / 3.3 / 3.4 / 4.0. On Ruby < 3.2 and JRuby the fiber-storage gem polyfills Fiber[], so :fiber is active there too; the polyfill inherits into child fibers (same bug, same fix) but not into Thread.new, so a thread there already starts from the main hub
  • Hot path costs about 26ns more per get_current_hub (50ns to 77ns), no extra allocations. The fork runs once per new context and is O(spans)

SeanLF and others added 2 commits July 25, 2026 21:56
Fiber storage is inherited by child fibers and by threads started from
them, handing over the same Hub object rather than a copy. Under
hub_isolation_level = :fiber that left sibling request fibers and worker
threads sharing one hub, and one unsynchronized scope stack, so
concurrent requests and jobs read each other's tags. The `|| clone`
fallback never fired because the inherited value was not nil.

Tag each stored entry with its owning fiber so a context that inherited
one takes its own copy. The copy keeps the live span: Scope#dup
deep-copies it, and spans recorded on a detached transaction copy are
never sent, which would break the Async fan-out this mode exists for.

Reported by @trevorturk in getsentry#3018.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fiber-storage gem polyfills Fiber[] on Ruby < 3.2 and on JRuby, so
:fiber is active there too, but the polyfill does not inherit into
Thread.new. A thread started from a fiber therefore begins from the main
hub and cannot stay on the parent's trace, which the thread half of this
expectation assumed it could.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant