| ... | @@ -176,6 +176,8 @@ const LIST_STATS_CACHE_TTL = Duration.seconds(60); | ... | @@ -176,6 +176,8 @@ const LIST_STATS_CACHE_TTL = Duration.seconds(60); |
| 176 | const FILES_VIEWED_CACHE_TTL = Duration.seconds(15); | 176 | const FILES_VIEWED_CACHE_TTL = Duration.seconds(15); |
| 177 | /** A diff can stay interactive while its next cached value is fetched off the critical path. */ | 177 | /** A diff can stay interactive while its next cached value is fetched off the critical path. */ |
| 178 | const DIFF_STALE_WINDOW = Duration.minutes(10); | 178 | const DIFF_STALE_WINDOW = Duration.minutes(10); |
| | 179 | /** How long an expired diff may stay held before it is dropped unread. */ |
| | 180 | const DIFF_SWEEP_INTERVAL = Duration.minutes(1); |
| 179 | /** How long one host's signed-in login is believed without asking its CLI again. */ | 181 | /** How long one host's signed-in login is believed without asking its CLI again. */ |
| 180 | const VIEWER_CACHE_TTL = Duration.minutes(10); | 182 | const VIEWER_CACHE_TTL = Duration.minutes(10); |
| 181 | const SEARCH_VISIBILITY_TTL = Duration.minutes(10); | 183 | const SEARCH_VISIBILITY_TTL = Duration.minutes(10); |
| ... | @@ -1977,6 +1979,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1977,6 +1979,7 @@ export const make = Effect.gen(function* () { |
| 1977 | ); | 1979 | ); |
| 1978 | | 1980 | |
| 1979 | const context = yield* Effect.context<never>(); | 1981 | const context = yield* Effect.context<never>(); |
| | 1982 | const scope = yield* Effect.scope; |
| 1980 | /** Runs a refresh as its own fiber, for the reads that answer from a held value first. */ | 1983 | /** Runs a refresh as its own fiber, for the reads that answer from a held value first. */ |
| 1981 | const runFork = Effect.runForkWith(context); | 1984 | const runFork = Effect.runForkWith(context); |
| 1982 | | 1985 | |
| ... | @@ -2697,8 +2700,9 @@ export const make = Effect.gen(function* () { | ... | @@ -2697,8 +2700,9 @@ export const make = Effect.gen(function* () { |
| 2697 | if (oldest !== undefined) held.delete(oldest); | 2700 | if (oldest !== undefined) held.delete(oldest); |
| 2698 | } | 2701 | } |
| 2699 | held.set(key, { at, value }); | 2702 | held.set(key, { at, value }); |
| | 2703 | armDiffSweep(); |
| 2700 | }); | 2704 | }); |
| 2701 | return <E>(key: string, read: Effect.Effect<PullRequestDiffResult, E>) => { | 2705 | const serve = <E>(key: string, read: Effect.Effect<PullRequestDiffResult, E>) => { |
| 2702 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); | 2706 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); |
| 2703 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { | 2707 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { |
| 2704 | const snapshot = held.get(key); | 2708 | const snapshot = held.get(key); |
| ... | @@ -2709,6 +2713,14 @@ export const make = Effect.gen(function* () { | ... | @@ -2709,6 +2713,14 @@ export const make = Effect.gen(function* () { |
| 2709 | return revalidate(recorded).pipe(Effect.as(snapshot.value)); | 2713 | return revalidate(recorded).pipe(Effect.as(snapshot.value)); |
| 2710 | }); | 2714 | }); |
| 2711 | }; | 2715 | }; |
| | 2716 | /** Drops what `serve` would already refuse, and says how many values are still held. */ |
| | 2717 | const dropExpired = Effect.map(Clock.currentTimeMillis, (now) => { |
| | 2718 | for (const [key, snapshot] of held) { |
| | 2719 | if (now - snapshot.at > staleMs) held.delete(key); |
| | 2720 | } |
| | 2721 | return held.size; |
| | 2722 | }); |
| | 2723 | return { serve, dropExpired }; |
| 2712 | })(); | 2724 | })(); |
| 2713 | | 2725 | |
| 2714 | const makeLastGoodRead = <A>(capacity: number) => { | 2726 | const makeLastGoodRead = <A>(capacity: number) => { |
| ... | @@ -3272,6 +3284,36 @@ export const make = Effect.gen(function* () { | ... | @@ -3272,6 +3284,36 @@ export const make = Effect.gen(function* () { |
| 3272 | }, | 3284 | }, |
| 3273 | }, | 3285 | }, |
| 3274 | ); | 3286 | ); |
| | 3287 | /** |
| | 3288 | * Both diff holders drop an expired value only when its own key is read again, and a diff's key |
| | 3289 | * moves with every push, so a closed page's patches would stay held until the server restarted. |
| | 3290 | * One pending sweep at a time, armed in the same step that leaves a value held and re-armed |
| | 3291 | * while anything still is, so an idle server keeps no timer. |
| | 3292 | */ |
| | 3293 | let diffSweepPending = false; |
| | 3294 | // Forked from the service's own context, in one step with the flag: a caller's interruption |
| | 3295 | // cannot strand the flag, and its request is not kept alive by the wait. |
| | 3296 | const armDiffSweep = () => { |
| | 3297 | if (diffSweepPending) return; |
| | 3298 | diffSweepPending = true; |
| | 3299 | runFork(Effect.forkIn(sweepDiffs, scope)); |
| | 3300 | }; |
| | 3301 | const sweepDiffs: Effect.Effect<void> = Effect.sleep(DIFF_SWEEP_INTERVAL).pipe( |
| | 3302 | Effect.andThen( |
| | 3303 | Effect.suspend(() => { |
| | 3304 | // Cleared before the sweep, so a read that settles during it arms the next one itself. |
| | 3305 | diffSweepPending = false; |
| | 3306 | return staleDiff.dropExpired; |
| | 3307 | }), |
| | 3308 | ), |
| | 3309 | // The cache drops its expired entries as its keys are walked. |
| | 3310 | Effect.flatMap((stale) => |
| | 3311 | Effect.map(Cache.keys(diffCache), (keys) => stale + Array.from(keys).length), |
| | 3312 | ), |
| | 3313 | Effect.map((held) => { |
| | 3314 | if (held > 0) armDiffSweep(); |
| | 3315 | }), |
| | 3316 | ); |
| 3275 | const diff: PullRequestService["Service"]["diff"] = (input) => { | 3317 | const diff: PullRequestService["Service"]["diff"] = (input) => { |
| 3276 | const key = JSON.stringify([ | 3318 | const key = JSON.stringify([ |
| 3277 | refCacheKey(input), | 3319 | refCacheKey(input), |
| ... | @@ -3294,8 +3336,10 @@ export const make = Effect.gen(function* () { | ... | @@ -3294,8 +3336,10 @@ export const make = Effect.gen(function* () { |
| 3294 | Effect.uninterruptible, | 3336 | Effect.uninterruptible, |
| 3295 | ), | 3337 | ), |
| 3296 | ), | 3338 | ), |
| | 3339 | // However the read ends, it may have left a value behind in the cache. |
| | 3340 | Effect.ensuring(Effect.sync(armDiffSweep)), |
| 3297 | ); | 3341 | ); |
| 3298 | return staleDiff(key, read); | 3342 | return staleDiff.serve(key, read); |
| 3299 | }; | 3343 | }; |
| 3300 | | 3344 | |
| 3301 | const filesViewedCache = yield* Cache.makeWith( | 3345 | const filesViewedCache = yield* Cache.makeWith( |