| ... | ... | @@ -176,6 +176,8 @@ const LIST_STATS_CACHE_TTL = Duration.seconds(60); |
| 176 | 176 | const FILES_VIEWED_CACHE_TTL = Duration.seconds(15); |
| 177 | 177 | /** A diff can stay interactive while its next cached value is fetched off the critical path. */ |
| 178 | 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 | 181 | /** How long one host's signed-in login is believed without asking its CLI again. */ |
| 180 | 182 | const VIEWER_CACHE_TTL = Duration.minutes(10); |
| 181 | 183 | const SEARCH_VISIBILITY_TTL = Duration.minutes(10); |
| ... | ... | @@ -1977,6 +1979,7 @@ export const make = Effect.gen(function* () { |
| 1977 | 1979 | ); |
| 1978 | 1980 | |
| 1979 | 1981 | const context = yield* Effect.context<never>(); |
| 1982 | const scope = yield* Effect.scope; |
| 1980 | 1983 | /** Runs a refresh as its own fiber, for the reads that answer from a held value first. */ |
| 1981 | 1984 | const runFork = Effect.runForkWith(context); |
| 1982 | 1985 | |
| ... | ... | @@ -2697,8 +2700,9 @@ export const make = Effect.gen(function* () { |
| 2697 | 2700 | if (oldest !== undefined) held.delete(oldest); |
| 2698 | 2701 | } |
| 2699 | 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 | 2706 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); |
| 2703 | 2707 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { |
| 2704 | 2708 | const snapshot = held.get(key); |
| ... | ... | @@ -2709,6 +2713,14 @@ export const make = Effect.gen(function* () { |
| 2709 | 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 | 2726 | const makeLastGoodRead = <A>(capacity: number) => { |
| ... | ... | @@ -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 | 3317 | const diff: PullRequestService["Service"]["diff"] = (input) => { |
| 3276 | 3318 | const key = JSON.stringify([ |
| 3277 | 3319 | refCacheKey(input), |
| ... | ... | @@ -3294,8 +3336,10 @@ export const make = Effect.gen(function* () { |
| 3294 | 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 | 3345 | const filesViewedCache = yield* Cache.makeWith( |