diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index 5e9890c1ea2..1af1ff9336d 100644 --- a/apps/server/src/auth/ServerSecretStore.ts +++ b/apps/server/src/auth/ServerSecretStore.ts @@ -181,7 +181,7 @@ export const make = Effect.gen(function* () { }), ), ), - Effect.withSpan("ServerSecretStore.get"), + Effect.withTracerEnabled(false), ); const set: ServerSecretStore["Service"]["set"] = (name, value) => { diff --git a/apps/server/src/preview/PortScanner.ts b/apps/server/src/preview/PortScanner.ts index c306fca2b33..0ff053e9171 100644 --- a/apps/server/src/preview/PortScanner.ts +++ b/apps/server/src/preview/PortScanner.ts @@ -25,6 +25,7 @@ import * as Schedule from "effect/Schedule"; import * as Scope from "effect/Scope"; import * as ProcessRunner from "../processRunner.ts"; +import { withoutAmbientParentSpan } from "../serverActivation.ts"; export class PortDiscovery extends Context.Service< PortDiscovery, @@ -292,9 +293,8 @@ export const make = Effect.gen(function* PortDiscoveryMake() { yield* Effect.forEach(listeners, (listener) => listener(servers), { discard: true }); }); - const pollTick = Effect.fn("PortDiscovery.pollTick")( + const pollTickActive = Effect.fn("PortDiscovery.pollTick")( function* () { - if ((yield* Ref.get(stateRef)).retainCount <= 0) return; const next = yield* scanOnce(); const changed = yield* Ref.modify(stateRef, (state) => serversEqual(state.lastSnapshot, next) @@ -308,10 +308,15 @@ export const make = Effect.gen(function* PortDiscoveryMake() { ), ); - // Single layer-scoped polling fiber. Ticks are no-ops when no client is - // currently retained, so the cost is one Ref.get every POLL_INTERVAL. - yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL)))); + // Idle early-return stays outside Effect.fn so no-op ticks create no span; detach ParentSpan (#5410). + const pollTick = Effect.gen(function* () { + if ((yield* Ref.get(stateRef)).retainCount <= 0) return; + yield* pollTickActive(); + }); + yield* Effect.forkScoped( + withoutAmbientParentSpan(pollTick.pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL)))), + ); const acquireRetention = Effect.fn("PortDiscovery.retain")(function* () { const wasIdle = yield* Ref.modify(stateRef, (state) => [ state.retainCount === 0, @@ -320,7 +325,7 @@ export const make = Effect.gen(function* PortDiscoveryMake() { if (wasIdle) { // Run an immediate scan + broadcast so the new retainer doesn't have // to wait up to POLL_INTERVAL for the first emission. - yield* pollTick(); + yield* pollTick; } }); @@ -385,6 +390,6 @@ export const make = Effect.gen(function* PortDiscoveryMake() { registerTerminalProcesses, unregisterTerminal, }); -}).pipe(Effect.withSpan("PortDiscovery.make")); +}); export const layer = Layer.effect(PortDiscovery, make); diff --git a/apps/server/src/serverActivation.test.ts b/apps/server/src/serverActivation.test.ts index a4f942a95b5..f07cfff319c 100644 --- a/apps/server/src/serverActivation.test.ts +++ b/apps/server/src/serverActivation.test.ts @@ -1,8 +1,11 @@ import { expect, it } from "@effect/vitest"; +import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Tracer from "effect/Tracer"; -import { forkParked, ServerActivation } from "./serverActivation.ts"; +import { forkParked, ServerActivation, withoutAmbientParentSpan } from "./serverActivation.ts"; it.effect("proves a root is parked before returning and releases it with one gate", () => Effect.scoped( @@ -21,3 +24,30 @@ it.effect("proves a root is parked before returning and releases it with one gat }), ), ); + +it.effect("withoutAmbientParentSpan drops inherited ParentSpan for long-lived roots", () => + Effect.gen(function* () { + const ambient = Tracer.externalSpan({ + traceId: "00000000000000000000000000000001", + spanId: "0000000000000001", + sampled: true, + }); + + const sawParent = yield* Effect.serviceOption(Tracer.ParentSpan).pipe( + Effect.map(Option.isSome), + withoutAmbientParentSpan, + Effect.provideService(Tracer.ParentSpan, ambient), + ); + + expect(sawParent).toBe(false); + + const stillHasParent = yield* Effect.serviceOption(Tracer.ParentSpan).pipe( + Effect.map(Option.isSome), + Effect.provideService(Tracer.ParentSpan, ambient), + ); + expect(stillHasParent).toBe(true); + + const stripped = Context.omit(Tracer.ParentSpan)(Context.make(Tracer.ParentSpan, ambient)); + expect(Option.isNone(Context.getOption(stripped, Tracer.ParentSpan))).toBe(true); + }), +); diff --git a/apps/server/src/serverActivation.ts b/apps/server/src/serverActivation.ts index c068d55e7e7..edabe2f7714 100644 --- a/apps/server/src/serverActivation.ts +++ b/apps/server/src/serverActivation.ts @@ -2,25 +2,42 @@ import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import type * as Scope from "effect/Scope"; +import * as Tracer from "effect/Tracer"; export class ServerActivation extends Context.Reference | undefined>( "t3/serverActivation", { defaultValue: () => undefined }, ) {} +// Clear inherited ParentSpan so long-lived forks don't pin short-lived startup spans (#5410). +export const withoutAmbientParentSpan = ( + effect: Effect.Effect, +): Effect.Effect> => + Effect.updateContext( + effect, + (context) => + Context.omit(Tracer.ParentSpan)(context) as Context.Context< + Exclude, Tracer.ParentSpan> + >, + ); + /** Forks a long-running root before commit and proves it is parked at the activation boundary. */ export const forkParked = ( effect: Effect.Effect, ): Effect.Effect => Effect.gen(function* () { const activation = yield* ServerActivation; + const detached = withoutAmbientParentSpan(effect); if (activation === undefined) { - yield* Effect.forkScoped(effect); + yield* Effect.forkScoped(detached); return; } const parked = yield* Deferred.make(); yield* Effect.forkScoped( - Deferred.succeed(parked, undefined).pipe(Effect.andThen(activation), Effect.andThen(effect)), + Deferred.succeed(parked, undefined).pipe( + Effect.andThen(activation), + Effect.andThen(detached), + ), ); yield* Deferred.await(parked); });