Repository navigation
perf(server): connects no longer wait on editor and SSH target discovery #14519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+422
−78
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
dc3bd6a
perf(server): serve connect-time discovery from cache instead of probing
github-actions[bot] ed5831a
chore: resolve merge conflicts with main
saphid 55e60d7
merge: preserve PR 14519 behavior on current main
saphid 42c4fdd
refactor(server): trace discovery cache settlement
saphid 4d73cc9
fix: integrate current main while preserving PR 14519 behavior
saphid 4306be0
fix: integrate current main while preserving PR 14519 behavior
saphid e73e7aa
test(server): retain parallel open discovery regression
saphid 8bdc7e7
merge: keep current discovery spans and shared cache ownership
saphid 4290ede
test(server): await discovery milestones instead of advancing schedul…
saphid aa741e1
test(server): await refresh fiber settlement and use current Effect APIs
saphid 5fa477d
test(server): capture completed discovery fiber in timeout case
saphid 70cafeb
test(server): flatten timeout discovery pipeline
saphid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
apps/server/src/environment/staleWhileRevalidate.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import { assert, it } from "@effect/vitest"; | ||
| import * as Deferred from "effect/Deferred"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Exit from "effect/Exit"; | ||
| import * as Fiber from "effect/Fiber"; | ||
| import * as Ref from "effect/Ref"; | ||
| import * as Scope from "effect/Scope"; | ||
| import * as TestClock from "effect/testing/TestClock"; | ||
|
|
||
| import { makeStaleWhileRevalidate } from "./staleWhileRevalidate.ts"; | ||
|
|
||
| it.effect("computes once, then answers from memory until the ttl lapses", () => | ||
| Effect.gen(function* () { | ||
| const calls = yield* Ref.make(0); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Ref.updateAndGet(calls, (count) => count + 1), | ||
| "60 seconds", | ||
| ); | ||
|
|
||
| assert.equal(yield* read, 1); | ||
| yield* TestClock.adjust("59 seconds"); | ||
| assert.equal(yield* read, 1); | ||
| assert.equal(yield* Ref.get(calls), 1); | ||
| }).pipe(Effect.scoped), | ||
| ); | ||
|
|
||
| it.effect("serves the stale value while one background refresh replaces it", () => | ||
| Effect.gen(function* () { | ||
| const calls = yield* Ref.make(0); | ||
| const release = yield* Deferred.make<void>(); | ||
| const started = yield* Deferred.make<void>(); | ||
| const refreshed = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>(); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Effect.gen(function* () { | ||
| const call = yield* Ref.updateAndGet(calls, (count) => count + 1); | ||
| if (call > 1) { | ||
| yield* Deferred.succeed(started, undefined); | ||
| yield* Deferred.await(release); | ||
| yield* Deferred.succeed(refreshed, yield* Effect.fiber); | ||
| } | ||
| return call; | ||
| }), | ||
| "60 seconds", | ||
| ); | ||
|
|
||
| assert.equal(yield* read, 1); | ||
| yield* TestClock.adjust("61 seconds"); | ||
| // The refresh is blocked, yet callers are answered immediately and only | ||
| // one refresh starts however many callers arrive. | ||
| assert.equal(yield* read, 1); | ||
| assert.equal(yield* read, 1); | ||
| yield* Deferred.await(started); | ||
| assert.equal(yield* Ref.get(calls), 2); | ||
|
|
||
| yield* Deferred.succeed(release, undefined); | ||
| yield* Fiber.await(yield* Deferred.await(refreshed)); | ||
| assert.equal(yield* read, 2); | ||
| }), | ||
| ); | ||
|
|
||
| it.effect("shares one first scan that an interrupted caller does not cancel", () => | ||
| Effect.gen(function* () { | ||
| const calls = yield* Ref.make(0); | ||
| const release = yield* Deferred.make<void>(); | ||
| const started = yield* Deferred.make<void>(); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Ref.updateAndGet(calls, (count) => count + 1).pipe( | ||
| Effect.tap(() => Deferred.succeed(started, undefined)), | ||
| Effect.tap(() => Deferred.await(release)), | ||
| ), | ||
| "60 seconds", | ||
| ); | ||
|
|
||
| // A connect that times out or disconnects mid-scan leaves the scan running. | ||
| const interrupted = yield* read.pipe(Effect.forkChild); | ||
| yield* Deferred.await(started); | ||
| yield* Fiber.interrupt(interrupted); | ||
|
|
||
| const next = yield* read.pipe(Effect.forkChild); | ||
| yield* Deferred.succeed(release, undefined); | ||
| assert.equal(yield* Fiber.join(next), 1); | ||
| assert.equal(yield* Ref.get(calls), 1); | ||
| }).pipe(Effect.scoped), | ||
| ); | ||
|
|
||
| it.effect("starts over after a failed first scan", () => | ||
| Effect.gen(function* () { | ||
| const calls = yield* Ref.make(0); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Ref.updateAndGet(calls, (count) => count + 1).pipe( | ||
| Effect.filterOrElse( | ||
| (call) => call !== 1, | ||
| () => Effect.die("probe crashed"), | ||
| ), | ||
| ), | ||
| "60 seconds", | ||
| ); | ||
|
|
||
| assert.isTrue(Exit.isFailure(yield* Effect.exit(read))); | ||
| assert.equal(yield* read, 2); | ||
| }).pipe(Effect.scoped), | ||
| ); | ||
|
|
||
| it.effect("abandons a hung refresh so a later call can start another", () => | ||
| Effect.gen(function* () { | ||
| const calls = yield* Ref.make(0); | ||
| const started = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>(); | ||
| const interrupted = yield* Deferred.make<void>(); | ||
| const refreshed = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>(); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Ref.updateAndGet(calls, (count) => count + 1).pipe( | ||
| Effect.flatMap((call) => | ||
| call === 2 | ||
| ? Effect.fiber.pipe( | ||
| Effect.flatMap((fiber) => Deferred.succeed(started, fiber)), | ||
| Effect.andThen(Effect.never), | ||
| Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)), | ||
| ) | ||
| : Effect.succeed(call).pipe( | ||
| Effect.tap(() => | ||
| call === 3 | ||
| ? Effect.fiber.pipe( | ||
| Effect.flatMap((fiber) => Deferred.succeed(refreshed, fiber)), | ||
| ) | ||
| : Effect.void, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| "60 seconds", | ||
| ); | ||
|
|
||
| assert.equal(yield* read, 1); | ||
| yield* TestClock.adjust("61 seconds"); | ||
| assert.equal(yield* read, 1); | ||
| yield* Deferred.await(started); | ||
| assert.equal(yield* Ref.get(calls), 2); | ||
|
|
||
| // The hung refresh times out, releasing the claim for the next caller. | ||
| yield* TestClock.adjust("30 seconds"); | ||
| yield* Deferred.await(interrupted); | ||
| yield* Fiber.await(yield* Deferred.await(started)); | ||
| assert.equal(yield* read, 1); | ||
| yield* Fiber.await(yield* Deferred.await(refreshed)); | ||
| assert.equal(yield* Ref.get(calls), 3); | ||
| assert.equal(yield* read, 3); | ||
| }).pipe(Effect.scoped), | ||
| ); | ||
|
|
||
| it.effect("interrupts a background refresh when the owning scope closes", () => | ||
| Effect.gen(function* () { | ||
| const interrupted = yield* Deferred.make<void>(); | ||
| const started = yield* Deferred.make<void>(); | ||
| const scope = yield* Scope.make(); | ||
| const calls = yield* Ref.make(0); | ||
| const read = yield* makeStaleWhileRevalidate( | ||
| Ref.updateAndGet(calls, (count) => count + 1).pipe( | ||
| Effect.filterOrElse( | ||
| (call) => call === 1, | ||
| () => | ||
| Deferred.succeed(started, undefined).pipe( | ||
| Effect.andThen(Effect.never), | ||
| Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)), | ||
| ), | ||
| ), | ||
| ), | ||
| "60 seconds", | ||
| ).pipe(Scope.provide(scope)); | ||
|
|
||
| yield* read; | ||
| yield* TestClock.adjust("61 seconds"); | ||
| yield* read; | ||
| yield* Deferred.await(started); | ||
| yield* Scope.close(scope, Exit.void); | ||
| yield* Deferred.await(interrupted); | ||
| }), | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import * as Clock from "effect/Clock"; | ||
| import * as Deferred from "effect/Deferred"; | ||
| import * as Duration from "effect/Duration"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Exit from "effect/Exit"; | ||
| import * as Ref from "effect/Ref"; | ||
|
|
||
| const REFRESH_TIMEOUT = Duration.seconds(30); | ||
|
|
||
| type Entry<A> = | ||
| | { readonly _tag: "Empty" } | ||
| | { readonly _tag: "Scanning"; readonly scan: Deferred.Deferred<A> } | ||
| | { | ||
| readonly _tag: "Ready"; | ||
| readonly value: A; | ||
| readonly expiresAtNanos: bigint; | ||
| readonly refreshing: boolean; | ||
| }; | ||
|
|
||
| // Shared scans survive caller cancellation; monotonic TTL bounds cache age. | ||
| // Warm reads return stale data while a scope-owned, bounded refresh runs. | ||
| export const makeStaleWhileRevalidate = Effect.fn("StaleWhileRevalidate.make")(function* <A>( | ||
| discover: Effect.Effect<A>, | ||
| ttl: Duration.Input, | ||
| ) { | ||
| const ttlNanos = Duration.toNanosUnsafe(Duration.fromInputUnsafe(ttl)); | ||
| const scope = yield* Effect.scope; | ||
| const state = yield* Ref.make<Entry<A>>({ _tag: "Empty" }); | ||
|
|
||
| const settle = Effect.fn("StaleWhileRevalidate.settle")(function* (exit: Exit.Exit<A>) { | ||
| const now = yield* Clock.monotonicTimeNanos; | ||
| yield* Ref.update(state, (entry): Entry<A> => | ||
| Exit.isSuccess(exit) | ||
| ? { | ||
| _tag: "Ready", | ||
| value: exit.value, | ||
| expiresAtNanos: now + ttlNanos, | ||
| refreshing: false, | ||
| } | ||
| : entry._tag === "Ready" | ||
| ? { ...entry, refreshing: false } | ||
| : { _tag: "Empty" }, | ||
| ); | ||
| }); | ||
| const fork = (scan: Effect.Effect<unknown>) => | ||
| scan.pipe(Effect.ignoreCause({ log: true }), Effect.interruptible, Effect.forkIn(scope)); | ||
| const firstScan = (scan: Deferred.Deferred<A>) => | ||
| fork( | ||
| discover.pipe( | ||
| Effect.onExit((exit) => settle(exit).pipe(Effect.andThen(Deferred.done(scan, exit)))), | ||
| ), | ||
| ); | ||
| const refresh = fork(discover.pipe(Effect.onExit(settle), Effect.timeoutOption(REFRESH_TIMEOUT))); | ||
|
|
||
| return Effect.gen(function* () { | ||
| const now = yield* Clock.monotonicTimeNanos; | ||
| // Claiming a scan and starting it are one uninterruptible step: a caller | ||
| // interrupted between them would leave a claimed scan that never runs. | ||
| const result = yield* Ref.modify( | ||
| state, | ||
| (entry): readonly [readonly [Effect.Effect<unknown>, Effect.Effect<A>], Entry<A>] => { | ||
| switch (entry._tag) { | ||
| case "Empty": { | ||
| const scan = Deferred.makeUnsafe<A>(); | ||
| return [[firstScan(scan), Deferred.await(scan)], { _tag: "Scanning", scan }]; | ||
| } | ||
| case "Scanning": | ||
| return [[Effect.void, Deferred.await(entry.scan)], entry]; | ||
| case "Ready": | ||
| return entry.refreshing || entry.expiresAtNanos > now | ||
| ? [[Effect.void, Effect.succeed(entry.value)], entry] | ||
| : [[refresh, Effect.succeed(entry.value)], { ...entry, refreshing: true }]; | ||
| } | ||
| }, | ||
| ).pipe( | ||
| Effect.flatMap(([start, result]) => Effect.as(start, result)), | ||
| Effect.uninterruptible, | ||
| ); | ||
| return yield* result; | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 10494
Script executed:
Repository: pingdotgg/t3code
Length of output: 41855
Script executed:
Repository: pingdotgg/t3code
Length of output: 1341
Script executed:
Repository: pingdotgg/t3code
Length of output: 21480
Script executed:
Repository: pingdotgg/t3code
Length of output: 14432
Script executed:
Repository: pingdotgg/t3code
Length of output: 14752
Bound the initial editor discovery scan and clear it after timeout.
firstScanhas no timeout, and everyScanningcaller awaits the sameDeferred. Editor discovery reacheswithPathDirectoryListings, whoseFileSystem.statcalls have no timeout. The existing interruption test demonstrates that a pending filesystem lookup survives the first caller’s interruption and is shared by the next caller.The config timeout only returns its fallback.
withLateEditorConfigwaits on the cached discovery without that timeout, so it can remain blocked.On first-scan timeout, reset the cache to
Emptyand complete current waiters with the cache-specific fallback ([]orundefined). Do not store that fallback asReady; the next caller must be able to retry. The remote-target path has bounded listener and Tailscale probes, so this finding should not claim that remote-target discovery can hang indefinitely.