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 ,
42fa9554 Andy McCall 1 month ago ↶
13
pub bookmark : String,
14
pub state : String,
15
pub heads : String,
16
pub pipeline_hash : String,
17
pub status : String,
18
pub error : String,
19
pub actor : String,
20
pub created_ms : i64 ,
21
pub finished_ms : Option< i64 > ,
22
}
23
24
impl Run {
25
pub fn head_list ( & self ) -> Vec< & str > {
26
self .heads.split ( ' +' ) .filter ( | h | ! h.is_empty ( ) ) .collect ( )
27
}
28
29
pub fn live ( & self ) -> bool {
30
matches! ( self .status.as_str ( ) , " queued" | " running" )
31
}
32
}
33
34
# [ derive ( Clone, Debug, FromRow ) ]
35
pub struct Job {
36
pub id : i64 ,
37
pub run_id : i64 ,
38
pub name : String,
39
pub depth : i64 ,
40
pub depends : String,
41
pub spec : String,
42
pub sandbox : String,
43
pub runtime : String,
44
pub status : String,
45
pub runner : String,
46
pub token_hash : String,
47
pub started_ms : Option< i64 > ,
48
pub finished_ms : Option< i64 > ,
49
pub exit_code : Option< i64 > ,
50
pub error : String,
51
pub receipt : String,
52
pub receipt_hash : String,
53
pub log_count : i64 ,
fd3db644 Andy McCall 1 month ago ↶
54
55
pub also_read : String,
42fa9554 Andy McCall 1 month ago ↶
56
}
57
58
impl Job {
59
pub fn depends_on ( & self ) -> Vec< String> {
60
serde_json:: from_str( & self .depends) .unwrap_or_default ( )
61
}
62
63
pub fn done ( & self ) -> bool {
64
matches! ( self .status.as_str ( ) , " passed" | " failed" | " skipped" | " error" )
65
}
66
}
67
68
# [ derive ( Clone, Debug, FromRow ) ]
69
pub struct LogLine {
70
pub seq : i64 ,
71
pub t : i64 ,
72
pub stream : String,
73
pub text : String,
74
}
75
76
77
pub struct NewJob {
78
pub name : String,
79
pub depth : i64 ,
80
pub depends : Vec< String> ,
81
pub spec : String,
82
pub sandbox : String,
83
pub runtime : String,
84
}
85
86
pub struct NewRun < 'a > {
87
pub repo_id : i64 ,
88
pub bookmark : & 'a str ,
89
pub state : & 'a str ,
90
pub heads : & 'a str ,
91
pub pipeline_hash : & 'a str ,
92
pub actor : & 'a str ,
93
94
pub error : Option< & 'a str > ,
95
}
96
97
pub async fn create_run ( pool : & Pool, r : NewRun< '_ > , jobs : & [NewJob]) -> sqlx:: Result< i64 > {
941abdd6 Andy McCall 1 month ago ↶
98
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
99
let now = now_ms ( ) ;
100
let status = if r.error.is_some ( ) { " error" } else { " queued" } ;
101
let id: i64 = sqlx:: query_scalar(
a0d4bbfb Andy McCall 1 month ago ↶
102
" INSERT INTO flow_runs (repo_id, number, bookmark, state, heads, pipeline_hash, status, error, actor, created_ms, finished_ms) \
103
VALUES (?, (SELECT COALESCE(MAX(number), 0) + 1 FROM flow_runs WHERE repo_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id" ,
42fa9554 Andy McCall 1 month ago ↶
104
)
105
.bind ( r.repo_id)
a0d4bbfb Andy McCall 1 month ago ↶
106
.bind ( r.repo_id)
42fa9554 Andy McCall 1 month ago ↶
107
.bind ( r.bookmark)
108
.bind ( r.state)
109
.bind ( r.heads)
110
.bind ( r.pipeline_hash)
111
.bind ( status)
112
.bind ( r.error.unwrap_or ( " " ) )
113
.bind ( r.actor)
114
.bind ( now)
115
.bind ( r.error.map ( | _| now ) )
116
.fetch_one ( & mut * tx)
117
.await? ;
118
for j in jobs {
119
sqlx:: query(
120
" INSERT INTO flow_jobs (run_id, name, depth, depends, spec, sandbox, runtime, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued')" ,
121
)
122
.bind ( id)
123
.bind ( & j.name)
124
.bind ( j.depth)
125
.bind ( serde_json:: to_string( & j.depends) .unwrap_or_else ( | _| " []" .into ( ) ) )
126
.bind ( & j.spec)
127
.bind ( & j.sandbox)
128
.bind ( & j.runtime)
129
.execute ( & mut * tx)
130
.await? ;
131
}
132
tx.commit ( ) .await? ;
133
Ok ( id)
134
}
135
136
pub async fn runs ( pool : & Pool, repo_id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< Run> > {
137
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND id < ? ORDER BY id DESC LIMIT ?" )
138
.bind ( repo_id)
139
.bind ( before.unwrap_or ( i64 :: MAX ) )
140
.bind ( limit)
141
.fetch_all ( pool)
142
.await
143
}
144
a0d4bbfb Andy McCall 1 month ago ↶
145
146
pub async fn run ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Option< Run> > {
147
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 ↶
148
}
149
150
pub async fn run_by_id ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Run> > {
151
sqlx:: query_as( " SELECT * FROM flow_runs WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
152
}
153
154
pub async fn jobs ( pool : & Pool, run_id : i64 ) -> sqlx:: Result< Vec< Job> > {
155
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ? ORDER BY depth, name" ) .bind ( run_id) .fetch_all ( pool) .await
156
}
157
158
pub async fn job ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Job> > {
159
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
160
}
161
162
163
pub async fn job_by_token ( pool : & Pool, token_hash : & str , max_age_ms : i64 ) -> sqlx:: Result< Option< Job> > {
164
sqlx:: query_as( " SELECT * FROM flow_jobs WHERE token_hash = ? AND status = 'running' AND started_ms >= ?" )
165
.bind ( token_hash)
166
.bind ( now_ms ( ) - max_age_ms)
167
.fetch_optional ( pool)
168
.await
169
}
170
171
172
fd3db644 Andy McCall 1 month ago ↶
173
174
pub async fn set_also_read ( pool : & Pool, job_id : i64 , repo_ids : & [i64 ]) -> sqlx:: Result< ( ) > {
175
let list: Vec< String> = repo_ids.iter ( ) .map ( | i | i.to_string ( ) ) .collect ( ) ;
176
sqlx:: query( " UPDATE flow_jobs SET also_read = ? WHERE id = ?" ) .bind ( list.join ( " ," ) ) .bind ( job_id) .execute ( pool) .await? ;
177
Ok ( ( ) )
178
}
179
180
impl Job {
181
pub fn also_read_ids ( & self ) -> Vec< i64 > {
182
self .also_read.split ( ' ,' ) .filter_map ( | x | x.trim ( ) .parse ( ) .ok ( ) ) .collect ( )
183
}
184
}
185
76bc5a05 Andy McCall 1 month ago ↶
186
187
# [ derive ( Clone, Copy, Debug, Default ) ]
188
pub struct Room {
189
pub memory_mib : Option< u32 > ,
190
pub cpus : Option< u32 > ,
191
}
192
193
impl Room {
194
195
pub fn fits ( & self , sandbox_json : & str ) -> bool {
196
let v: serde_json:: Value = serde_json:: from_str( sandbox_json) .unwrap_or_default ( ) ;
197
let need = | k : & str | v[ k] .as_u64 ( ) .unwrap_or ( 0 ) ;
198
self .memory_mib.is_none_or ( | m | need ( " memory_mib" ) < = m as u64 ) & & self .cpus.is_none_or ( | c | need ( " cpus" ) < = c as u64 )
199
}
200
}
201
42fa9554 Andy McCall 1 month ago ↶
202
pub async fn claim ( pool : & Pool, runtimes : & [String], runner : & str , token_hash : & str ) -> sqlx:: Result< Option< Job> > {
76bc5a05 Andy McCall 1 month ago ↶
203
claim_within ( pool, runtimes, runner, token_hash, Room:: default( ) ) .await
204
}
205
206
207
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 ↶
208
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
209
let queued: Vec< Job> = sqlx:: query_as(
210
" 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" ,
211
)
212
.fetch_all ( & mut * tx)
213
.await? ;
214
for j in queued {
76bc5a05 Andy McCall 1 month ago ↶
215
if ! runtimes.iter ( ) .any ( | r | r = = & j.runtime ) | | ! room.fits ( & j.sandbox) {
42fa9554 Andy McCall 1 month ago ↶
216
continue ;
217
}
218
let deps = j.depends_on ( ) ;
219
if ! deps.is_empty ( ) {
220
let mut passed = 0 i64 ;
221
for d in & deps {
222
let st: Option< String> = sqlx:: query_scalar( " SELECT status FROM flow_jobs WHERE run_id = ? AND name = ?" )
223
.bind ( j.run_id)
224
.bind ( d)
225
.fetch_optional ( & mut * tx)
226
.await? ;
227
if st.as_deref ( ) = = Some ( " passed" ) {
228
passed + = 1 ;
229
}
230
}
231
if passed ! = deps.len ( ) as i64 {
232
continue ;
233
}
234
}
235
let now = now_ms ( ) ;
236
let done = sqlx:: query(
237
" UPDATE flow_jobs SET status = 'running', runner = ?, token_hash = ?, started_ms = ? WHERE id = ? AND status = 'queued'" ,
238
)
239
.bind ( runner)
240
.bind ( token_hash)
241
.bind ( now)
242
.bind ( j.id)
243
.execute ( & mut * tx)
244
.await? ;
245
if done.rows_affected ( ) = = 1 {
246
sqlx:: query( " UPDATE flow_runs SET status = 'running' WHERE id = ? AND status = 'queued'" )
247
.bind ( j.run_id)
248
.execute ( & mut * tx)
249
.await? ;
250
tx.commit ( ) .await? ;
251
return job ( pool, j.id) .await;
252
}
253
}
254
tx.commit ( ) .await? ;
255
Ok ( None )
256
}
257
258
259
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 ↶
260
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
261
let count: i64 = sqlx:: query_scalar( " SELECT log_count FROM flow_jobs WHERE id = ?" ) .bind ( job_id) .fetch_one ( & mut * tx) .await? ;
262
let room = ( cap - count) .max ( 0 ) as usize ;
263
let keep = & records[ .. records.len ( ) .min ( room) ] ;
264
for ( i, r) in keep.iter ( ) .enumerate ( ) {
265
sqlx:: query( " INSERT INTO flow_logs (job_id, seq, t, stream, text) VALUES (?, ?, ?, ?, ?)" )
266
.bind ( job_id)
267
.bind ( count + i as i64 + 1 )
268
.bind ( r.t as i64 )
269
.bind ( & r.stream)
270
.bind ( & r.text)
271
.execute ( & mut * tx)
272
.await? ;
273
}
274
sqlx:: query( " UPDATE flow_jobs SET log_count = log_count + ? WHERE id = ?" )
275
.bind ( keep.len ( ) as i64 )
276
.bind ( job_id)
277
.execute ( & mut * tx)
278
.await? ;
279
tx.commit ( ) .await? ;
280
Ok ( keep.len ( ) )
281
}
282
283
pub async fn log ( pool : & Pool, job_id : i64 , after : i64 , limit : i64 ) -> sqlx:: Result< Vec< LogLine> > {
284
sqlx:: query_as( " SELECT seq, t, stream, text FROM flow_logs WHERE job_id = ? AND seq > ? ORDER BY seq LIMIT ?" )
285
.bind ( job_id)
286
.bind ( after)
287
.bind ( limit)
288
.fetch_all ( pool)
289
.await
290
}
291
292
pub struct Outcome < 'a > {
293
pub status : & 'a str ,
294
pub exit_code : Option< i64 > ,
295
pub error : & 'a str ,
296
pub receipt : & 'a str ,
297
pub receipt_hash : & 'a str ,
298
}
299
300
301
302
pub async fn finish ( pool : & Pool, job_id : i64 , o : Outcome< '_ > ) -> sqlx:: Result< Vec< Job> > {
941abdd6 Andy McCall 1 month ago ↶
303
let mut tx = super :: begin_write( pool) .await? ;
42fa9554 Andy McCall 1 month ago ↶
304
let now = now_ms ( ) ;
305
let j: Job = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE id = ?" ) .bind ( job_id) .fetch_one ( & mut * tx) .await? ;
306
sqlx:: query( " UPDATE flow_jobs SET status = ?, exit_code = ?, error = ?, receipt = ?, receipt_hash = ?, finished_ms = ?, token_hash = '' WHERE id = ?" )
307
.bind ( o.status)
308
.bind ( o.exit_code)
309
.bind ( o.error)
310
.bind ( o.receipt)
311
.bind ( o.receipt_hash)
312
.bind ( now)
313
.bind ( job_id)
314
.execute ( & mut * tx)
315
.await? ;
316
let mut changed = vec! [ job_id] ;
317
318
loop {
319
let all: Vec< Job> = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ?" ) .bind ( j.run_id) .fetch_all ( & mut * tx) .await? ;
320
let blocked: Vec< i64 > = all
321
.iter ( )
322
.filter ( | x | x.status = = " queued" )
323
.filter ( | x | {
324
x.depends_on ( )
325
.iter ( )
326
.any ( | d | all.iter ( ) .any ( | y | & y.name = = d & & matches! ( y.status.as_str ( ) , " failed" | " skipped" | " error" ) ) )
327
} )
328
.map ( | x | x.id )
329
.collect ( ) ;
330
if blocked.is_empty ( ) {
331
break ;
332
}
333
for id in blocked {
334
sqlx:: query( " UPDATE flow_jobs SET status = 'skipped', finished_ms = ? WHERE id = ?" )
335
.bind ( now)
336
.bind ( id)
337
.execute ( & mut * tx)
338
.await? ;
339
changed.push ( id) ;
340
}
341
}
342
let all: Vec< Job> = sqlx:: query_as( " SELECT * FROM flow_jobs WHERE run_id = ?" ) .bind ( j.run_id) .fetch_all ( & mut * tx) .await? ;
343
if all.iter ( ) .all ( Job:: done) {
344
let status = if all.iter ( ) .all ( | x | x.status = = " passed" ) { " passed" } else { " failed" } ;
345
sqlx:: query( " UPDATE flow_runs SET status = ?, finished_ms = ? WHERE id = ?" )
346
.bind ( status)
347
.bind ( now)
348
.bind ( j.run_id)
349
.execute ( & mut * tx)
350
.await? ;
351
}
352
tx.commit ( ) .await? ;
353
let mut out = Vec :: new( ) ;
354
for id in changed {
355
if let Some ( x) = job ( pool, id) .await? {
356
out.push ( x) ;
357
}
358
}
359
Ok ( out)
360
}
736f6813 Andy McCall 1 month ago ↶
361
362
363
pub async fn latest_for_heads ( pool : & Pool, repo_id : i64 , heads : & str ) -> sqlx:: Result< Option< Run> > {
364
sqlx:: query_as( " SELECT * FROM flow_runs WHERE repo_id = ? AND heads = ? ORDER BY id DESC LIMIT 1" )
365
.bind ( repo_id)
366
.bind ( heads)
367
.fetch_optional ( pool)
368
.await
369
}