diff --git a/apps/server/src/auth/ServerSecretStore.ts b/apps/server/src/auth/ServerSecretStore.ts index 5e9890c1ea28..1af1ff9336d7 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 4571aeef4c6b..e9ecd9e66d16 100644 --- a/apps/server/src/preview/PortScanner.ts +++ b/apps/server/src/preview/PortScanner.ts @@ -39,6 +39,7 @@ import * as Semaphore from "effect/Semaphore"; import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import * as ProcessRunner from "../processRunner.ts"; +import { withoutAmbientParentSpan } from "../serverActivation.ts"; export class PortDiscovery extends Context.Service< PortDiscovery, @@ -547,9 +548,8 @@ export const make = Effect.gen(function* PortDiscoveryMake() { ); }; - const pollTick = Effect.fn("PortDiscovery.pollTick")( + const pollTickActive = Effect.fn("PortDiscovery.pollTick")( function* () { - if ((yield* Ref.get(stateRef)).retainCount <= 0) return; const configuredUrls = [ ...new Set( [...(yield* Ref.get(stateRef)).listeners.values()].flatMap( @@ -578,10 +578,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, @@ -590,7 +595,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; } }); @@ -659,6 +664,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 a4f942a95b5d..f07cfff319c6 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 c068d55e7e76..edabe2f77144 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); });