@@ -11,11 +11,47 @@ import { timeFilters } from "~/components/runs/v3/SharedFilters";
1111import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server" ;
1212import { getTaskIdentifiers } from "~/models/task.server" ;
1313import { RunsRepository } from "~/services/runsRepository/runsRepository.server" ;
14+ import { env } from "~/env.server" ;
15+ import {
16+ createCache ,
17+ createLRUMemoryStore ,
18+ DefaultStatefulContext ,
19+ Namespace ,
20+ } from "@internal/cache" ;
21+ import { RedisCacheStore } from "~/services/unkey/redisCacheStore.server" ;
22+ import { singleton } from "~/utils/singleton" ;
1423import { regionForDisplay } from "~/runEngine/concerns/workerQueueSplit.server" ;
1524import { machinePresetFromRun } from "~/v3/machinePresets.server" ;
1625import { ServiceValidationError } from "~/v3/services/baseService.server" ;
1726import { isCancellableRunStatus , isFinalRunStatus , isPendingRunStatus } from "~/v3/taskStatus" ;
1827
28+ // Positive-only cache: only envs known to have runs are stored (empty envs are re-checked),
29+ // so "has runs" is monotonic and the TTL can be very long. Tiered memory + Redis.
30+ const runsExistCache = singleton ( "runsExistCache" , ( ) => {
31+ const ctx = new DefaultStatefulContext ( ) ;
32+ const memory = createLRUMemoryStore ( 5000 , "runs-has-runs-cache" ) ;
33+ const redis = new RedisCacheStore ( {
34+ name : "runs-has-runs" ,
35+ connection : {
36+ keyPrefix : "tr:cache:runs-has-runs" ,
37+ port : env . CACHE_REDIS_PORT ,
38+ host : env . CACHE_REDIS_HOST ,
39+ username : env . CACHE_REDIS_USERNAME ,
40+ password : env . CACHE_REDIS_PASSWORD ,
41+ tlsDisabled : env . CACHE_REDIS_TLS_DISABLED === "true" ,
42+ clusterMode : env . CACHE_REDIS_CLUSTER_MODE_ENABLED === "1" ,
43+ } ,
44+ } ) ;
45+
46+ return createCache ( {
47+ hasRuns : new Namespace < boolean > ( ctx , {
48+ stores : [ memory , redis ] ,
49+ fresh : env . RUN_LIST_HAS_RUNS_CACHE_FRESH_MS ,
50+ stale : env . RUN_LIST_HAS_RUNS_CACHE_STALE_MS ,
51+ } ) ,
52+ } ) ;
53+ } ) ;
54+
1955export type RunListOptions = {
2056 userId ?: string ;
2157 projectId : string ;
@@ -42,6 +78,8 @@ export type RunListOptions = {
4278 direction ?: Direction ;
4379 cursor ?: string ;
4480 pageSize ?: number ;
81+ // Run the empty-state "has any run ever" probe. Only the runs list consumes it.
82+ includeHasAnyRuns ?: boolean ;
4583} ;
4684
4785const DEFAULT_PAGE_SIZE = 25 ;
@@ -65,37 +103,31 @@ export class NextRunListPresenter {
65103 }
66104 ) { }
67105
68- // Existence probe for the empty-state. run-ops (TaskRun) read.
69- // Split off / single-DB: one plain findFirst on `replica` (passthrough).
70- // Split on: true if a row exists in the NEW run-ops DB OR the LEGACY run-ops
71- // read replica only (never the legacy writer). New first to avoid touching
72- // legacy whenever the new DB already answers.
73- async #anyRunExistsInEnv( environmentId : string ) : Promise < boolean > {
74- const splitEnabled = this . readThroughDeps ?. splitEnabled ?? false ;
106+ // Empty-state existence probe, served from ClickHouse (same connection as the runs
107+ // list) so it no longer scans TaskRun in Postgres. SWR-cached to spare ClickHouse;
108+ // RUN_LIST_HAS_RUNS_LOOKBACK_DAYS bounds the prove-absence partition scan.
109+ async #anyRunExistsInEnv(
110+ runsRepository : RunsRepository ,
111+ organizationId : string ,
112+ projectId : string ,
113+ environmentId : string
114+ ) : Promise < boolean > {
115+ const lookbackDays = env . RUN_LIST_HAS_RUNS_LOOKBACK_DAYS ;
116+ const createdAtLowerBoundMs =
117+ lookbackDays > 0 ? Date . now ( ) - lookbackDays * 24 * 60 * 60 * 1000 : undefined ;
75118
76- if ( ! splitEnabled ) {
77- const firstRun = await this . replica . taskRun . findFirst ( {
78- where : { runtimeEnvironmentId : environmentId } ,
79- select : { id : true } ,
119+ const result = await runsExistCache . hasRuns . swr ( environmentId , async ( ) => {
120+ const exists = await runsRepository . runExistsInEnvironment ( {
121+ organizationId,
122+ projectId,
123+ environmentId,
124+ createdAtLowerBoundMs,
80125 } ) ;
81- return firstRun !== null ;
82- }
83-
84- const newClient = this . readThroughDeps ?. newClient ?? this . replica ;
85- const newRun = await newClient . taskRun . findFirst ( {
86- where : { runtimeEnvironmentId : environmentId } ,
87- select : { id : true } ,
126+ // undefined (not false) so swr does NOT cache the empty result — re-check until a run exists.
127+ return exists ? true : undefined ;
88128 } ) ;
89- if ( newRun ) {
90- return true ;
91- }
92129
93- const legacyReplica = this . readThroughDeps ?. legacyReplica ?? this . replica ;
94- const legacyRun = await legacyReplica . taskRun . findFirst ( {
95- where : { runtimeEnvironmentId : environmentId } ,
96- select : { id : true } ,
97- } ) ;
98- return legacyRun !== null ;
130+ return result . val ?? false ;
99131 }
100132
101133 public async call (
@@ -125,6 +157,7 @@ export class NextRunListPresenter {
125157 direction = "forward" ,
126158 cursor,
127159 pageSize = DEFAULT_PAGE_SIZE ,
160+ includeHasAnyRuns = false ,
128161 } : RunListOptions
129162 ) {
130163 //get the time values from the raw values (including a default period)
@@ -252,8 +285,13 @@ export class NextRunListPresenter {
252285
253286 let hasAnyRuns = runs . length > 0 ;
254287
255- if ( ! hasAnyRuns ) {
256- hasAnyRuns = await this . #anyRunExistsInEnv( environmentId ) ;
288+ if ( ! hasAnyRuns && includeHasAnyRuns ) {
289+ hasAnyRuns = await this . #anyRunExistsInEnv(
290+ runsRepository ,
291+ organizationId ,
292+ projectId ,
293+ environmentId
294+ ) ;
257295 }
258296
259297 return {
0 commit comments