Skip to content

Decorate fork bodies with Scope.FORK_DECORATOR - #394

Open
pawelsadlo wants to merge 3 commits into
softwaremill:mainfrom
pawelsadlo:fork-decorator
Open

pawelsadlo wants to merge 3 commits into
softwaremill:mainfrom
pawelsadlo:fork-decorator

Conversation

@pawelsadlo

@pawelsadlo pawelsadlo commented Nov 3, 2026 •

Copy link
Copy Markdown

Needed for #395 as a tool to propagate opentelemetry context to forks

This is OpenTelemetry agnostic approach that enables propagation of open telemetry context.

Scope gets a new ScopeLocal field "Fork_decorator" which can be used to decorate fork creation logic.

decoration is invoked when fork() method is called (not when forked thread runs it)

Why its needed for OpenTelemetry context propagation:

Threads are created inside supervised block by thread which runs supervised, not by the one which calls fork.

Therefore propagation approach used in ox, via (virtual) thread factory doesn't work, because all threads (and their threadLocals) are children of one thread, even though they are syntactically nested.

#395 introduces

public enum PropagatingForkDecorator implements Decorator {
    INSTANCE;

    @Override
    public <T> Callable<T> decorate(Callable<T> f) {
        // captured on the thread which starts the fork, where the context is still current
        Context forkingContext = Context.current();
        return () -> {
            try (Scope ignored = forkingContext.makeCurrent()) {
                return f.call();
            }
        };
    }
}

which can be bound to scopelocal value of Fork_decorator to propagate open telemetry context properly.

ScopeLocal storage of Decorator enables this feature with minimal api change

Pawel Sadlo and others added 3 commits November 3, 2026 14:06
Forks are started by the thread which runs `supervised`, not by the one which
calls `fork`: `fork` sends a command down the supervisor channel, and the
thread is created later, by the scope's loop. So values which are propagated
when a thread is created - `ScopedValue`s, or a context held in a thread local
- are inherited from the thread which opened the scope, and not from the one
which started the fork.

A `ScopedValue` bound inside a scope body is therefore invisible to the forks
started under it:

    supervised(scope -> ScopedValue.where(V, "x").call(() ->
        scope.fork(() -> V.isBound()).join()));   // false

`Scope.FORK_DECORATOR` takes a `ForkDecorator`, applied to the fork's body on
the thread which calls `fork`, where that thread's state is still available.
The decorator can capture it, and restore it around the body.

It is generic - it only maps a `Callable` to a `Callable` - so it can carry a
`ScopedValue`, a thread-local context, an MDC, or anything else inherited at
thread creation. Applied in `fork`, `forkUser`, `forkUnsupervised` and
`forkCancellable`, so flow operators which fork internally are covered too.

When the scoped value is unbound, `fork` is unchanged.

Co-authored-by: Claude-Opus-4.8 via Claude-Code <noreply@anthropic.com>
Co-authored-by: Claude-Opus-4.8 via Claude-Code <noreply@anthropic.com>
Co-authored-by: Claude-Opus-4.8 via Claude-Code <noreply@anthropic.com>

This branch has not been deployed

No deployments
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