@@ -30,6 +30,7 @@ use sync_types::CanonicalizedModulePath;
30
30
use value:: {
31
31
heap_size:: WithHeapSize ,
32
32
ConvexValue ,
33
+ DeveloperDocumentId ,
33
34
FieldPath ,
34
35
ResolvedDocumentId ,
35
36
TableName ,
@@ -46,6 +47,7 @@ use crate::{
46
47
CronJobLogLines ,
47
48
CronJobState ,
48
49
CronJobStatus ,
50
+ CronNextRun ,
49
51
CronSpec ,
50
52
} ,
51
53
} ,
@@ -85,6 +87,21 @@ pub static CRON_JOB_LOGS_NAME_FIELD: LazyLock<FieldPath> =
85
87
static CRON_JOB_LOGS_TS_FIELD : LazyLock < FieldPath > =
86
88
LazyLock :: new ( || "ts" . parse ( ) . expect ( "invalid ts field" ) ) ;
87
89
90
+ pub static CRON_NEXT_RUN_TABLE : LazyLock < TableName > = LazyLock :: new ( || {
91
+ "_cron_next_run"
92
+ . parse ( )
93
+ . expect ( "_cron_next_run is not a valid system table name" )
94
+ } ) ;
95
+
96
+ pub static CRON_NEXT_RUN_INDEX_BY_NEXT_TS : LazyLock < IndexName > =
97
+ LazyLock :: new ( || system_index ( & CRON_NEXT_RUN_TABLE , "by_next_ts" ) ) ;
98
+ pub static CRON_NEXT_RUN_INDEX_BY_CRON_JOB_ID : LazyLock < IndexName > =
99
+ LazyLock :: new ( || system_index ( & CRON_NEXT_RUN_TABLE , "by_cron_job_id" ) ) ;
100
+ static CRON_NEXT_RUN_NEXT_TS_FIELD : LazyLock < FieldPath > =
101
+ LazyLock :: new ( || "nextTs" . parse ( ) . expect ( "invalid nextTs field" ) ) ;
102
+ static CRON_NEXT_RUN_CRON_JOB_ID_FIELD : LazyLock < FieldPath > =
103
+ LazyLock :: new ( || "cronJobId" . parse ( ) . expect ( "invalid cronJobId field" ) ) ;
104
+
88
105
pub struct CronJobsTable ;
89
106
impl SystemTable for CronJobsTable {
90
107
fn table_name ( & self ) -> & ' static TableName {
@@ -134,6 +151,34 @@ impl SystemTable for CronJobLogsTable {
134
151
}
135
152
}
136
153
154
+ pub struct CronNextRunTable ;
155
+ impl SystemTable for CronNextRunTable {
156
+ fn table_name ( & self ) -> & ' static TableName {
157
+ & CRON_NEXT_RUN_TABLE
158
+ }
159
+
160
+ fn indexes ( & self ) -> Vec < SystemIndex > {
161
+ vec ! [
162
+ SystemIndex {
163
+ name: CRON_NEXT_RUN_INDEX_BY_NEXT_TS . clone( ) ,
164
+ fields: vec![ CRON_NEXT_RUN_NEXT_TS_FIELD . clone( ) ]
165
+ . try_into( )
166
+ . unwrap( ) ,
167
+ } ,
168
+ SystemIndex {
169
+ name: CRON_NEXT_RUN_INDEX_BY_CRON_JOB_ID . clone( ) ,
170
+ fields: vec![ CRON_NEXT_RUN_CRON_JOB_ID_FIELD . clone( ) ]
171
+ . try_into( )
172
+ . unwrap( ) ,
173
+ } ,
174
+ ]
175
+ }
176
+
177
+ fn validate_document ( & self , document : ResolvedDocument ) -> anyhow:: Result < ( ) > {
178
+ ParseDocument :: < CronNextRun > :: parse ( document) . map ( |_| ( ) )
179
+ }
180
+ }
181
+
137
182
const MAX_LOGS_PER_CRON : usize = 5 ;
138
183
139
184
pub struct CronModel < ' a , RT : Runtime > {
@@ -199,19 +244,51 @@ impl<'a, RT: Runtime> CronModel<'a, RT> {
199
244
cron_spec : CronSpec ,
200
245
) -> anyhow:: Result < ( ) > {
201
246
let now = self . runtime ( ) . generate_timestamp ( ) ?;
247
+ let next_ts = compute_next_ts ( & cron_spec, None , now) ?;
202
248
let cron = CronJob {
203
249
name,
204
- next_ts : compute_next_ts ( & cron_spec , None , now ) ? ,
250
+ next_ts,
205
251
cron_spec,
206
252
state : CronJobState :: Pending ,
207
253
prev_ts : None ,
208
254
} ;
209
- SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
255
+
256
+ let cron_job_id = SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
210
257
. insert ( & CRON_JOBS_TABLE , cron. try_into ( ) ?)
258
+ . await ?
259
+ . developer_id ;
260
+
261
+ let next_run = CronNextRun {
262
+ cron_job_id,
263
+ state : CronJobState :: Pending ,
264
+ prev_ts : None ,
265
+ next_ts,
266
+ } ;
267
+
268
+ SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
269
+ . insert ( & CRON_NEXT_RUN_TABLE , next_run. try_into ( ) ?)
211
270
. await ?;
271
+
212
272
Ok ( ( ) )
213
273
}
214
274
275
+ async fn next_run (
276
+ & mut self ,
277
+ cron_job_id : DeveloperDocumentId ,
278
+ ) -> anyhow:: Result < Option < ParsedDocument < CronNextRun > > > {
279
+ let query = Query :: index_range ( IndexRange {
280
+ index_name : CRON_NEXT_RUN_INDEX_BY_CRON_JOB_ID . clone ( ) ,
281
+ range : vec ! [ IndexRangeExpression :: Eq (
282
+ CRON_NEXT_RUN_CRON_JOB_ID_FIELD . clone( ) ,
283
+ ConvexValue :: from( cron_job_id) . into( ) ,
284
+ ) ] ,
285
+ order : Order :: Asc ,
286
+ } ) ;
287
+ let mut query_stream = ResolvedQuery :: new ( self . tx , self . component . into ( ) , query) ?;
288
+ let next_run = query_stream. expect_at_most_one ( self . tx ) . await ?;
289
+ next_run. map ( |v| v. parse ( ) ) . transpose ( )
290
+ }
291
+
215
292
pub async fn update (
216
293
& mut self ,
217
294
cron_job : ParsedDocument < CronJob > ,
@@ -228,9 +305,16 @@ impl<'a, RT: Runtime> CronModel<'a, RT> {
228
305
}
229
306
230
307
pub async fn delete ( & mut self , cron_job : ParsedDocument < CronJob > ) -> anyhow:: Result < ( ) > {
308
+ let id = cron_job. id ( ) ;
231
309
SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
232
- . delete ( cron_job . clone ( ) . id ( ) )
310
+ . delete ( id )
233
311
. await ?;
312
+ let next_run = self . next_run ( id. developer_id ) . await ?;
313
+ if let Some ( next_run) = next_run {
314
+ SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
315
+ . delete ( next_run. id ( ) )
316
+ . await ?;
317
+ }
234
318
self . apply_job_log_retention ( cron_job. name . clone ( ) , 0 )
235
319
. await ?;
236
320
Ok ( ( ) )
@@ -247,8 +331,25 @@ impl<'a, RT: Runtime> CronModel<'a, RT> {
247
331
. namespace( self . component. into( ) )
248
332
. tablet_matches_name( id. tablet_id, & CRON_JOBS_TABLE ) ) ;
249
333
SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
250
- . replace ( id, job. try_into ( ) ?)
334
+ . replace ( id, job. clone ( ) . try_into ( ) ?)
251
335
. await ?;
336
+
337
+ let next_run = CronNextRun {
338
+ cron_job_id : id. developer_id ,
339
+ state : job. state ,
340
+ prev_ts : None ,
341
+ next_ts : job. next_ts ,
342
+ } ;
343
+ let existing_next_run = self . next_run ( id. developer_id ) . await ?;
344
+ if let Some ( existing_next_run) = existing_next_run {
345
+ SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
346
+ . replace ( existing_next_run. id ( ) , next_run. try_into ( ) ?)
347
+ . await ?;
348
+ } else {
349
+ SystemMetadataModel :: new ( self . tx , self . component . into ( ) )
350
+ . insert ( & CRON_NEXT_RUN_TABLE , next_run. try_into ( ) ?)
351
+ . await ?;
352
+ }
252
353
Ok ( ( ) )
253
354
}
254
355
0 commit comments