42fa9554 Andy McCall 1 month ago ↶
1
2
3
use super :: { now_ms, Pool} ;
4
use sqlx:: FromRow;
5
6
# [ derive ( Clone, Debug, FromRow ) ]
7
pub struct Run {
8
pub id : i64 ,
9
pub repo_id : i64 ,
a0d4bbfb Andy McCall 1 month ago ↶
10
11
#[sqlx(default)]
12
pub number : i64 ,
1edac4b6 Andy McCall 1 month ago ↶
13
14
#[sqlx(default)]
15
pub trigger : String,
42fa9554 Andy McCall 1 month ago ↶
16
pub bookmark : String,
17
pub state : String,
18
pub heads : String,
19
pub pipeline_hash : String,
20
pub status : String,
21
pub error : String,
22
pub actor : String,
23
pub created_ms : i64 ,
24
pub finished_ms : Option< i64 > ,
25
}
26
27
impl Run {
1edac4b6 Andy McCall 1 month ago ↶
28
29
pub fn started_by ( & self ) -> String {
30
match self .trigger.as_str ( ) {
31
" manual" => format! ( " started by {} " , self .actor) ,
32
t if t.starts_with ( " rerun:" ) => format! ( " re-run of #{} by {} " , & t[ 6 .. ] , self .actor) ,
33
_ => format! ( " pushed by {} " , self .actor) ,
34
}
35
}
36
42fa9554 Andy McCall 1 month ago ↶
37
pub fn head_list ( & self ) -> Vec< & str > {
38
self .heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .collect ( )
39
}
40
41
pub fn live ( & self ) -> bool {
42
matches! ( self .status.as_str ( ) , " queued" | " running" )
43
}
44
}
45
46
# [ derive ( Clone, Debug, FromRow ) ]
47
pub struct Job {
48
pub id : i64 ,
49
pub run_id : i64 ,
50
pub name : String,
51
pub depth : i64 ,
52
pub depends : String,
53
pub spec : String,
54
pub sandbox : String,
55
pub runtime : String,
56
pub status : String,
57
pub runner : String,
58
pub token_hash : String,
59
pub started_ms : Option< i64 > ,
60
pub finished_ms : Option< i64 > ,
61
pub exit_code : Option< i64 > ,
62
pub error : String,
63
pub receipt : String,
64
pub receipt_hash : String,
65
pub log_count : i64 ,
fd3db644 Andy McCall 1 month ago ↶
66
67
pub also_read : String,
cc19697f Andy McCall 1 month ago ↶
68
69
pub receipt_attest : String,
70
71
pub receipt_signer : String,
72
73
pub receipt_note : String,
42fa9554 Andy McCall 1 month ago ↶
74
}
75
76
impl Job {
77
pub fn depends_on ( & self ) -> Vec< String> {
78
serde_json:: from_str( & self .depends) .unwrap_or_default ( )
79
}
80
81
pub fn done ( & self ) -> bool {
82
matches! ( self .status.as_str ( ) , " passed" | " failed" | " skipped" | " error" )
83
}
84
}
85
86
# [ derive ( Clone, Debug, FromRow ) ]
87
pub struct LogLine {
88
pub seq : i64 ,
89
pub t : i64 ,
90
pub stream : String,
91
pub text : String,
92
}
93
94
95
pub struct NewJob {
96
pub name : String,
97
pub depth : i64 ,
98
pub depends : Vec< String> ,
99
pub spec : String,
100
pub sandbox : String,
101
pub runtime : String,
102
}
103
104
pub struct NewRun < 'a > {
105
pub repo_id : i64 ,
106
pub bookmark : & 'a str ,
107
pub state : & 'a str ,
108
pub heads : & 'a str ,
109
pub pipeline_hash : & 'a str ,
110
pub actor : & 'a str ,
1edac4b6 Andy McCall 1 month ago ↶
111
112
pub trigger : & 'a str ,
42fa9554 Andy McCall 1 month ago ↶
113
114
pub error : Option< & 'a str > ,
115
}
116
117
pub async fn create_run ( pool : & Pool, r : NewRun< '_ > , jobs : & [NewJob]) -> sqlx:: Result< i64 > {
941abdd6 Andy McCall 1 month ago ↶
118
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
119
let now = now_ms ( ) ;
120
let status = if r.error.is_some ( ) { " error" } else { " queued" } ;
121
let id: i64 = sqlx:: query_scalar(
1edac4b6 Andy McCall 1 month ago ↶
122
" INSERT INTO flow_runs (repo_id, number, bookmark, state, heads, pipeline_hash, status, error, actor, trigger, created_ms, finished_ms) \
123
VALUES (?, (SELECT COALESCE(MAX(number), 0) + 1 FROM flow_runs WHERE repo_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id" ,
42fa9554 Andy McCall 1 month ago ↶
124
)
125
.bind ( r.repo_id)
a0d4bbfb Andy McCall 1 month ago ↶
126
.bind ( r.repo_id)
42fa9554 Andy McCall 1 month ago ↶
127
.bind ( r.bookmark)
128
.bind ( r.state)
129
.bind ( r.heads)
130
.bind ( r.pipeline_hash)
131
.bind ( status)
132
.bind ( r.error.unwrap_or ( " " ) )
133
.bind ( r.actor)
1edac4b6 Andy McCall 1 month ago ↶
134
.bind ( r.trigger)
42fa9554 Andy McCall 1 month ago ↶
135
.bind ( now)
136
.bind ( r.error.map ( | _| now ) )
137
.fetch_one ( & mut * tx)
138
.await? ;
139
for j in jobs {
140
sqlx:: query(
141
" INSERT INTO flow_jobs (run_id, name, depth, depends, spec, sandbox, runtime, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued')" ,
142
)
143
.bind ( id)
144
.bind ( & j.name)
145
.bind ( j.depth)
146
.bind ( serde_json:: to_string( & j.depends) .unwrap_or_else ( | _| " []" .into ( ) ) )
147
.bind ( & j.spec)
148
.bind ( & j.sandbox)
149
.bind ( & j.runtime)
150
.execute ( & mut * tx)
151
.await? ;
152
}
153
tx.commit ( ) .await? ;
154
Ok ( id)
155
}
156
bf3d0754 Andy McCall 1 month ago ↶
157
158
# [ derive ( Clone, Debug, FromRow ) ]
159
pub struct QueueEntry {
160
pub job_id : i64 ,
161
pub run_id : i64 ,
162
pub repo_id : i64 ,
163
pub owner : String,
164
pub name : String,
165
pub visibility : String,
166
pub run_number : i64 ,
167
pub job : String,
168
pub status : String,
169
}
170
171
172
173
pub async fn queue ( pool : & Pool) -> sqlx:: Result< Vec< QueueEntry> > {
174
sqlx:: query_as(
175
" SELECT j.id AS job_id, j.run_id, r.repo_id, p.owner, p.name, p.visibility, r.number AS run_number, j.name AS job, j.status \
176
FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id \
177
WHERE j.status IN ('queued', 'running') AND r.status IN ('queued', 'running') \
178
ORDER BY CASE j.status WHEN 'running' THEN 0 ELSE 1 END, j.run_id, j.depth, j.id" ,
179
)
180
.fetch_all ( pool)
181
.await
182
}
183
90326c96 Andy McCall 1 month ago ↶
184
185
pub async fn runs_by_number ( pool : & Pool, repo_id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< Run> > {
186
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND number < ? ORDER BY number DESC LIMIT ?" )
187
.bind ( repo_id)
188
.bind ( before.unwrap_or ( i64 :: MAX ) )
189
.bind ( limit)
190
.fetch_all ( pool)
191
.await
192
}
193
42fa9554 Andy McCall 1 month ago ↶
194
pub async fn runs ( pool : & Pool, repo_id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< Run> > {
195
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND id < ? ORDER BY id DESC LIMIT ?" )
196
.bind ( repo_id)
197
.bind ( before.unwrap_or ( i64 :: MAX ) )
198
.bind ( limit)
199
.fetch_all ( pool)
200
.await
201
}
202
a0d4bbfb Andy McCall 1 month ago ↶
203
204
pub async fn run ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Option< Run> > {
205
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND number = ?" ) .bind ( repo_id) .bind ( number) .fetch_optional ( pool) .await
42fa9554 Andy McCall 1 month ago ↶
206
}
207
208
pub async fn run_by_id ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Run> > {
209
sqlx:: query_as( " SELECT * FROM flow_runs WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
210
}
211
212
pub async fn jobs ( pool : & Pool, run_id : i64 ) -> sqlx:: Result< Vec< Job> > {
213
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ? ORDER BY depth, name" ) .bind ( run_id) .fetch_all ( pool) .await
214
}
215
216
pub async fn job ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Job> > {
217
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
218
}
219
220
221
pub async fn job_by_token ( pool : & Pool, token_hash : & str , max_age_ms : i64 ) -> sqlx:: Result< Option< Job> > {
222
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE token_hash = ? AND status = 'running' AND started_ms >= ?" )
223
.bind ( token_hash)
224
.bind ( now_ms ( ) - max_age_ms)
225
.fetch_optional ( pool)
226
.await
227
}
228
229
230
fd3db644 Andy McCall 1 month ago ↶
231
232
pub async fn set_also_read ( pool : & Pool, job_id : i64 , repo_ids : & [i64 ]) -> sqlx:: Result< ( ) > {
233
let list: Vec< String> = repo_ids.iter ( ) .map ( | i | i.to_string ( ) ) .collect ( ) ;
234
sqlx:: query( " UPDATE flow_jobs SET also_read = ? WHERE id = ?" ) .bind ( list.join ( " ," ) ) .bind ( job_id) .execute ( pool) .await? ;
235
Ok ( ( ) )
236
}
237
238
impl Job {
239
pub fn also_read_ids ( & self ) -> Vec< i64 > {
240
self .also_read.split ( ' ,' ) .filter_map ( | x | x.trim ( ) .parse ( ) .ok ( ) ) .collect ( )
241
}
242
}
243
76bc5a05 Andy McCall 1 month ago ↶
244
245
# [ derive ( Clone, Copy, Debug, Default ) ]
246
pub struct Room {
247
pub memory_mib : Option< u32 > ,
248
pub cpus : Option< u32 > ,
249
}
250
251
impl Room {
252
253
pub fn fits ( & self , sandbox_json : & str ) -> bool {
254
let v: serde_json:: Value = serde_json:: from_str( sandbox_json) .unwrap_or_default ( ) ;
255
let need = | k : & str | v[ k] .as_u64 ( ) .unwrap_or ( 0 ) ;
256
self .memory_mib.is_none_or ( | m | need ( " memory_mib" ) < = m as u64 ) & & self .cpus.is_none_or ( | c | need ( " cpus" ) < = c as u64 )
257
}
258
}
259
42fa9554 Andy McCall 1 month ago ↶
260
pub async fn claim ( pool : & Pool, runtimes : & [String], runner : & str , token_hash : & str ) -> sqlx:: Result< Option< Job> > {
76bc5a05 Andy McCall 1 month ago ↶
261
claim_within ( pool, runtimes, runner, token_hash, Room:: default( ) ) .await
262
}
263
264
265
pub async fn claim_within ( pool : & Pool, runtimes : & [String], runner : & str , token_hash : & str , room : Room) -> sqlx:: Result< Option< Job> > {
941abdd6 Andy McCall 1 month ago ↶
266
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
267
let queued: Vec< Job> = sqlx:: query_as(
268
" SELECT j.* FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id WHERE j.status = 'queued' AND r.status IN ('queued', 'running') ORDER BY j.run_id, j.depth, j.id LIMIT 200" ,
269
)
270
.fetch_all ( & mut * tx)
271
.await? ;
272
for j in queued {
76bc5a05 Andy McCall 1 month ago ↶
273
if ! runtimes.iter ( ) .any ( | r | r = = & j.runtime ) | | ! room.fits ( & j.sandbox) {
42fa9554 Andy McCall 1 month ago ↶
274
continue ;
275
}
276
let deps = j.depends_on ( ) ;
277
if ! deps.is_empty ( ) {
278
let mut passed = 0 i64 ;
279
for d in & deps {
280
let st: Option< String> = sqlx:: query_scalar( " SELECT status FROM flow_jobs WHERE run_id = ? AND name = ?" )
281
.bind ( j.run_id)
282
.bind ( d)
283
.fetch_optional ( & mut * tx)
284
.await? ;
285
if st.as_deref ( ) = = Some ( " passed" ) {
286
passed + = 1 ;
287
}
288
}
289
if passed ! = deps.len ( ) as i64 {
290
continue ;
291
}
292
}
293
let now = now_ms ( ) ;
294
let done = sqlx:: query(
295
" UPDATE flow_jobs SET status = 'running', runner = ?, token_hash = ?, started_ms = ? WHERE id = ? AND status = 'queued'" ,
296
)
297
.bind ( runner)
298
.bind ( token_hash)
299
.bind ( now)
300
.bind ( j.id)
301
.execute ( & mut * tx)
302
.await? ;
303
if done.rows_affected ( ) = = 1 {
304
sqlx:: query( " UPDATE flow_runs SET status = 'running' WHERE id = ? AND status = 'queued'" )
305
.bind ( j.run_id)
306
.execute ( & mut * tx)
307
.await? ;
308
tx.commit ( ) .await? ;
309
return job ( pool, j.id) .await;
310
}
311
}
312
tx.commit ( ) .await? ;
313
Ok ( None )
314
}
315
316
317
pub async fn append_log ( pool : & Pool, job_id : i64 , records : & [flow_core:: api:: Record], cap : i64 ) -> sqlx:: Result< usize > {
941abdd6 Andy McCall 1 month ago ↶
318
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
319
let count: i64 = sqlx:: query_scalar( " SELECT log_count FROM flow_jobs WHERE id = ?" ) .bind ( job_id) .fetch_one ( & mut * tx) .await? ;
320
let room = ( cap - count) .max ( 0 ) as usize ;
321
let keep = & records[ .. records.len ( ) .min ( room) ] ;
322
for ( i, r) in keep.iter ( ) .enumerate ( ) {
323
sqlx:: query( " INSERT INTO flow_logs (job_id, seq, t, stream, text) VALUES (?, ?, ?, ?, ?)" )
324
.bind ( job_id)
325
.bind ( count + i as i64 + 1 )
326
.bind ( r.t as i64 )
327
.bind ( & r.stream)
328
.bind ( & r.text)
329
.execute ( & mut * tx)
330
.await? ;
331
}
332
sqlx:: query( " UPDATE flow_jobs SET log_count = log_count + ? WHERE id = ?" )
333
.bind ( keep.len ( ) as i64 )
334
.bind ( job_id)
335
.execute ( & mut * tx)
336
.await? ;
337
tx.commit ( ) .await? ;
338
Ok ( keep.len ( ) )
339
}
340
341
pub async fn log ( pool : & Pool, job_id : i64 , after : i64 , limit : i64 ) -> sqlx:: Result< Vec< LogLine> > {
342
sqlx:: query_as( " SELECT seq, t, stream, text FROM flow_logs WHERE job_id = ? AND seq > ? ORDER BY seq LIMIT ?" )
343
.bind ( job_id)
344
.bind ( after)
345
.bind ( limit)
346
.fetch_all ( pool)
347
.await
348
}
349
350
pub struct Outcome < 'a > {
351
pub status : & 'a str ,
352
pub exit_code : Option< i64 > ,
353
pub error : & 'a str ,
354
pub receipt : & 'a str ,
355
pub receipt_hash : & 'a str ,
356
}
357
358
359
360
pub async fn finish ( pool : & Pool, job_id : i64 , o : Outcome< '_ > ) -> sqlx:: Result< Vec< Job> > {
941abdd6 Andy McCall 1 month ago ↶
361
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
362
let now = now_ms ( ) ;
363
let j: Job = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE id = ?" ) .bind ( job_id) .fetch_one ( & mut * tx) .await? ;
364
sqlx:: query( " UPDATE flow_jobs SET status = ?, exit_code = ?, error = ?, receipt = ?, receipt_hash = ?, finished_ms = ?, token_hash = '' WHERE id = ?" )
365
.bind ( o.status)
366
.bind ( o.exit_code)
367
.bind ( o.error)
368
.bind ( o.receipt)
369
.bind ( o.receipt_hash)
370
.bind ( now)
371
.bind ( job_id)
372
.execute ( & mut * tx)
373
.await? ;
374
let mut changed = vec! [ job_id] ;
375
376
loop {
377
let all: Vec< Job> = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ?" ) .bind ( j.run_id) .fetch_all ( & mut * tx) .await? ;
378
let blocked: Vec< i64 > = all
379
.iter ( )
380
.filter ( | x | x.status = = " queued" )
381
.filter ( | x | {
382
x.depends_on ( )
383
.iter ( )
384
.any ( | d | all.iter ( ) .any ( | y | & y.name = = d & & matches! ( y.status.as_str ( ) , " failed" | " skipped" | " error" ) ) )
385
} )
386
.map ( | x | x.id )
387
.collect ( ) ;
388
if blocked.is_empty ( ) {
389
break ;
390
}
391
for id in blocked {
392
sqlx:: query( " UPDATE flow_jobs SET status = 'skipped', finished_ms = ? WHERE id = ?" )
393
.bind ( now)
394
.bind ( id)
395
.execute ( & mut * tx)
396
.await? ;
397
changed.push ( id) ;
398
}
399
}
400
let all: Vec< Job> = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ?" ) .bind ( j.run_id) .fetch_all ( & mut * tx) .await? ;
401
if all.iter ( ) .all ( Job:: done) {
402
let status = if all.iter ( ) .all ( | x | x.status = = " passed" ) { " passed" } else { " failed" } ;
403
sqlx:: query( " UPDATE flow_runs SET status = ?, finished_ms = ? WHERE id = ?" )
404
.bind ( status)
405
.bind ( now)
406
.bind ( j.run_id)
407
.execute ( & mut * tx)
408
.await? ;
409
}
410
tx.commit ( ) .await? ;
411
let mut out = Vec :: new( ) ;
412
for id in changed {
413
if let Some ( x) = job ( pool, id) .await? {
414
out.push ( x) ;
415
}
416
}
417
Ok ( out)
418
}
736f6813 Andy McCall 1 month ago ↶
419
420
421
pub async fn latest_for_heads ( pool : & Pool, repo_id : i64 , heads : & str ) -> sqlx:: Result< Option< Run> > {
422
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND heads = ? ORDER BY id DESC LIMIT 1" )
423
.bind ( repo_id)
424
.bind ( heads)
425
.fetch_optional ( pool)
426
.await
cc19697f Andy McCall 1 month ago ↶
427
}
428
429
430
pub async fn set_receipt ( pool : & Pool, job_id : i64 , attest : & str , signer : & str , note : & str ) -> sqlx:: Result< ( ) > {
431
sqlx:: query( " UPDATE flow_jobs SET receipt_attest = ?, receipt_signer = ?, receipt_note = ? WHERE id = ?" )
432
.bind ( attest)
433
.bind ( signer)
434
.bind ( note)
435
.bind ( job_id)
436
.execute ( pool)
437
.await? ;
438
Ok ( ( ) )
736f6813 Andy McCall 1 month ago ↶
439
}
a3c8288b Andy McCall 1 month ago ↶
440
441
442
# [ derive ( Clone, Debug, FromRow ) ]
443
pub struct Artifact {
444
pub job_id : i64 ,
445
pub name : String,
446
pub path : String,
447
pub blob : String,
448
pub size : i64 ,
449
450
pub content : String,
451
pub created_ms : i64 ,
452
}
453
454
pub async fn add_artifact ( pool : & Pool, a : & Artifact) -> sqlx:: Result< ( ) > {
455
sqlx:: query( " INSERT OR REPLACE INTO flow_artifacts (job_id, name, path, blob, size, content, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?)" )
456
.bind ( a.job_id)
457
.bind ( & a.name)
458
.bind ( & a.path)
459
.bind ( & a.blob)
460
.bind ( a.size)
461
.bind ( & a.content)
462
.bind ( a.created_ms)
463
.execute ( pool)
464
.await? ;
465
Ok ( ( ) )
466
}
467
468
469
pub async fn artifacts ( pool : & Pool, job_id : i64 ) -> sqlx:: Result< Vec< Artifact> > {
470
sqlx:: query_as( " SELECT * FROM flow_artifacts WHERE job_id = ? ORDER BY name" ) .bind ( job_id) .fetch_all ( pool) .await
471
}
10708ab8 Andy McCall 1 month ago ↶
472
473
474
475
# [ derive ( Clone, Debug, FromRow ) ]
476
pub struct Provenance {
477
pub run_number : i64 ,
478
pub job : String,
479
pub name : String,
480
pub receipt_attest : String,
481
pub receipt_signer : String,
482
}
483
484
485
486
487
pub async fn provenance ( pool : & Pool, repo_id : i64 , content : & str ) -> sqlx:: Result< Option< Provenance> > {
488
sqlx:: query_as(
489
" SELECT r.number AS run_number, j.name AS job, a.name AS name, j.receipt_attest, j.receipt_signer \
490
FROM flow_artifacts a JOIN flow_jobs j ON j.id = a.job_id JOIN flow_runs r ON r.id = j.run_id \
491
WHERE r.repo_id = ? AND a.content = ? AND j.status = 'passed' \
492
ORDER BY (j.receipt_attest != '') DESC, r.number DESC LIMIT 1" ,
493
)
494
.bind ( repo_id)
495
.bind ( content)
496
.fetch_optional ( pool)
497
.await
498
}