42fa9554 Andy McCall 1 month ago ↶
1
2
3
4
5
use crate :: api:: ApiError;
6
use crate :: db:: { self , flow as fdb} ;
7
use crate :: repos:: RepoName;
8
use crate :: Shared;
9
use axum:: extract:: { Path, State} ;
10
use axum:: http:: { HeaderMap, StatusCode} ;
11
use axum:: response:: { IntoResponse, Response} ;
12
use axum:: Json;
13
use flow_core:: api:: { Claim, ClaimRequest, Finish, LogBatch, MAX_RECORDS , MAX_RECORD_TEXT } ;
14
use flow_core:: { Pipeline, PIPELINE_PATH } ;
15
use oxy_core:: hash:: ChangeHash;
16
17
18
pub const JOB_TOKEN_PREFIX : & str = " flowjob_" ;
19
20
pub const JOB_TOKEN_MAX_AGE_MS : i64 = 2 * 3600 * 1000 ;
21
9008bf6c Andy McCall 1 month ago ↶
22
23
24
pub fn enabled ( app : & Shared, row : & db:: repos:: RepoRow) -> bool {
25
match row.flow.as_str ( ) {
1870e4fa Andy McCall 22 days ago ↶
26
27
" on" | " own" => true ,
9008bf6c Andy McCall 1 month ago ↶
28
" off" => false ,
29
_ => app.config.flow.allows ( & row.owner, & row.name) ,
30
}
31
}
32
42fa9554 Andy McCall 1 month ago ↶
33
34
35
pub async fn on_push ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , state : & str , heads : & [ChangeHash], actor : & str ) {
c889911b Andy McCall 1 month ago ↶
36
if heads.is_empty ( ) | | ! enabled ( app, row) {
42fa9554 Andy McCall 1 month ago ↶
37
return ;
38
}
c889911b Andy McCall 1 month ago ↶
39
40
let started = match bookmark.strip_prefix ( " tags/" ) {
41
Some ( tag) => start_run_with ( app, row, bookmark, state, heads, actor, & format! ( " tag:{tag} " ) , None ) .await,
42
None => {
43
let touched = touched_since_last_run ( app, row, bookmark, heads) .await;
745d225d Andy McCall 1 month ago ↶
44
let started = start_run_scoped ( app, row, bookmark, state, heads, actor, " push" , touched.as_deref ( ) ) .await;
45
if let Ok ( Some ( n) ) = started {
46
supersede ( app, row, bookmark, n) .await;
47
}
48
started
c889911b Andy McCall 1 month ago ↶
49
}
50
} ;
51
if let Err ( e) = started {
1edac4b6 Andy McCall 1 month ago ↶
52
tracing:: warn! ( " flow: starting a run of {}/{}: {e}" , row.owner, row.name) ;
53
}
54
}
55
56
57
0650d94e Andy McCall 26 days ago ↶
58
59
60
pub async fn run_bookmark ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , actor : & str , jobs : & [String]) -> Result< i64 , String> {
1edac4b6 Andy McCall 1 month ago ↶
61
if ! enabled ( app, row) {
62
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
63
}
64
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
65
let b = bookmark.to_string ( ) ;
66
let found = app
67
.repos
68
.read ( & repo, move | r, _ | {
69
let Some ( st) = r.view ( ) ? .bookmarks.get ( & b) .copied ( ) else { return Ok ( None ) } ;
70
Ok ( Some ( ( st, r.store ( ) .get_state ( & st) ? .heads) ) )
71
} )
72
.await
73
.map_err ( | e | e.to_string ( ) ) ? ;
74
let Some ( ( state, heads) ) = found else { return Err ( format! ( " no bookmark {bookmark} " ) ) } ;
75
if heads.is_empty ( ) {
76
return Err ( format! ( " {bookmark} has no changes yet" ) ) ;
77
}
0650d94e Andy McCall 26 days ago ↶
78
start_run_at ( app, row, bookmark, & state.to_hex ( ) , & heads, actor, " manual" , None , None , jobs)
1edac4b6 Andy McCall 1 month ago ↶
79
.await?
80
.ok_or_else ( | | format! ( " {bookmark} has no {PIPELINE_PATH} " ) )
81
}
82
4f692533 Andy McCall 1 month ago ↶
83
84
0650d94e Andy McCall 26 days ago ↶
85
pub async fn run_change ( app : & Shared, row : & db:: repos:: RepoRow, change : & str , actor : & str , jobs : & [String]) -> Result< i64 , String> {
4f692533 Andy McCall 1 month ago ↶
86
if ! enabled ( app, row) {
87
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
88
}
89
let h = ChangeHash:: from_hex( change.trim ( ) .trim_start_matches ( ' #' ) ) .ok_or ( " give the full hash of a change" ) ? ;
90
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
df15b747 Andy McCall 1 month ago ↶
91
let known = app.repos.read ( & repo, move | r, _ | Ok ( r.store ( ) .get_change_meta ( & h) .is_ok ( ) ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
4f692533 Andy McCall 1 month ago ↶
92
if ! known {
93
return Err ( format! ( " no change {} in this repository" , & change.trim ( ) [ .. 12. min ( change.trim ( ) .len ( ) ) ] ) ) ;
94
}
95
let state = oxy_core:: hash:: hash_object( oxy_core:: hash:: ObjectType:: State, & oxy_core:: objects:: StateObject:: new( vec! [ h] ) .encode ( ) ) ;
0650d94e Andy McCall 26 days ago ↶
96
start_run_at ( app, row, " " , & state.to_hex ( ) , & [ h] , actor, " manual" , None , None , jobs)
4f692533 Andy McCall 1 month ago ↶
97
.await?
98
.ok_or_else ( | | format! ( " that change's state has no {PIPELINE_PATH} " ) )
99
}
100
1edac4b6 Andy McCall 1 month ago ↶
101
102
103
pub async fn rerun ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, actor : & str ) -> Result< i64 , String> {
104
if ! enabled ( app, row) {
105
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
106
}
107
if run.live ( ) {
108
return Err ( format! ( " run #{} is still {} " , run.number, run.status) ) ;
109
}
110
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
111
if heads.is_empty ( ) {
112
return Err ( " the run names no changes" .into ( ) ) ;
113
}
c889911b Andy McCall 1 month ago ↶
114
115
let trigger = format! ( " rerun:{} :{} " , run.number, original_event ( run) ) ;
116
start_run ( app, row, & run.bookmark, & run.state, & heads, actor, & trigger)
1edac4b6 Andy McCall 1 month ago ↶
117
.await?
118
.ok_or_else ( | | format! ( " that state has no {PIPELINE_PATH} " ) )
745d225d Andy McCall 1 month ago ↶
119
}
120
e8fa5369 Andy McCall 23 days ago ↶
121
122
123
124
125
pub async fn rerun_job ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, job : & str , actor : & str ) -> Result< usize , String> {
126
if ! enabled ( app, row) {
127
return Err ( " OxygenFlow is not enabled for this repository" .into ( ) ) ;
128
}
129
if run.live ( ) {
130
return Err ( format! ( " run #{} is still {} : wait for it, or cancel it, first" , run.number, run.status) ) ;
131
}
132
let changed = fdb:: rerun_job( & app.db, run.id, job) .await.map_err ( | e | {
133
tracing:: warn! ( " flow: rerunning {job} of run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
134
" the job could not be queued again" .to_string ( )
135
} ) ? ;
136
if changed.is_empty ( ) {
137
return Err ( format! ( " run #{} has no job {job:?} " , run.number) ) ;
138
}
139
for j in & changed {
140
report_check ( app, row, & run.state, run.number, & j.name, " queued" , & format! ( " queued again by {actor} " ) ) .await;
141
}
142
Ok ( changed.len ( ) )
143
}
144
745d225d Andy McCall 1 month ago ↶
145
146
pub async fn cancel ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, actor : & str ) -> Result< ( ) , String> {
147
if ! run.live ( ) {
148
return Err ( format! ( " run #{} is already {} " , run.number, run.status) ) ;
149
}
150
cancel_with ( app, run, & format! ( " cancelled by {actor} " ) ) .await.map_err ( | e | {
151
tracing:: warn! ( " flow: cancelling run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
152
" the run could not be cancelled" .to_string ( )
153
} )
154
}
155
156
async fn cancel_with ( app : & Shared, run : & fdb:: Run, why : & str ) -> sqlx:: Result< ( ) > {
157
let changed = fdb:: cancel_run( & app.db, run.id, why) .await? ;
158
report_changed ( app, run.id, changed) .await
c889911b Andy McCall 1 month ago ↶
159
}
160
417184c7 Andy McCall 22 days ago ↶
161
162
163
164
165
pub async fn may_decide (
166
app : & Shared,
167
row : & db:: repos:: RepoRow,
168
run : & fdb:: Run,
169
job : & fdb:: Job,
170
who : & db:: accounts:: Account,
171
) -> Result< flow_core:: Approval, String> {
172
let spec: flow_core:: Job = serde_json:: from_str( & job.spec) .map_err ( | e | format! ( " stored job: {e} " ) ) ? ;
173
let Some ( approval) = spec.approval else { return Err ( format! ( " {} does not wait for approval" , job.name) ) } ;
174
if job.status ! = " queued" | | job.approval ! = " waiting" {
175
return Err ( format! ( " {} is not waiting for approval" , job.name) ) ;
176
}
177
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
178
if let Some ( d) = job.depends_on ( ) .iter ( ) .find ( | d | ! jobs.iter ( ) .any ( | j | & j.name = = * d & & j.status = = " passed" ) ) {
179
return Err ( format! ( " {} waits for {d} first" , job.name) ) ;
180
}
181
let need = crate :: auth:: roles:: Role:: parse( & approval.by) .unwrap_or ( crate :: auth:: roles:: Role:: Maintain) ;
182
let role = crate :: auth:: roles:: role_for( & app.db, Some ( who) , row) .await.map_err ( | e | e.to_string ( ) ) ? ;
183
if role.is_none_or ( | r | r < need ) {
184
return Err ( format! ( " approving {} takes the {} role on {} /{} " , job.name, approval.by, row.owner, row.name) ) ;
185
}
186
if approval.others_only & & who.username = = run.actor {
187
return Err ( format! ( " {} must be approved by someone other than {} , whose push started the run" , job.name, run.actor) ) ;
188
}
189
Ok ( approval)
190
}
191
192
193
194
195
pub async fn decide (
196
app : & Shared,
197
row : & db:: repos:: RepoRow,
198
run : & fdb:: Run,
199
name : & str ,
200
who : & db:: accounts:: Account,
201
approve : bool ,
05f1a85a Andy McCall 20 days ago ↶
202
via : Option< & str > ,
417184c7 Andy McCall 22 days ago ↶
203
) -> Result< String, String> {
05f1a85a Andy McCall 20 days ago ↶
204
decide_with ( app, row, run, name, who, approve, Through { seal: None , via } ) .await
47e37402 Andy McCall 21 days ago ↶
205
}
206
05f1a85a Andy McCall 20 days ago ↶
207
208
209
# [ derive ( Clone, Copy, Default ) ]
210
struct Through < 'a > {
211
seal : Option< & 'a str > ,
212
via : Option< & 'a str > ,
213
}
214
47e37402 Andy McCall 21 days ago ↶
215
216
217
async fn decide_with (
218
app : & Shared,
219
row : & db:: repos:: RepoRow,
220
run : & fdb:: Run,
221
name : & str ,
222
who : & db:: accounts:: Account,
223
approve : bool ,
05f1a85a Andy McCall 20 days ago ↶
224
through : Through< '_ > ,
47e37402 Andy McCall 21 days ago ↶
225
) -> Result< String, String> {
05f1a85a Andy McCall 20 days ago ↶
226
let Through { seal, via } = through;
417184c7 Andy McCall 22 days ago ↶
227
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
228
let Some ( job) = jobs.into_iter ( ) .find ( | j | j.name = = name ) else { return Err ( format! ( " run #{} has no job {name} " , run.number) ) } ;
47e37402 Andy McCall 21 days ago ↶
229
let approval = may_decide ( app, row, run, & job, who) .await? ;
4a68f106 Andy McCall 19 days ago ↶
230
231
232
233
if let ( Some ( agent) , None ) = ( via, seal) {
234
if ! row.agents_decide_gates {
235
return Err ( format! (
236
" {name} is decided by a person here, not through an agent's token ({agent} ): decide it yourself on the run page, or a maintainer can let agents decide approval gates in Settings › OxygenFlow"
237
) ) ;
238
}
239
}
47e37402 Andy McCall 21 days ago ↶
240
if approve & & approval.sealed & & seal.is_none ( ) {
241
return Err ( format! (
242
" {name} takes an approval signed with your key: in a checkout of {} /{} , run oxy seal --flow {} /{} /{} /flow/{} --job {name} " ,
243
row.owner,
244
row.name,
245
app.config.base_url ( ) ,
246
row.owner,
247
row.name,
248
run.number
249
) ) ;
250
}
417184c7 Andy McCall 22 days ago ↶
251
let target = format! ( " {} /{} #{} {name} " , row.owner, row.name, run.number) ;
47e37402 Andy McCall 21 days ago ↶
252
let sealed = seal.map ( | r | format! ( " , sealed (approval {} )" , & r[ .. 12. min ( r.len ( ) ) ] ) ) .unwrap_or_default ( ) ;
05f1a85a Andy McCall 20 days ago ↶
253
254
let by = match via {
255
Some ( v) => format! ( " {} via {v} " , who.username) ,
256
None => who.username.clone ( ) ,
257
} ;
258
let detail = [ seal.unwrap_or ( " " ) , & via.map ( | v | format! ( " via {v} " ) ) .unwrap_or_default ( ) ]
259
.iter ( )
260
.filter ( | s | ! s.is_empty ( ) )
261
.cloned ( )
262
.collect:: < Vec< _ > > ( )
263
.join ( " , " ) ;
417184c7 Andy McCall 22 days ago ↶
264
let said = if approve {
05f1a85a Andy McCall 20 days ago ↶
265
if ! fdb:: approve_job( & app.db, job.id, & who.username, seal.unwrap_or ( " " ) , via.unwrap_or ( " " ) ) .await.map_err ( | e | e.to_string ( ) ) ? {
417184c7 Andy McCall 22 days ago ↶
266
return Err ( format! ( " {name} was decided on meanwhile" ) ) ;
267
}
05f1a85a Andy McCall 20 days ago ↶
268
db:: accounts:: audit( & app.db, Some ( who) , " flow.approved" , & target, & detail) .await.ok ( ) ;
269
format! ( " approved by {by} {sealed} " )
417184c7 Andy McCall 22 days ago ↶
270
} else {
05f1a85a Andy McCall 20 days ago ↶
271
fdb:: note_rejection( & app.db, job.id, & who.username, via.unwrap_or ( " " ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
272
let why = format! ( " rejected by {by} {sealed} " ) ;
417184c7 Andy McCall 22 days ago ↶
273
let changed =
274
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " cancelled" , exit_code: None , error: & why, receipt: " " , receipt_hash: " " } )
275
.await
276
.map_err ( | e | e.to_string ( ) ) ? ;
277
report_changed ( app, run.id, changed) .await.ok ( ) ;
05f1a85a Andy McCall 20 days ago ↶
278
db:: accounts:: audit( & app.db, Some ( who) , " flow.rejected" , & target, & detail) .await.ok ( ) ;
417184c7 Andy McCall 22 days ago ↶
279
why
280
} ;
281
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said.clone ( ) } ;
282
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
283
Ok ( format! ( " {name} : {said} " ) )
284
}
285
dc59d1b5 Andy McCall 21 days ago ↶
286
287
288
289
async fn tell_approvers ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
290
const TOLD : & str = " waiting for approval; the people who may approve were told" ;
291
let Ok ( jobs) = fdb:: jobs( & app.db, run.id) .await else { return } ;
292
for job in jobs.iter ( ) .filter ( | j | j.status = = " queued" & & j.approval = = " waiting" ) {
293
if ! job.depends_on ( ) .iter ( ) .all ( | d | jobs.iter ( ) .any ( | j | & j.name = = d & & j.status = = " passed" ) ) {
294
continue ;
295
}
296
let already = fdb:: log( & app.db, job.id, 0 , 50 ) .await.unwrap_or_default ( ) .iter ( ) .any ( | l | l.text = = TOLD ) ;
297
if already {
298
continue ;
299
}
300
let mut targets = Vec :: new( ) ;
301
for a in db:: accounts:: accounts( & app.db) .await.unwrap_or_default ( ) .iter ( ) .filter ( | a | ! a.blocked ( ) ) {
302
if may_decide ( app, row, run, job, a) .await.is_ok ( ) {
303
targets.push ( ( a.id, db:: notify:: Reason:: Participating) ) ;
304
}
305
}
306
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: TOLD .into ( ) } ;
307
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
308
let title = format! ( " {} /{} run #{} : {} waits for your approval" , row.owner, row.name, run.number, job.name) ;
309
let url = format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ;
310
crate :: notify:: tell( app, " flow.approval" , & title, & url, & run.actor, Some ( row.id) , targets) .await;
311
}
47e37402 Andy McCall 21 days ago ↶
312
}
313
314
315
316
317
pub async fn approval_statement (
318
app : & Shared,
319
row : & db:: repos:: RepoRow,
320
run : & fdb:: Run,
321
name : & str ,
322
) -> Result< ( oxy_core:: approval:: Statement, fdb:: Job) , String> {
323
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
324
let Some ( job) = jobs.into_iter ( ) .find ( | j | j.name = = name ) else { return Err ( format! ( " run #{} has no job {name} " , run.number) ) } ;
325
let spec: flow_core:: Job = serde_json:: from_str( & job.spec) .map_err ( | e | format! ( " stored job: {e} " ) ) ? ;
326
let sandbox: flow_core:: Sandbox = serde_json:: from_str( & job.sandbox) .map_err ( | e | format! ( " stored sandbox: {e} " ) ) ? ;
327
let repo_name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
328
let repo = app.repos.read ( & repo_name, | r , _| Ok ( r.id ( ) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
329
let state = oxy_core:: hash:: ObjectRef:: from_hex( & run.state) .ok_or ( " the run has no state" ) ? ;
330
let inputs = artifact_inputs ( app, & job, & spec) .await? .iter ( ) .filter_map ( | i | oxy_core:: hash:: ObjectRef:: from_hex( & i.blob) ) .collect ( ) ;
331
let statement = oxy_core:: approval:: Statement {
332
repo,
333
run: format! ( " {} /{} /{} /flow/{} " , app.config.base_url ( ) , row.owner, row.name, run.number) ,
334
job: name.to_string ( ) ,
335
job_hash: oxy_core:: hash:: ObjectRef( spec.hash ( & sandbox) ) ,
336
state,
337
inputs,
338
environment: spec.environment.clone ( ) .unwrap_or_default ( ) ,
339
approve: true ,
340
} ;
341
Ok ( ( statement, job) )
342
}
343
344
345
346
347
348
pub async fn decide_sealed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run, name : & str , signed_hex : & str ) -> Result< String, String> {
349
let bytes = hex:: decode( signed_hex.trim ( ) ) .map_err ( | _| " the signed approval is not hex" .to_string ( ) ) ? ;
350
let sa = oxy_core:: approval:: SignedApproval:: decode( & bytes) .map_err ( | e | format! ( " not a signed approval: {e} " ) ) ? ;
351
sa.verify ( ) .map_err ( | e | format! ( " it does not verify: {e} " ) ) ? ;
352
let ( mut expected, _ ) = approval_statement ( app, row, run, name) .await? ;
353
let given = sa.statement ( ) .map_err ( | e | e.to_string ( ) ) ? ;
354
expected.approve = given.approve;
355
if ! sa.covers ( & expected) {
356
return Err ( format! (
357
" it approves something else than job {name} of run #{} as it is now (another run, state, plan or environment): sign again" ,
358
run.number
359
) ) ;
360
}
361
let key = sa.signer.hash ( ) .to_hex ( ) ;
362
let links = db:: accounts:: identity_accounts( & app.db) .await.map_err ( | e | e.to_string ( ) ) ? ;
363
let Some ( ( _ , username) ) = links.iter ( ) .find ( | ( k , _) | * k = = key) else {
364
return Err ( format! (
365
" key {} is not linked to an account here (link it under Settings › Keys; an agent's delegated key cannot approve)" ,
366
& key[ .. 12 ]
367
) ) ;
368
} ;
369
let who = db:: accounts:: account_by_name( & app.db, username) .await.map_err ( | e | e.to_string ( ) ) ? .ok_or ( " the key's account is gone" ) ? ;
370
let repo_name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
371
let jobs = fdb:: jobs( & app.db, run.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
372
let job = jobs.iter ( ) .find ( | j | j.name = = name ) .ok_or ( " no such job" ) ? ;
373
may_decide ( app, row, run, job, & who) .await? ;
965b950f Andy McCall 20 days ago ↶
374
let ( rid, signers) = ( sa.attest.repo, sa.sigs.iter ( ) .map ( | s | s.key ) .collect:: < Vec< _ > > ( ) ) ;
47e37402 Andy McCall 21 days ago ↶
375
let kept = app.repos.write ( & repo_name, move | r, _ | Ok ( r.add_approval ( & sa) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
965b950f Andy McCall 20 days ago ↶
376
crate :: tlog:: log( app, vec! [ crate :: tlog:: Pending { kind: " approval" , repo: rid, repo_row: Some ( row.id) , object: kept, signers } ] ) .await;
05f1a85a Andy McCall 20 days ago ↶
377
decide_with ( app, row, run, name, & who, given.approve, Through { seal: Some ( & kept.to_hex ( ) ) , via: None } ) .await
dc59d1b5 Andy McCall 21 days ago ↶
378
}
379
417184c7 Andy McCall 22 days ago ↶
380
381
382
pub async fn expire_approvals ( app : & Shared, now : i64 ) -> sqlx:: Result< usize > {
383
let mut ended = 0 ;
384
for job in fdb:: waiting_for_approval( & app.db) .await? {
385
let Ok ( spec) = serde_json:: from_str:: < flow_core:: Job> ( & job.spec) else { continue } ;
386
let Some ( approval) = spec.approval else { continue } ;
387
let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? else { continue } ;
388
let jobs = fdb:: jobs( & app.db, run.id) .await? ;
389
let wanted = job.depends_on ( ) ;
390
let deps: Vec< & fdb:: Job> = jobs.iter ( ) .filter ( | j | wanted.contains ( & j.name) ) .collect ( ) ;
391
if deps.iter ( ) .any ( | d | d.status ! = " passed" ) {
392
continue ;
393
}
394
let since = deps.iter ( ) .filter_map ( | d | d.finished_ms ) .max ( ) .unwrap_or ( run.created_ms) ;
395
if now - since < approval.expires_s as i64 * 1000 {
396
continue ;
397
}
398
let hours = approval.expires_s as f64 / 3600. 0 ;
399
let why =
400
format! ( " no approval within {} " , if hours > = 1. 0 { format! ( " {hours:.0} h" ) } else { format! ( " {} m" , approval.expires_s / 60 ) } ) ;
401
let changed =
402
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " cancelled" , exit_code: None , error: & why, receipt: " " , receipt_hash: " " } )
403
.await? ;
404
report_changed ( app, run.id, changed) .await? ;
405
ended + = 1 ;
406
}
407
Ok ( ended)
408
}
409
745d225d Andy McCall 1 month ago ↶
410
411
412
async fn supersede ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , number : i64 ) {
413
let Ok ( older) = fdb:: live_push_runs_before( & app.db, row.id, bookmark, number) .await else { return } ;
414
for run in older {
415
if let Err ( e) = cancel_with ( app, & run, & format! ( " superseded by run #{number} " ) ) .await {
416
tracing:: warn! ( " flow: superseding run #{} of {}/{}: {e}" , run.number, row.owner, row.name) ;
417
}
418
}
419
}
420
c889911b Andy McCall 1 month ago ↶
421
422
423
fn event_of < 'a > ( trigger : & 'a str , bookmark : & 'a str ) -> flow_core:: pipeline:: Event< 'a > {
424
use flow_core:: pipeline:: Event;
425
let t = match trigger.strip_prefix ( " rerun:" ) {
426
Some ( rest) => rest.split_once ( ' :' ) .map ( | ( _, e ) | e ) .unwrap_or ( " manual" ) ,
427
None => trigger,
428
} ;
429
if let Some ( tag) = t.strip_prefix ( " tag:" ) {
430
Event:: Tag( tag)
431
} else if t = = " push" {
432
Event:: Push( bookmark)
433
} else if t = = " schedule" {
434
Event:: Schedule
435
} else {
436
Event:: Manual
437
}
1edac4b6 Andy McCall 1 month ago ↶
438
}
439
c889911b Andy McCall 1 month ago ↶
440
441
fn original_event ( run : & fdb:: Run) -> String {
442
let t = run.trigger.as_str ( ) ;
443
match t.strip_prefix ( " rerun:" ) {
444
Some ( rest) => rest.split_once ( ' :' ) .map ( | ( _, e ) | e.to_string ( ) ) .unwrap_or_else ( | | " manual" .into ( ) ) ,
445
None => t.to_string ( ) ,
446
}
447
}
448
d7ce41ac Andy McCall 1 month ago ↶
449
450
451
452
453
async fn touched_since_last_run ( app : & Shared, row : & db:: repos:: RepoRow, bookmark : & str , heads : & [ChangeHash]) -> Option< Vec< String> > {
454
let last = fdb:: latest_on( & app.db, row.id, bookmark) .await.ok ( ) ? ? ;
455
let old: Vec< ChangeHash> = last.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
456
if old.is_empty ( ) {
457
return None ;
458
}
459
let repo = RepoName:: new( & row.owner, & row.name) ? ;
460
let new = heads.to_vec ( ) ;
461
app.repos
462
.read ( & repo, move | r, cache| {
463
let fresh = r.negotiate ( & old, & new) ? ;
464
let loaded = cache.load ( r, & new) ? ;
465
let by_node: std:: collections:: HashMap< oxy_core:: ids:: NodeId, & String> =
466
loaded.snapshot.files.iter ( ) .map ( | ( p , f ) | ( f.node, p) ) .collect ( ) ;
467
let mut paths = std:: collections:: BTreeSet:: new( ) ;
468
for h in fresh {
469
let ( c, _ ) = r.store ( ) .get_change_lazy ( & h) ? ;
470
let nodes = c.edits.iter ( ) .map ( | e | e.node ( ) ) .chain ( c.tree_ops.iter ( ) .map ( | o | o.node ( ) ) ) ;
471
for n in nodes {
472
if let Some ( p) = by_node.get ( & n) {
473
paths.insert ( ( * p) .clone ( ) ) ;
474
}
475
}
476
}
477
Ok ( paths.into_iter ( ) .collect:: < Vec< String> > ( ) )
478
} )
479
.await
480
.ok ( )
481
}
482
483
484
485
fn in_scope ( scope : & [String], touched : Option< & [ String] > ) -> bool {
486
let Some ( touched) = touched else { return true } ;
487
if scope.is_empty ( ) {
488
return true ;
489
}
490
let mut b = globset:: GlobSetBuilder:: new( ) ;
491
for g in scope {
492
match globset:: Glob:: new( g) {
493
Ok ( g) => {
494
b.add ( g) ;
495
}
496
Err ( _ ) => return true ,
497
}
498
}
499
let Ok ( set) = b.build ( ) else { return true } ;
500
touched.iter ( ) .any ( | p | set.is_match ( p) )
501
}
502
503
504
505
# [ allow ( clippy::too_many_arguments ) ]
506
pub async fn start_run_scoped (
507
app : & Shared,
508
row : & db:: repos:: RepoRow,
509
bookmark : & str ,
510
state : & str ,
511
heads : & [ChangeHash],
512
actor : & str ,
513
trigger : & str ,
514
touched : Option< & [ String] > ,
515
) -> Result< Option< i64 > , String> {
516
start_run_with ( app, row, bookmark, state, heads, actor, trigger, touched) .await
517
}
518
1edac4b6 Andy McCall 1 month ago ↶
519
520
521
522
523
524
525
pub async fn start_run (
526
app : & Shared,
527
row : & db:: repos:: RepoRow,
528
bookmark : & str ,
529
state : & str ,
530
heads : & [ChangeHash],
531
actor : & str ,
532
trigger : & str ,
533
) -> Result< Option< i64 > , String> {
d7ce41ac Andy McCall 1 month ago ↶
534
start_run_with ( app, row, bookmark, state, heads, actor, trigger, None ) .await
535
}
536
537
# [ allow ( clippy::too_many_arguments ) ]
538
async fn start_run_with (
539
app : & Shared,
540
row : & db:: repos:: RepoRow,
541
bookmark : & str ,
542
state : & str ,
543
heads : & [ChangeHash],
544
actor : & str ,
545
trigger : & str ,
546
touched : Option< & [ String] > ,
547
) -> Result< Option< i64 > , String> {
0650d94e Andy McCall 26 days ago ↶
548
start_run_at ( app, row, bookmark, state, heads, actor, trigger, touched, None , & [ ] ) .await
348f8c4b Andy McCall 1 month ago ↶
549
}
550
0650d94e Andy McCall 26 days ago ↶
551
552
348f8c4b Andy McCall 1 month ago ↶
553
# [ allow ( clippy::too_many_arguments ) ]
554
async fn start_run_at (
555
app : & Shared,
556
row : & db:: repos:: RepoRow,
557
bookmark : & str ,
558
state : & str ,
559
heads : & [ChangeHash],
560
actor : & str ,
561
trigger : & str ,
562
touched : Option< & [ String] > ,
563
at : Option< flow_core:: pipeline:: When> ,
0650d94e Andy McCall 26 days ago ↶
564
only : & [String],
348f8c4b Andy McCall 1 month ago ↶
565
) -> Result< Option< i64 > , String> {
1edac4b6 Andy McCall 1 month ago ↶
566
let repo = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
42fa9554 Andy McCall 1 month ago ↶
567
let h = heads.to_vec ( ) ;
568
let text = app
569
.repos
570
.read ( & repo, move | r, cache| {
571
let loaded = cache.load ( r, & h) ? ;
572
Ok ( match loaded.snapshot.files.get ( PIPELINE_PATH ) .map ( | f | & f.content) {
573
Some ( oxy_core:: state:: FileContent:: Text { bytes, .. } ) => Some ( String :: from_utf8_lossy( bytes) .into_owned ( ) ) ,
574
_ => None ,
575
} )
576
} )
1edac4b6 Andy McCall 1 month ago ↶
577
.await
578
.map_err ( | e | format! ( " reading the pipeline: {e} " ) ) ? ;
579
let Some ( text) = text else { return Ok ( None ) } ;
42fa9554 Andy McCall 1 month ago ↶
580
let mut hex: Vec< String> = heads.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ;
581
hex.sort ( ) ;
582
let heads_s = hex.join ( " +" ) ;
1edac4b6 Andy McCall 1 month ago ↶
583
let base = db:: flow:: NewRun { repo_id: row.id, bookmark, state, heads: & heads_s, pipeline_hash: " " , actor, trigger, error: None } ;
584
let id = match Pipeline:: parse( & text) {
42fa9554 Andy McCall 1 month ago ↶
585
Err ( problems) => {
586
let msg = problems.to_string ( ) ;
1edac4b6 Andy McCall 1 month ago ↶
587
db:: flow:: create_run( & app.db, db:: flow:: NewRun { error: Some ( & msg) , .. base } , & [ ] ) .await.map_err ( | e | e.to_string ( ) ) ?
42fa9554 Andy McCall 1 month ago ↶
588
}
589
Ok ( p) => {
590
let depths = p.depths ( ) ;
d7ce41ac Andy McCall 1 month ago ↶
591
592
c889911b Andy McCall 1 month ago ↶
593
594
348f8c4b Andy McCall 1 month ago ↶
595
let event = match at {
596
Some ( w) => flow_core:: pipeline:: Event:: ScheduleAt( w) ,
597
None => event_of ( trigger, bookmark) ,
598
} ;
0650d94e Andy McCall 26 days ago ↶
599
let starts = | j : & flow_core:: Job| p.on_of ( j) .matches ( event) & & in_scope ( & j.scope, touched) ;
600
let mut keep: std:: collections:: HashSet< String> = if only.is_empty ( ) {
601
p.order ( ) .into_iter ( ) .filter ( | j | starts ( j) ) .map ( | j | j.name.clone ( ) ) .collect ( )
602
} else {
603
604
let startable: Vec< & str > = p.order ( ) .into_iter ( ) .filter ( | j | starts ( j) ) .map ( | j | j.name.as_str ( ) ) .collect ( ) ;
605
for name in only {
606
if ! startable.contains ( & name.as_str ( ) ) {
607
let what = if p.jobs.contains_key ( name) {
608
" is not started by hand (no `on { manual }`)"
609
} else {
610
" is not in the pipeline"
611
} ;
612
return Err ( format! ( " job {name:?} {what} ; jobs a manual run can start: {} " , startable.join ( " , " ) ) ) ;
613
}
614
}
615
only.iter ( ) .cloned ( ) .collect ( )
616
} ;
d7ce41ac Andy McCall 1 month ago ↶
617
loop {
618
let needed: Vec< String> = p
619
.order ( )
620
.into_iter ( )
621
.filter ( | j | keep.contains ( & j.name) )
622
.flat_map ( | j | j.depends_on.iter ( ) .cloned ( ) )
623
.filter ( | d | ! keep.contains ( d) )
624
.collect ( ) ;
625
if needed.is_empty ( ) {
626
break ;
627
}
628
keep.extend ( needed) ;
629
}
44a3d2eb Andy McCall 26 days ago ↶
630
let mut jobs: Vec< fdb:: NewJob> = p
42fa9554 Andy McCall 1 month ago ↶
631
.order ( )
632
.into_iter ( )
d7ce41ac Andy McCall 1 month ago ↶
633
.filter ( | j | keep.contains ( & j.name) )
42fa9554 Andy McCall 1 month ago ↶
634
.map ( | j | {
635
let sb = & p.sandboxes[ & j.sandbox] ;
636
fdb:: NewJob {
637
name: j.name.clone ( ) ,
638
depth: depths[ j.name.as_str ( ) ] as i64 ,
639
depends: j.depends_on.clone ( ) ,
640
spec: serde_json:: to_string( j) .unwrap_or_default ( ) ,
641
sandbox: serde_json:: to_string( sb) .unwrap_or_default ( ) ,
642
runtime: sb.runtime.as_str ( ) .into ( ) ,
44a3d2eb Andy McCall 26 days ago ↶
643
need_disk_mib: None ,
42fa9554 Andy McCall 1 month ago ↶
644
}
645
} )
646
.collect ( ) ;
44a3d2eb Andy McCall 26 days ago ↶
647
648
for j in & mut jobs {
649
j.need_disk_mib = fdb:: learned_disk_need( & app.db, row.id, & j.name) .await.map_err ( | e | e.to_string ( ) ) ? ;
650
}
c889911b Andy McCall 1 month ago ↶
651
if jobs.is_empty ( ) {
652
d7ce41ac Andy McCall 1 month ago ↶
653
return Ok ( None ) ;
654
}
42fa9554 Andy McCall 1 month ago ↶
655
let hash = hex:: encode( p.hash ( ) ) ;
1edac4b6 Andy McCall 1 month ago ↶
656
let id =
657
db:: flow:: create_run( & app.db, db:: flow:: NewRun { pipeline_hash: & hash, .. base } , & jobs) .await.map_err ( | e | e.to_string ( ) ) ? ;
658
if let Ok ( Some ( run) ) = fdb:: run_by_id( & app.db, id) .await {
42fa9554 Andy McCall 1 month ago ↶
659
for j in & jobs {
a0d4bbfb Andy McCall 1 month ago ↶
660
report_check ( app, row, state, run.number, & j.name, " queued" , " waiting for a runner" ) .await;
42fa9554 Andy McCall 1 month ago ↶
661
}
1c8276b9 Andy McCall 26 days ago ↶
662
images_check ( app, row, state, run.number, & jobs) .await;
42fa9554 Andy McCall 1 month ago ↶
663
}
664
id
665
}
666
} ;
1edac4b6 Andy McCall 1 month ago ↶
667
let run = fdb:: run_by_id( & app.db, id) .await.map_err ( | e | e.to_string ( ) ) ? .ok_or ( " the run vanished" ) ? ;
668
Ok ( Some ( run.number) )
42fa9554 Andy McCall 1 month ago ↶
669
}
670
1c8276b9 Andy McCall 26 days ago ↶
671
672
673
674
675
async fn images_check ( app : & Shared, row : & db:: repos:: RepoRow, state : & str , run_number : i64 , jobs : & [fdb:: NewJob]) {
676
let mut pinned: Vec< String> = jobs
677
.iter ( )
678
.filter_map ( | j | serde_json:: from_str:: < serde_json:: Value> ( & j.sandbox) .ok ( ) )
679
.filter_map ( | v | v[ " rootfs" ] .as_str ( ) .map ( String :: from) )
680
.filter ( | r | ! r.is_empty ( ) )
681
.collect ( ) ;
682
pinned.sort ( ) ;
683
pinned.dedup ( ) ;
684
if pinned.is_empty ( ) {
685
return ;
686
}
687
let mut held = crate :: images:: held( & app.db) .await.unwrap_or_default ( ) ;
688
689
690
let runners: Vec< db:: runners:: Runner> =
691
db:: runners:: list( & app.db) .await.unwrap_or_default ( ) .into_iter ( ) .filter ( | r | r.state ! = " disabled" ) .collect ( ) ;
692
if runners.is_empty ( ) | | runners.iter ( ) .any ( | r | r.image_list ( ) .is_none ( ) ) {
693
return ;
694
}
695
for r in & runners {
696
held.extend ( r.image_list ( ) .unwrap_or_default ( ) ) ;
697
}
698
let missing: Vec< & String> = pinned.iter ( ) .filter ( | h | ! held.contains ( * h) ) .collect ( ) ;
699
700
701
let failing_before = db:: hooks:: checks_for_state( & app.db, row.id, state)
702
.await
703
.unwrap_or_default ( )
704
.iter ( )
705
.any ( | c | c.name = = " pipeline/images" & & c.status ! = " passed" ) ;
706
if missing.is_empty ( ) & & ! failing_before {
707
return ;
708
}
709
let ( status, summary) = if missing.is_empty ( ) {
710
(
711
" passed" ,
712
format! ( " {} now held" , if pinned.len ( ) = = 1 { " its image is" .to_string ( ) } else { format! ( " all {} images are" , pinned.len ( ) ) } ) ,
713
)
714
} else {
715
let names: Vec< String> = missing.iter ( ) .map ( | h | h[ .. 12. min ( h.len ( ) ) ] .to_string ( ) ) .collect ( ) ;
716
( " failed" , format! ( " neither the hub nor a runner holds image {} : send it with flow rootfs push HASH" , names.join ( " , " ) ) )
717
} ;
718
let url = format! ( " {} /{} /{} /flow/{run_number} " , app.config.base_url ( ) , row.owner, row.name) ;
719
if let Ok ( c) = db:: hooks:: upsert_check(
720
& app.db,
721
row.id,
722
db:: hooks:: CheckReport { state, name: " pipeline/images" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
723
)
724
.await
725
{
726
let api = crate :: api:: check_of( & c) ;
727
app.events.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
728
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
729
}
730
}
731
a0d4bbfb Andy McCall 1 month ago ↶
732
733
async fn report_check ( app : & Shared, row : & db:: repos:: RepoRow, state : & str , run_number : i64 , job : & str , status : & str , summary : & str ) {
734
let url = format! ( " {} /{} /{} /flow/{run_number} " , app.config.base_url ( ) , row.owner, row.name) ;
42fa9554 Andy McCall 1 month ago ↶
735
let name = format! ( " flow/{job} " ) ;
736
let c = db:: hooks:: upsert_check(
737
& app.db,
738
row.id,
739
db:: hooks:: CheckReport { state, name: & name, status, summary, url: & url, reporter: None , seal: " " } ,
740
)
741
.await;
742
if let Ok ( c) = c {
743
let api = crate :: api:: check_of( & c) ;
744
app.events.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
745
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
746
}
747
}
748
749
750
fn check_status ( job_status : & str ) -> & 'static str {
751
match job_status {
752
" queued" => " queued" ,
753
" running" => " running" ,
754
" passed" => " passed" ,
755
" skipped" => " skipped" ,
745d225d Andy McCall 1 month ago ↶
756
" cancelled" => " cancelled" ,
42fa9554 Andy McCall 1 month ago ↶
757
_ => " failed" ,
758
}
759
}
760
b6b7e4cf Andy McCall 1 month ago ↶
761
762
763
92a797b2 Andy McCall 26 days ago ↶
764
pub ( crate ) async fn runner_auth ( app : & Shared, headers : & HeaderMap) -> Result< db:: runners:: Runner, ApiError> {
42fa9554 Andy McCall 1 month ago ↶
765
let Some ( t) = oxy_server:: bearer_token( headers) else {
766
return Err ( ApiError( StatusCode:: UNAUTHORIZED , " a runner token is required" .into ( ) ) ) ;
b6b7e4cf Andy McCall 1 month ago ↶
767
} ;
768
let hash = blake3:: hash( t.as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
769
let runner = match db:: runners:: by_token( & app.db, & hash) .await? {
770
Some ( r) => r,
771
None if app.config.flow.runner_tokens.iter ( ) .any ( | x | crate :: auth:: mac_equal( x, t) ) => {
772
db:: runners:: configured( & app.db, & hash, " runner" ) .await?
773
}
774
None => return Err ( ApiError( StatusCode:: UNAUTHORIZED , " not a runner token" .into ( ) ) ) ,
42fa9554 Andy McCall 1 month ago ↶
775
} ;
b6b7e4cf Andy McCall 1 month ago ↶
776
if runner.state = = " disabled" {
777
return Err ( ApiError( StatusCode:: FORBIDDEN , format! ( " runner {} is disabled on this hub" , runner.name) ) ) ;
42fa9554 Andy McCall 1 month ago ↶
778
}
b6b7e4cf Andy McCall 1 month ago ↶
779
Ok ( runner)
42fa9554 Andy McCall 1 month ago ↶
780
}
781
b6b7e4cf Andy McCall 1 month ago ↶
782
783
784
785
pub async fn register (
786
State( app ) : State< Shared> ,
787
Json( req ) : Json< flow_core:: api:: Register> ,
788
) -> Result< Json< flow_core:: api:: Registered> , ApiError> {
789
let bytes = hex:: decode( req.identity.trim ( ) ) .map_err ( | _| ApiError( StatusCode:: BAD_REQUEST , " identity: not hex" .into ( ) ) ) ? ;
790
let identity = oxy_core:: identity:: Identity:: decode( & bytes) .map_err ( | e | ApiError( StatusCode:: BAD_REQUEST , format! ( " identity: {e} " ) ) ) ? ;
791
let name: String = req.name.chars ( ) .filter ( | c | c.is_ascii_alphanumeric ( ) | | matches! ( c, ' -' | ' _' | ' .' ) ) .take ( 64 ) .collect ( ) ;
792
if name.is_empty ( ) {
793
return Err ( ApiError( StatusCode:: BAD_REQUEST , " give the runner a name of letters, digits, - _ or ." .into ( ) ) ) ;
794
}
795
let credential = format! ( " flowrunner_{} " , crate :: auth:: random_hex( 32 ) ) ;
796
let hash = blake3:: hash( credential.as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
797
let reg = blake3:: hash( req.token.trim ( ) .as_bytes ( ) ) .to_hex ( ) .to_string ( ) ;
798
let runner =
799
db:: runners:: register( & app.db, & reg, & name, & hash, & identity.hash ( ) .to_hex ( ) , & identity.name, " " ) .await? .ok_or_else ( | | {
800
ApiError( StatusCode:: FORBIDDEN , " that registration token is unknown, used or expired; create one on Admin → Runners" .into ( ) )
801
} ) ? ;
802
db:: runners:: seen( & app.db, runner.id, None , Some ( & req.info) ) .await? ;
803
tracing:: info! ( " flow: runner {} registered (key {})" , runner.name, & runner.key[ .. 12 ] ) ;
804
Ok ( Json( flow_core:: api:: Registered { id: runner.id, name: runner.name, credential } ) )
805
}
806
42fa9554 Andy McCall 1 month ago ↶
807
808
pub async fn claim ( State( app ) : State< Shared> , headers : HeaderMap, Json( req ) : Json< ClaimRequest> ) -> Result< Response, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
809
let me = runner_auth ( & app, & headers) .await? ;
810
let claimed: String = req.runner.chars ( ) .filter ( | c | ! c.is_control ( ) ) .take ( 64 ) .collect ( ) ;
811
db:: runners:: seen( & app.db, me.id, Some ( & claimed) , req.info.as_ref ( ) ) .await? ;
eef677c4 Andy McCall 1 month ago ↶
812
if let Some ( holding) = & req.holding {
813
end_jobs_not_held ( & app, & me, holding) .await? ;
814
}
b6b7e4cf Andy McCall 1 month ago ↶
815
816
if me.state = = " draining" {
817
return Ok ( StatusCode:: NO_CONTENT .into_response ( ) ) ;
818
}
819
let runner = if me.source = = " registered" { me.name.clone ( ) } else { claimed } ;
42fa9554 Andy McCall 1 month ago ↶
820
let secret = format! ( " {JOB_TOKEN_PREFIX} {} " , crate :: auth:: random_hex( 32 ) ) ;
efd29a00 Andy McCall 1 month ago ↶
821
822
823
if let Some ( archs) = & req.image_archs {
824
fdb:: record_image_archs( & app.db, archs) .await? ;
825
}
826
let arch = req.info.as_ref ( ) .map ( | i | i.arch.clone ( ) ) .filter ( | a | ! a.is_empty ( ) ) .unwrap_or_else ( | | me.arch.clone ( ) ) ;
4d2e44d7 Andy McCall 1 month ago ↶
827
828
829
let profile = profile_of ( & req, & arch) ;
92a797b2 Andy McCall 26 days ago ↶
830
db:: runners:: set_offer( & app.db, me.id, & req.runtimes, req.images.as_deref ( ) , req.fetches_images, & profile) .await? ;
2fb70d01 Andy McCall 26 days ago ↶
831
if let Some ( f) = & req.facts {
832
db:: runners:: set_facts( & app.db, me.id, f) .await? ;
833
}
a46d15e7 Andy McCall 25 days ago ↶
834
app.swarm.claimed ( me.id, req.swarm.as_ref ( ) ) ;
c2be148c Andy McCall 24 days ago ↶
835
836
4fa0da12 Andy McCall 24 days ago ↶
837
if let Some ( c) = & req.caches {
c2be148c Andy McCall 24 days ago ↶
838
let kept = | slot : & str | req.cache_times.as_ref ( ) .and_then ( | t | t.get ( slot) ) .map_or ( 0 , | ms | * ms as i64 ) ;
4fa0da12 Andy McCall 24 days ago ↶
839
CACHES
840
.lock ( )
841
.unwrap_or_else ( | p | p.into_inner ( ) )
842
.get_or_insert_with ( Default :: default)
c2be148c Andy McCall 24 days ago ↶
843
.insert ( ( app.repos.root ( ) .display ( ) .to_string ( ) , me.id) , c.iter ( ) .map ( | s | ( s.clone ( ) , kept ( s) ) ) .collect ( ) ) ;
4fa0da12 Andy McCall 24 days ago ↶
844
}
b0612e7d Andy McCall 1 month ago ↶
845
let room = fdb:: Room {
4d2e44d7 Andy McCall 1 month ago ↶
846
declined: fdb:: declined_by( & app.db, me.id, & profile) .await? ,
b0612e7d Andy McCall 1 month ago ↶
847
memory_mib: req.free_memory_mib,
848
cpus: req.free_cpus,
849
images: req.images.as_ref ( ) .map ( | i | i.iter ( ) .map ( | h | h.to_ascii_lowercase ( ) ) .collect ( ) ) ,
efd29a00 Andy McCall 1 month ago ↶
850
arch,
851
image_archs: fdb:: image_archs( & app.db) .await? ,
92a797b2 Andy McCall 26 days ago ↶
852
fetchable: if req.fetches_images { crate :: images:: held( & app.db) .await? } else { Default :: default( ) } ,
44a3d2eb Andy McCall 26 days ago ↶
853
free_disk_mib: req.facts.as_ref ( ) .and_then ( | f | f.free_disk_mib ) ,
b0612e7d Andy McCall 1 month ago ↶
854
} ;
7f5f6c05 Andy McCall 1 month ago ↶
855
856
857
let mut labels: Vec< String> = match & req.info {
c4c87f48 Andy McCall 1 month ago ↶
858
Some ( i) => i.labels.clone ( ) ,
7f5f6c05 Andy McCall 1 month ago ↶
859
None => me.reported_label_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ,
c4c87f48 Andy McCall 1 month ago ↶
860
} ;
7f5f6c05 Andy McCall 1 month ago ↶
861
for l in me.admin_label_list ( ) {
862
if ! labels.iter ( ) .any ( | x | x = = l ) {
863
labels.push ( l.to_string ( ) ) ;
864
}
865
}
501d8b26 Andy McCall 26 days ago ↶
866
867
868
let runners = db:: runners:: list( & app.db) .await? ;
869
let me_now = runners.iter ( ) .find ( | r | r.id = = me.id ) .cloned ( ) .unwrap_or_else ( | | me.clone ( ) ) ;
870
let running: std:: collections:: HashMap< i64 , i64 > = db:: runners:: running( & app.db) .await? .into_iter ( ) .collect ( ) ;
4fa0da12 Andy McCall 24 days ago ↶
871
let queue = fdb:: queue( & app.db) .await? ;
872
let owners: std:: collections:: HashMap< i64 , ( String, i64 ) > = queue.iter ( ) .map ( | q | ( q.job_id, ( q.owner.clone ( ) , q.repo_id) ) ) .collect ( ) ;
873
874
let slots: std:: collections:: HashMap< i64 , String> =
875
queue.iter ( ) .map ( | q | ( q.job_id, flow_core:: api:: cache_slot( & format! ( " {} /{} " , q.owner, q.name) , & q.job) ) ) .collect ( ) ;
501d8b26 Andy McCall 26 days ago ↶
876
let ( archs, held) = ( fdb:: image_archs( & app.db) .await? , crate :: images:: held( & app.db) .await? ) ;
877
let now = db:: now_ms( ) ;
878
let fleet = Fleet { image_archs: & archs, hub_images: & held, now } ;
c95446a1 Andy McCall 26 days ago ↶
879
let hub_key = app.repos.root ( ) .display ( ) .to_string ( ) ;
501d8b26 Andy McCall 26 days ago ↶
880
let better = | j : & fdb:: Job| -> bool {
881
let Some ( ( owner, repo_id) ) = owners.get ( & j.id) else { return false } ;
882
let rootfs = rootfs_of ( j) ;
883
let mut holds = HELD .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) ;
884
let holds = holds.get_or_insert_with ( Default :: default) ;
885
holds.retain ( | _, t | now - * t < 600_000 ) ;
c95446a1 Andy McCall 26 days ago ↶
886
if holds.get ( & ( hub_key.clone ( ) , j.id) ) .is_some_and ( | t | now - * t > = BETTER_HOLD_MS ) {
501d8b26 Andy McCall 26 days ago ↶
887
return false ;
888
}
4fa0da12 Andy McCall 24 days ago ↶
889
let slot = slots.get ( & j.id) .map ( String :: as_str) .unwrap_or ( " " ) ;
c2be148c Andy McCall 24 days ago ↶
890
let mine = fit ( & hub_key, & me_now, & rootfs, slot) ;
501d8b26 Andy McCall 26 days ago ↶
891
let other = runners.iter ( ) .any ( | r | {
892
r.id ! = me.id
893
& & r.state = = " active"
894
& & r.last_seen_ms.is_some_and ( | t | now - t < POLLING_MS )
895
& & running.get ( & r.id) .copied ( ) .unwrap_or ( 0 ) < r.max_jobs.max ( 1 )
c2be148c Andy McCall 24 days ago ↶
896
& & fit ( & hub_key, r, & rootfs, slot) .beats ( & mine)
501d8b26 Andy McCall 26 days ago ↶
897
& & runner_lacks ( j, owner, * repo_id, r, & fleet, & [ ] ) .is_none ( )
898
} ) ;
899
if other {
c95446a1 Andy McCall 26 days ago ↶
900
holds.entry ( ( hub_key.clone ( ) , j.id) ) .or_insert ( now) ;
501d8b26 Andy McCall 26 days ago ↶
901
}
902
other
903
} ;
c4c87f48 Andy McCall 1 month ago ↶
904
let Some ( job) =
501d8b26 Andy McCall 26 days ago ↶
905
fdb:: claim_ranked( & app.db, & req.runtimes, & runner, & crate :: auth:: token_hash( & secret) , room, & me.scope ( ) , & labels, & better) .await?
c4c87f48 Andy McCall 1 month ago ↶
906
else {
42fa9554 Andy McCall 1 month ago ↶
907
return Ok ( StatusCode:: NO_CONTENT .into_response ( ) ) ;
908
} ;
501d8b26 Andy McCall 26 days ago ↶
909
c95446a1 Andy McCall 26 days ago ↶
910
let waited =
911
HELD .lock ( ) .unwrap_or_else ( | p | p.into_inner ( ) ) .as_mut ( ) .and_then ( | h | h.remove ( & ( hub_key.clone ( ) , job.id) ) ) .map ( | t | now - t ) ;
501d8b26 Andy McCall 26 days ago ↶
912
let rootfs = rootfs_of ( & job) ;
913
let mut why = vec! [ if me_now.image_list ( ) .is_some_and ( | have | have.contains ( & rootfs) ) {
914
" holds the image" .to_string ( )
915
} else {
916
" fetches the image from the hub" .to_string ( )
917
} ] ;
c2be148c Andy McCall 24 days ago ↶
918
if let Some ( kept) = slots.get ( & job.id) .and_then ( | slot | cache_kept ( & hub_key, me.id, slot) ) {
919
why.insert (
920
0 ,
921
if kept > 0 { format! ( " holds its build cache (kept {} ago)" , ago ( now - kept) ) } else { " holds its build cache" .into ( ) } ,
922
) ;
4fa0da12 Andy McCall 24 days ago ↶
923
}
501d8b26 Andy McCall 26 days ago ↶
924
match running.get ( & me.id) .copied ( ) .unwrap_or ( 0 ) {
925
0 => why.push ( " idle" .into ( ) ) ,
926
n => why.push ( format! ( " running {n} other job{} " , if n = = 1 { " " } else { " s" } ) ) ,
927
}
928
if let Some ( sp) = me_now.speed {
929
why.push ( format! ( " speed {sp} (class {} )" , me_now.speed_class.unwrap_or ( 0 ) ) ) ;
930
}
931
if let Some ( w) = waited {
932
why.push ( format! ( " after {} s held for a better runner" , w / 1000 ) ) ;
933
}
934
fdb:: set_placed( & app.db, job.id, & why.join ( " , " ) ) .await? ;
b6b7e4cf Andy McCall 1 month ago ↶
935
db:: runners:: set_job_runner( & app.db, job.id, me.id) .await? ;
42fa9554 Andy McCall 1 month ago ↶
936
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
937
let row =
938
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
a0d4bbfb Andy McCall 1 month ago ↶
939
report_check ( & app, & row, & run.state, run.number, & job.name, " running" , & format! ( " running on {runner} " ) ) .await;
42fa9554 Andy McCall 1 month ago ↶
940
let bad = | e : serde_json:: Error| ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ;
5dead1f4 Andy McCall 1 month ago ↶
941
942
943
let limits = limits ( & app) .await;
944
let spec: flow_core:: pipeline:: Job = serde_json:: from_str( & job.spec) .map_err ( bad) ? ;
945
let sandbox: flow_core:: pipeline:: Sandbox = serde_json:: from_str( & job.sandbox) .map_err ( bad) ? ;
7f5f6c05 Andy McCall 1 month ago ↶
946
let mut timeout_s = spec.timeout_s.or ( sandbox.timeout_s) .unwrap_or ( limits.default_s) .min ( limits.max_s) .max ( 60 ) ;
947
948
if let Some ( m) = me.timeout_minutes.filter ( | m | * m > 0 ) {
949
timeout_s = timeout_s.min ( m as u64 * 60 ) .max ( 60 ) ;
950
}
5dead1f4 Andy McCall 1 month ago ↶
951
fdb:: set_job_timeout( & app.db, job.id, timeout_s) .await? ;
42fa9554 Andy McCall 1 month ago ↶
952
let claim = Claim {
953
job_id: job.id,
954
run_id: run.id,
955
repo: format! ( " {} /{} " , row.owner, row.name) ,
956
clone_url: format! ( " {} /{} /{} " , app.config.base_url ( ) , row.owner, row.name) ,
957
token: secret,
958
heads: run.head_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ,
959
pipeline_hash: run.pipeline_hash.clone ( ) ,
5dead1f4 Andy McCall 1 month ago ↶
960
job: spec,
961
sandbox,
962
timeout_s,
fd3db644 Andy McCall 1 month ago ↶
963
subrepos: vec! [ ] ,
9a065bc0 Andy McCall 1 month ago ↶
964
state: run.state.clone ( ) ,
965
bookmark: run.bookmark.clone ( ) ,
966
run_number: run.number,
2a83f530 Andy McCall 1 month ago ↶
967
968
env: crate :: variables:: env_for_repo( & app.db, & row) .await? ,
4ac01a73 Andy McCall 1 month ago ↶
969
secrets: vec! [ ] ,
cd437c4f Andy McCall 22 days ago ↶
970
inputs: vec! [ ] ,
42fa9554 Andy McCall 1 month ago ↶
971
} ;
fd3db644 Andy McCall 1 month ago ↶
972
let mut claim = claim;
4ac01a73 Andy McCall 1 month ago ↶
973
let ( inputs, ids, mut left_out) = subrepo_inputs ( & app, & row, & run) .await;
974
975
976
let names: Vec< String> = claim.job.secrets.iter ( ) .map ( | s | s.name.clone ( ) ) .collect ( ) ;
977
let found = crate :: secrets:: for_job( & app, & row, & names) .await? ;
d1727c57 Andy McCall 1 month ago ↶
978
b644e175 Andy McCall 21 days ago ↶
979
980
981
let environment = claim.job.environment.clone ( ) .filter ( | _| job.approval = = " approved" ) ;
982
if let ( Some ( e) , None , false ) = ( & claim.job.environment, & environment, claim.job.identities.is_empty ( ) ) {
983
left_out.push ( format! (
984
" environment {e} : not in the job's identity tokens, because the job was not approved (give it an approval block)"
985
) ) ;
986
}
d1727c57 Andy McCall 1 month ago ↶
987
if ! claim.job.identities.is_empty ( ) {
988
let issuer = app.oidc ( ) .await.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " oidc: {e} " ) ) ) ? ;
989
for i in & claim.job.identities {
990
let token = issuer.mint ( & crate :: oidc:: JobClaims {
991
issuer: app.config.base_url ( ) ,
992
audience: & i.audience,
993
owner: & row.owner,
994
name: & row.name,
995
visibility: & row.visibility,
996
bookmark: & run.bookmark,
997
run_number: run.number,
998
job: & job.name,
999
state: & run.state,
1000
runner: & runner,
1001
actor: & run.actor,
b644e175 Andy McCall 21 days ago ↶
1002
environment: environment.as_deref ( ) ,
47e37402 Andy McCall 21 days ago ↶
1003
approved_by: ( job.approval = = " approved" ) .then_some ( job.approved_by.as_str ( ) ) ,
1004
approval: ( ! job.approval_seal.is_empty ( ) ) .then_some ( job.approval_seal.as_str ( ) ) ,
d1727c57 Andy McCall 1 month ago ↶
1005
now_ms: db:: now_ms( ) ,
1006
ttl_s: timeout_s,
1007
} ) ;
1008
claim.secrets.push ( flow_core:: api:: SecretInput {
1009
name: format! ( " identity:{} " , i.audience) ,
1010
path: i.mount_path.clone ( ) ,
1011
value: token,
1012
} ) ;
1013
}
1014
}
4ac01a73 Andy McCall 1 month ago ↶
1015
for s in & claim.job.secrets {
1016
match found.iter ( ) .find ( | ( n , _) | * n = = s.name) {
1017
Some ( ( name, value) ) => {
1018
claim.secrets.push ( flow_core:: api:: SecretInput { name: name.clone ( ) , path: s.mount_path.clone ( ) , value: value.clone ( ) } )
1019
}
1020
None => left_out.push ( format! (
1021
" secret {:?} is not set for this repository, its owner or the site; {} will be missing" ,
1022
s.name, s.mount_path
1023
) ) ,
1024
}
1025
}
fd3db644 Andy McCall 1 month ago ↶
1026
if ! ids.is_empty ( ) {
1027
fdb:: set_also_read( & app.db, job.id, & ids) .await? ;
1028
}
afd0be38 Andy McCall 1 month ago ↶
1029
1030
1031
if ! left_out.is_empty ( ) {
1032
let records: Vec< flow_core:: api:: Record> =
1033
left_out.into_iter ( ) .map ( | text | flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text } ) .collect ( ) ;
1034
fdb:: append_log( & app.db, job.id, & records, MAX_RECORDS as i64 ) .await? ;
1035
}
fd3db644 Andy McCall 1 month ago ↶
1036
claim.subrepos = inputs;
cd437c4f Andy McCall 22 days ago ↶
1037
1038
1039
match artifact_inputs ( & app, & job, & claim.job) .await {
1040
Ok ( found) => claim.inputs = found,
1041
Err ( why) => {
1042
let error = format! ( " inputs: {why} " ) ;
1043
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
1044
.await? ;
1045
return Err ( ApiError( StatusCode:: CONFLICT , error) ) ;
1046
}
1047
}
42fa9554 Andy McCall 1 month ago ↶
1048
Ok ( Json( claim) .into_response ( ) )
cd437c4f Andy McCall 22 days ago ↶
1049
}
1050
1051
1052
1053
1054
async fn artifact_inputs ( app : & Shared, job : & fdb:: Job, spec : & flow_core:: Job) -> Result< Vec< flow_core:: api:: ArtifactInput> , String> {
1055
if spec.inputs.is_empty ( ) {
1056
return Ok ( vec! [ ] ) ;
1057
}
1058
let jobs = fdb:: jobs( & app.db, job.run_id) .await.map_err ( | e | e.to_string ( ) ) ? ;
1059
let mut out = Vec :: new( ) ;
1060
for i in & spec.inputs {
1061
let Some ( producer) = jobs.iter ( ) .find ( | j | j.name = = i.from ) else {
1062
return Err ( format! ( " {:?} is not a job of this run" , i.from) ) ;
1063
} ;
1064
if producer.status ! = " passed" | | producer.receipt_hash.is_empty ( ) {
1065
return Err ( format! ( " {} has not passed with a receipt, so {} cannot be taken from it" , i.from, i.artifact) ) ;
1066
}
1067
let artifacts = fdb:: artifacts( & app.db, producer.id) .await.map_err ( | e | e.to_string ( ) ) ? ;
1068
let Some ( a) = artifacts.iter ( ) .find ( | a | a.name = = i.artifact ) else {
1069
return Err ( format! ( " {} kept no artifact {} " , i.from, i.artifact) ) ;
1070
} ;
1071
out.push ( flow_core:: api:: ArtifactInput {
1072
artifact: i.artifact.clone ( ) ,
1073
from: i.from.clone ( ) ,
1074
path: i.path.clone ( ) ,
1075
blob: a.blob.clone ( ) ,
1076
} ) ;
1077
}
1078
Ok ( out)
1079
}
1080
1081
1082
1083
pub async fn input ( State( app ) : State< Shared> , headers : HeaderMap, Path( (id , index ) ) : Path< ( i64 , usize ) > ) -> Result< Response, ApiError> {
1084
let runner = runner_auth ( & app, & headers) .await? ;
1085
let job = running_job ( & app, id) .await? ;
1086
1087
1088
if runner.source = = " registered" & & job.runner ! = runner.name {
1089
return Err ( ApiError( StatusCode:: FORBIDDEN , " this job is held by another runner" .into ( ) ) ) ;
1090
}
1091
let spec: flow_core:: Job =
1092
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
1093
let found = artifact_inputs ( & app, & job, & spec) .await.map_err ( | why | ApiError( StatusCode:: CONFLICT , why) ) ? ;
1094
let Some ( i) = found.get ( index) else {
1095
return Err ( ApiError( StatusCode:: NOT_FOUND , format! ( " job {} has no input {index} " , job.name) ) ) ;
1096
} ;
1097
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1098
let row =
1099
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
1100
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
1101
let blob = oxy_core:: hash:: ObjectRef:: from_hex( & i.blob)
1102
.ok_or_else ( | | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , " a kept artifact has a bad blob" .into ( ) ) ) ? ;
1103
let bytes = app
1104
.repos
1105
.read ( & repo, move | r, _ | Ok ( r.read_blob ( & blob) ? ) )
1106
.await
1107
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " read the artifact: {e} " ) ) ) ? ;
1108
Ok ( ( [ ( axum:: http:: header:: CONTENT_TYPE , " application/octet-stream" ) ] , bytes) .into_response ( ) )
fd3db644 Andy McCall 1 month ago ↶
1109
}
1110
4d2e44d7 Andy McCall 1 month ago ↶
1111
1112
1113
fn profile_of ( req : & ClaimRequest, arch : & str ) -> String {
1114
let sorted = | v : & [String]| {
1115
let mut v = v.to_vec ( ) ;
1116
v.sort ( ) ;
1117
v
1118
} ;
1119
let info = req.info.as_ref ( ) ;
1120
let v = serde_json:: json! ( {
1121
" arch" : arch,
1122
" tiers" : sorted ( & req.runtimes) ,
1123
" images" : req.images.as_deref ( ) .map ( sorted) ,
1124
" labels" : info.map ( | i | sorted ( & i.labels) ) ,
1125
" memory_mib" : info.and_then ( | i | i.memory_mib ) ,
1126
" cpus" : info.and_then ( | i | i.cpus ) ,
1127
} ) ;
1128
blake3:: hash( v.to_string ( ) .as_bytes ( ) ) .to_hex ( ) .to_string ( )
1129
}
1130
1131
1132
pub const UNPLACEABLE_AFTER : usize = 3 ;
1133
1134
1135
1136
1137
1138
1139
e9b9619c Andy McCall 21 days ago ↶
1140
1141
1142
pub async fn heartbeat ( State( app ) : State< Shared> , headers : HeaderMap) -> Result< StatusCode, ApiError> {
1143
let me = runner_auth ( & app, & headers) .await? ;
1144
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1145
Ok ( StatusCode:: NO_CONTENT )
1146
}
1147
4d2e44d7 Andy McCall 1 month ago ↶
1148
pub async fn decline (
1149
State( app ) : State< Shared> ,
1150
headers : HeaderMap,
1151
Path( id ) : Path< i64 > ,
1152
Json( d ) : Json< flow_core:: api:: Decline> ,
1153
) -> Result< StatusCode, ApiError> {
1154
let me = runner_auth ( & app, & headers) .await? ;
1155
db:: runners:: seen( & app.db, me.id, None , None ) .await? ;
1156
let reason: String = d.reason.chars ( ) .map ( | c | if c.is_control ( ) { ' ' } else { c } ) .take ( 500 ) .collect ( ) ;
1157
let reason = match reason.trim ( ) {
1158
" " => " no reason given" .to_string ( ) ,
1159
r => r.to_string ( ) ,
1160
} ;
1161
let Some ( all) = fdb:: decline( & app.db, id, me.id, & me.name, & me.profile, & reason) .await? else {
1162
return Err ( ApiError( StatusCode:: CONFLICT , " that job is not running on this runner" .into ( ) ) ) ;
1163
} ;
1164
tracing:: info! ( " flow: runner {} declined job {id}: {reason}" , me.name) ;
1165
let job = fdb:: job( & app.db, id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such job" .into ( ) ) ) ? ;
1166
let runners: std:: collections:: BTreeSet< i64 > = all.iter ( ) .map ( | d | d.runner_id ) .collect ( ) ;
1167
if runners.len ( ) > = UNPLACEABLE_AFTER {
1168
let error = format! (
1169
" unplaceable: declined by {} runners: {} " ,
1170
runners.len ( ) ,
1171
all.iter ( ) .map ( | d | format! ( " {} : {} " , d.runner, d.reason) ) .collect:: < Vec< _ > > ( ) .join ( " ; " )
1172
) ;
1173
let changed =
1174
fdb:: finish( & app.db, id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
1175
.await? ;
1176
report_changed ( & app, job.run_id, changed) .await? ;
1177
} else if let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? {
1178
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
1179
let summary = format! ( " declined by {} : {reason} ; waiting for another runner" , me.name) ;
1180
report_check ( & app, & row, & run.state, run.number, & job.name, " queued" , & summary) .await;
1181
fdb:: set_waiting( & app.db, id, & summary) .await? ;
1182
}
1183
}
1184
Ok ( StatusCode:: NO_CONTENT )
501d8b26 Andy McCall 26 days ago ↶
1185
}
1186
1187
1188
1189
1190
1191
1192
1193
pub struct Fleet < 'a > {
1194
pub image_archs : & 'a std:: collections:: HashMap< String, String> ,
1195
pub hub_images : & 'a std:: collections:: HashSet< String> ,
1196
pub now : i64 ,
1197
}
1198
1199
1200
1201
1202
pub fn runner_lacks (
1203
job : & fdb:: Job,
1204
owner : & str ,
1205
repo_id : i64 ,
1206
r : & db:: runners:: Runner,
1207
fleet : & Fleet,
1208
declines : & [fdb:: DeclineRow],
1209
) -> Option< String> {
1210
let sandbox: serde_json:: Value = serde_json:: from_str( & job.sandbox) .unwrap_or_default ( ) ;
1211
let rootfs = sandbox[ " rootfs" ] .as_str ( ) .unwrap_or ( " " ) ;
1212
let wanted_arch = job.arch ( ) .or_else ( | | fleet.image_archs.get ( rootfs) .cloned ( ) ) ;
1213
let need = | k : & str | sandbox[ k] .as_u64 ( ) .unwrap_or ( 0 ) ;
1214
if r.state = = " draining" {
1215
return Some ( " is draining" .into ( ) ) ;
1216
}
1217
let scope = r.scope ( ) ;
1218
if ( ! scope.owner.is_empty ( ) & & scope.owner ! = owner) | | scope.repo_id.is_some_and ( | id | id ! = repo_id ) {
1219
return Some ( " serves other repositories" .into ( ) ) ;
1220
}
1221
let tiers = r.tier_list ( ) ;
1222
if ! tiers.is_empty ( ) & & ! tiers.contains ( & job.runtime.as_str ( ) ) {
1223
return Some ( format! ( " has no {} tier (it offers {} )" , job.runtime, tiers.join ( " , " ) ) ) ;
1224
}
1225
if let Some ( w) = wanted_arch.as_deref ( ) .filter ( | w | ! r.arch.is_empty ( ) & & * w ! = r.arch) {
1226
return Some ( format! ( " is {} ; the job needs {w} " , r.arch) ) ;
1227
}
1228
let fetches = r.fetches_images & & fleet.hub_images.contains ( rootfs) ;
1229
if ! fetches & & r.image_list ( ) .is_some_and ( | have | ! have.iter ( ) .any ( | h | h = = rootfs ) ) {
1230
let why = if r.fetches_images { " (nor does the hub: send it with flow rootfs push)" } else { " " } ;
1231
return Some ( format! ( " does not hold image {} {why} " , & rootfs[ .. 12. min ( rootfs.len ( ) ) ] ) ) ;
1232
}
1233
let labels = r.label_list ( ) ;
1234
if let Some ( l) = job.needs ( ) .iter ( ) .find ( | l | ! labels.contains ( & l.as_str ( ) ) ) {
1235
return Some ( format! ( " lacks label {l} " ) ) ;
1236
}
1237
if let Some ( m) = r.memory_mib.filter ( | m | ( * m as u64 ) < need ( " memory_mib" ) ) {
1238
return Some ( format! ( " has {m} MiB; the job needs {} MiB" , need ( " memory_mib" ) ) ) ;
1239
}
1240
if let Some ( c) = r.cpus.filter ( | c | ( * c as u64 ) < need ( " cpus" ) ) {
1241
return Some ( format! ( " has {c} CPUs; the job needs {} " , need ( " cpus" ) ) ) ;
1242
}
1243
1244
if let ( Some ( n) , Some ( free) ) = ( job.need_disk_mib, r.free_disk_mib) {
1245
if free < n {
1246
return Some ( format! (
1247
" has {} free; the job needs ~{} (its recent runs took up to {} )" ,
1248
mib ( free) ,
1249
mib ( n) ,
1250
mib ( ( n - 64 ) * 4 / 5 )
1251
) ) ;
1252
}
1253
}
1254
if let Some ( d) = declines.iter ( ) .find ( | d | d.runner_id = = r.id & & fleet.now - d.ms < fdb:: DECLINE_HOLDS_MS ) {
1255
return Some ( format! ( " declined it: {} " , d.reason) ) ;
1256
}
1257
None
4d2e44d7 Andy McCall 1 month ago ↶
1258
}
1259
1260
1261
1262
1263
92a797b2 Andy McCall 26 days ago ↶
1264
# [ allow ( clippy::too_many_arguments ) ]
4d2e44d7 Andy McCall 1 month ago ↶
1265
pub fn why_waiting (
1266
job : & fdb:: Job,
1267
owner : & str ,
1268
repo_id : i64 ,
1269
runners : & [db:: runners:: Runner],
1270
image_archs : & std:: collections:: HashMap< String, String> ,
1271
declines : & [fdb:: DeclineRow],
1272
now : i64 ,
92a797b2 Andy McCall 26 days ago ↶
1273
hub_images : & std:: collections:: HashSet< String> ,
4d2e44d7 Andy McCall 1 month ago ↶
1274
) -> String {
501d8b26 Andy McCall 26 days ago ↶
1275
let fleet = Fleet { image_archs, hub_images, now } ;
4d2e44d7 Andy McCall 1 month ago ↶
1276
let online: Vec< & db:: runners:: Runner> = runners.iter ( ) .filter ( | r | r.state ! = " disabled" & & r.online ( ) ) .collect ( ) ;
1277
if online.is_empty ( ) {
1278
return " no runner is online" .into ( ) ;
1279
}
1280
let mut why = Vec :: new( ) ;
1281
for r in & online {
501d8b26 Andy McCall 26 days ago ↶
1282
match runner_lacks ( job, owner, repo_id, r, & fleet, declines) {
4d2e44d7 Andy McCall 1 month ago ↶
1283
None => return String :: new( ) ,
1284
Some ( w) => why.push ( format! ( " {} {w} " , r.name) ) ,
1285
}
1286
}
1287
let more = why.len ( ) .saturating_sub ( 4 ) ;
1288
why.truncate ( 4 ) ;
1289
let mut text = format! ( " no runner can take it: {} " , why.join ( " ; " ) ) ;
1290
if more > 0 {
1291
text.push_str ( & format! ( " ; and {more} more" ) ) ;
1292
}
1293
text
501d8b26 Andy McCall 26 days ago ↶
1294
}
1295
1296
1297
1298
const BETTER_HOLD_MS : i64 = 15_000 ;
1299
1300
const POLLING_MS : i64 = 10_000 ;
1301
1302
c95446a1 Andy McCall 26 days ago ↶
1303
1304
static HELD : std:: sync:: Mutex< Option< std:: collections:: HashMap< ( String, i64 ) , i64 > > > = std:: sync:: Mutex:: new( None ) ;
4fa0da12 Andy McCall 24 days ago ↶
1305
1306
1307
1308
c2be148c Andy McCall 24 days ago ↶
1309
1310
type CacheSlots = std:: collections:: HashMap< ( String, i64 ) , std:: collections:: HashMap< String, i64 > > ;
4fa0da12 Andy McCall 24 days ago ↶
1311
static CACHES : std:: sync:: Mutex< Option< CacheSlots> > = std:: sync:: Mutex:: new( None ) ;
1312
c2be148c Andy McCall 24 days ago ↶
1313
1314
1315
fn cache_kept ( hub : & str , runner : i64 , slot : & str ) -> Option< i64 > {
4fa0da12 Andy McCall 24 days ago ↶
1316
CACHES
1317
.lock ( )
1318
.unwrap_or_else ( | p | p.into_inner ( ) )
1319
.as_ref ( )
1320
.and_then ( | c | c.get ( & ( hub.to_string ( ) , runner) ) )
c2be148c Andy McCall 24 days ago ↶
1321
.and_then ( | held | held.get ( slot) .copied ( ) )
1322
}
1323
1324
1325
1326
const FRESHER_MS : i64 = 60_000 ;
1327
1328
1329
struct Fit {
1330
1331
cache : Option< i64 > ,
1332
image : bool ,
1333
speed : i64 ,
1334
}
1335
1336
impl Fit {
1337
1338
1339
1340
fn beats ( & self , other : & Fit) -> bool {
1341
match ( self .cache, other.cache) {
1342
( Some ( _ ) , None ) => return true ,
1343
( None , Some ( _ ) ) => return false ,
1344
( Some ( a) , Some ( b) ) if a > 0 & & b > 0 & & ( a - b) .abs ( ) > = FRESHER_MS => return a > b,
1345
_ => { }
1346
}
1347
( self .image, self .speed) > ( other.image, other.speed)
1348
}
1349
}
1350
1351
1352
1353
fn fit ( hub : & str , r : & db:: runners:: Runner, rootfs : & str , slot : & str ) -> Fit {
1354
Fit {
1355
cache: cache_kept ( hub, r.id, slot) ,
1356
image: r.image_list ( ) .is_some_and ( | have | have.iter ( ) .any ( | h | h = = rootfs ) ) ,
1357
speed: r.speed_class.unwrap_or ( 0 ) ,
1358
}
4fa0da12 Andy McCall 24 days ago ↶
1359
}
501d8b26 Andy McCall 26 days ago ↶
1360
c2be148c Andy McCall 24 days ago ↶
1361
1362
fn ago ( ms : i64 ) -> String {
1363
let s = ms.max ( 0 ) / 1000 ;
1364
match s {
1365
0 .. = 59 => format! ( " {s} s" ) ,
1366
60 .. = 3599 => format! ( " {} min" , s / 60 ) ,
1367
3600 .. = 172_799 => format! ( " {} h" , s / 3600 ) ,
1368
_ => format! ( " {} days" , s / 86_400 ) ,
1369
}
501d8b26 Andy McCall 26 days ago ↶
1370
}
1371
1372
fn rootfs_of ( job : & fdb:: Job) -> String {
1373
serde_json:: from_str:: < serde_json:: Value> ( & job.sandbox) .ok ( ) .and_then ( | v | v[ " rootfs" ] .as_str ( ) .map ( String :: from) ) .unwrap_or_default ( )
44a3d2eb Andy McCall 26 days ago ↶
1374
}
1375
1376
1377
fn mib ( m : i64 ) -> String {
1378
if m > = 1024 {
1379
format! ( " {:.1} GiB" , m as f64 / 1024. 0 )
1380
} else {
1381
format! ( " {m} MiB" )
1382
}
4d2e44d7 Andy McCall 1 month ago ↶
1383
}
1384
1385
1386
pub async fn explain_waits ( app : & Shared) -> sqlx:: Result< ( ) > {
1387
let ready = fdb:: ready_queued( & app.db) .await? ;
1388
if ready.is_empty ( ) {
1389
return Ok ( ( ) ) ;
1390
}
1391
let runners = db:: runners:: list( & app.db) .await? ;
1392
let archs = fdb:: image_archs( & app.db) .await? ;
92a797b2 Andy McCall 26 days ago ↶
1393
let held = crate :: images:: held( & app.db) .await? ;
4d2e44d7 Andy McCall 1 month ago ↶
1394
let now = db:: now_ms( ) ;
1395
for ( job, owner, repo_id) in ready {
1396
let declines = fdb:: declines_of_job( & app.db, job.id) .await? ;
92a797b2 Andy McCall 26 days ago ↶
1397
let why = why_waiting ( & job, & owner, repo_id, & runners, & archs, & declines, now, & held) ;
67040b15 Andy McCall 26 days ago ↶
1398
if why = = job.waiting {
1399
continue ;
1400
}
4d2e44d7 Andy McCall 1 month ago ↶
1401
fdb:: set_waiting( & app.db, job.id, & why) .await? ;
67040b15 Andy McCall 26 days ago ↶
1402
1403
1404
if let ( Some ( run) , Some ( row) ) = ( fdb:: run_by_id( & app.db, job.run_id) .await? , db:: repos:: get_by_id( & app.db, repo_id) .await? ) {
1405
let summary = if why.is_empty ( ) { " waiting for a runner" } else { why.as_str ( ) } ;
1406
report_check ( app, & row, & run.state, run.number, & job.name, " queued" , summary) .await;
1407
}
4d2e44d7 Andy McCall 1 month ago ↶
1408
}
1409
Ok ( ( ) )
1410
}
1411
1412
1413
pub async fn placement ( app : Shared) {
1414
loop {
1415
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 15 ) ) .await;
1416
if let Err ( e) = explain_waits ( & app) .await {
1417
tracing:: warn! ( " flow: placement pass: {e}" ) ;
1418
}
1419
}
1420
}
1421
25091e5c Andy McCall 1 month ago ↶
1422
f677d627 Andy McCall 25 days ago ↶
1423
pub ( crate ) async fn pins_in (
25091e5c Andy McCall 1 month ago ↶
1424
app : & Shared,
1425
name : crate:: repos:: RepoName,
1426
heads : Vec< oxy_core:: hash:: ChangeHash> ,
5d54cf74 Andy McCall 22 days ago ↶
1427
) -> Result< Vec< ( String, [u8 ; 16], Vec< oxy_core:: hash:: ChangeHash> ) > , String> {
25091e5c Andy McCall 1 month ago ↶
1428
app.repos
fd3db644 Andy McCall 1 month ago ↶
1429
.read ( & name, move | repo, cache| {
1430
let loaded = cache.load ( repo, & heads) ? ;
1431
let mut out = Vec :: new( ) ;
1432
for ( path, f) in & loaded.snapshot.files {
1433
if f.kind ! = oxy_core:: objects:: NodeKind:: Subrepo {
1434
continue ;
1435
}
1436
if let oxy_core:: state:: FileContent:: Opaque { blobs } = & f.content {
1437
if let Some ( b) = blobs.first ( ) {
1438
if let Ok ( oxy_core:: subrepo:: Pin:: Oxy { repo: id, heads, .. } ) = repo
1439
.read_blob ( b)
1440
.map_err ( crate :: repos:: RepoError:: Oxy)
1441
.and_then ( | bytes | oxy_core:: subrepo:: Pin:: decode( & bytes) .map_err ( | e | crate :: repos:: RepoError:: Oxy( e.into ( ) ) ) )
1442
{
1443
out.push ( ( path.clone ( ) , id, heads) ) ;
1444
}
1445
}
1446
}
1447
}
1448
Ok ( out)
1449
} )
1450
.await
5d54cf74 Andy McCall 22 days ago ↶
1451
.map_err ( | e | e.to_string ( ) )
25091e5c Andy McCall 1 month ago ↶
1452
}
1453
1454
1455
const MAX_SUBREPO_DEPTH : usize = 3 ;
1456
1457
1458
1459
afd0be38 Andy McCall 1 month ago ↶
1460
1461
1462
async fn subrepo_inputs (
1463
app : & Shared,
1464
row : & db:: repos:: RepoRow,
1465
run : & fdb:: Run,
1466
) -> ( Vec< flow_core:: api:: SubrepoInput> , Vec< i64 > , Vec< String> ) {
1467
let Some ( name) = crate :: repos:: RepoName:: new( & row.owner, & row.name) else { return ( vec! [ ] , vec! [ ] , vec! [ ] ) } ;
25091e5c Andy McCall 1 month ago ↶
1468
let heads: Vec< oxy_core:: hash:: ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( oxy_core:: hash:: ChangeHash:: from_hex) .collect ( ) ;
fd3db644 Andy McCall 1 month ago ↶
1469
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
25091e5c Andy McCall 1 month ago ↶
1470
let rows = db:: repos:: list( & app.db) .await.unwrap_or_default ( ) ;
fd3db644 Andy McCall 1 month ago ↶
1471
let mut inputs = Vec :: new( ) ;
1472
let mut ids = Vec :: new( ) ;
afd0be38 Andy McCall 1 month ago ↶
1473
let mut left_out = Vec :: new( ) ;
1474
let actor = if run.actor.is_empty ( ) { " the run's starter" .to_string ( ) } else { run.actor.clone ( ) } ;
25091e5c Andy McCall 1 month ago ↶
1475
1476
let mut queue = std:: collections:: VecDeque:: from( [ ( String :: new( ) , name, heads, 0 usize ) ] ) ;
1477
while let Some ( ( prefix, name, heads, depth) ) = queue.pop_front ( ) {
1478
if depth = = MAX_SUBREPO_DEPTH {
1479
continue ;
1480
}
5d54cf74 Andy McCall 22 days ago ↶
1481
for ( path, id, pinned) in pins_in ( app, name, heads) .await.unwrap_or_default ( ) {
25091e5c Andy McCall 1 month ago ↶
1482
let path = if prefix.is_empty ( ) { path } else { format! ( " {prefix} /{path} " ) } ;
afd0be38 Andy McCall 1 month ago ↶
1483
let mut hosted = false ;
25091e5c Andy McCall 1 month ago ↶
1484
for other in & rows {
1485
let Some ( other_name) = crate :: repos:: RepoName:: new( & other.owner, & other.name) else { continue } ;
1486
let matches = app.repos.read ( & other_name, move | r, _ | Ok ( r.id ( ) .ok ( ) = = Some ( id) ) ) .await.unwrap_or ( false ) ;
1487
if ! matches {
1488
continue ;
1489
}
afd0be38 Andy McCall 1 month ago ↶
1490
hosted = true ;
25091e5c Andy McCall 1 month ago ↶
1491
let role = crate :: auth:: roles:: role_for( & app.db, pusher.as_ref ( ) , other) .await.ok ( ) .flatten ( ) ;
180dc43a Andy McCall 24 days ago ↶
1492
1493
1494
1495
let diffused = run.actor = = crate :: diffusion:: ACCOUNT
1496
& & run.bookmark.starts_with ( " diffusion/" )
5d90e0e0 Andy McCall 21 days ago
1497
& & sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1 AND upstream_repo_id = $2" )
180dc43a Andy McCall 24 days ago ↶
1498
.bind ( run.repo_id)
1499
.bind ( other.id)
1500
.fetch_one ( & app.db)
1501
.await
1502
.unwrap_or ( 0 )
1503
> 0 ;
1504
if diffused | | crate :: auth:: roles:: allows( role, crate :: auth:: Access:: Read) {
25091e5c Andy McCall 1 month ago ↶
1505
inputs.push ( flow_core:: api:: SubrepoInput {
1506
path: path.clone ( ) ,
1507
repo: format! ( " {} /{} " , other.owner, other.name) ,
1508
clone_url: format! ( " {} /{} /{} " , app.config.base_url ( ) , other.owner, other.name) ,
1509
heads: pinned.iter ( ) .map ( | h | h.to_hex ( ) ) .collect ( ) ,
1510
} ) ;
1511
if ! ids.contains ( & other.id) {
1512
ids.push ( other.id) ;
1513
}
1514
queue.push_back ( ( path.clone ( ) , other_name, pinned.clone ( ) , depth + 1 ) ) ;
afd0be38 Andy McCall 1 month ago ↶
1515
} else {
1516
left_out.push ( format! (
1517
" subrepo {path} ({} /{} ) is left out: {actor} cannot read it, so the job has no files there" ,
1518
other.owner, other.name
1519
) ) ;
25091e5c Andy McCall 1 month ago ↶
1520
}
1521
break ;
afd0be38 Andy McCall 1 month ago ↶
1522
}
1523
if ! hosted {
1524
left_out.push ( format! ( " subrepo {path} is left out: the repository it pins is not on this hub" ) ) ;
fd3db644 Andy McCall 1 month ago ↶
1525
}
1526
}
1527
}
afd0be38 Andy McCall 1 month ago ↶
1528
( inputs, ids, left_out)
42fa9554 Andy McCall 1 month ago ↶
1529
}
1530
1531
async fn running_job ( app : & Shared, id : i64 ) -> Result< fdb:: Job, ApiError> {
1532
match fdb:: job( & app.db, id) .await? {
1533
Some ( j) if j.status = = " running" => Ok ( j) ,
1534
Some ( _ ) => Err ( ApiError( StatusCode:: CONFLICT , " this job is not running" .into ( ) ) ) ,
1535
None => Err ( ApiError( StatusCode:: NOT_FOUND , " no such job" .into ( ) ) ) ,
1536
}
1537
}
1538
1539
1540
pub async fn log (
1541
State( app ) : State< Shared> ,
1542
headers : HeaderMap,
1543
Path( id ) : Path< i64 > ,
1544
Json( mut batch ) : Json< LogBatch> ,
1545
) -> Result< StatusCode, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
1546
runner_auth ( & app, & headers) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1547
let job = running_job ( & app, id) .await? ;
745d225d Andy McCall 1 month ago ↶
1548
1549
1550
if batch.records.is_empty ( ) {
1551
return Ok ( StatusCode:: NO_CONTENT ) ;
1552
}
4ac01a73 Andy McCall 1 month ago ↶
1553
1554
let values: Vec< String> = match serde_json:: from_str:: < flow_core:: Job> ( & job.spec) {
1555
Ok ( spec) if ! spec.secrets.is_empty ( ) => {
1556
let run = fdb:: run_by_id( & app.db, job.run_id) .await? ;
1557
let row = match run {
1558
Some ( run) => db:: repos:: get_by_id( & app.db, run.repo_id) .await? ,
1559
None => None ,
1560
} ;
1561
match row {
1562
Some ( row) => {
1563
let names: Vec< String> = spec.secrets.iter ( ) .map ( | s | s.name.clone ( ) ) .collect ( ) ;
1564
crate :: secrets:: for_job( & app, & row, & names) .await? .into_iter ( ) .map ( | ( _, v ) | v ) .collect ( )
1565
}
1566
None => vec! [ ] ,
1567
}
1568
}
1569
_ => vec! [ ] ,
1570
} ;
d1727c57 Andy McCall 1 month ago ↶
1571
let kid = app.oidc.get ( ) .map ( | i | i.kid.clone ( ) ) ;
42fa9554 Andy McCall 1 month ago ↶
1572
for r in & mut batch.records {
4ac01a73 Andy McCall 1 month ago ↶
1573
if ! values.is_empty ( ) {
1574
r.text = crate :: secrets:: mask( & r.text, & values) ;
d1727c57 Andy McCall 1 month ago ↶
1575
}
1576
if let Some ( kid) = & kid {
1577
r.text = crate :: oidc:: mask( & r.text, kid) ;
4ac01a73 Andy McCall 1 month ago ↶
1578
}
42fa9554 Andy McCall 1 month ago ↶
1579
if r.text.len ( ) > MAX_RECORD_TEXT {
1580
let mut cut = MAX_RECORD_TEXT ;
1581
while ! r.text.is_char_boundary ( cut) {
1582
cut - = 1 ;
1583
}
1584
r.text.truncate ( cut) ;
1585
}
1586
if ! matches! ( r.stream.as_str ( ) , " out" | " err" | " console" | " flow" ) {
1587
r.stream = " console" .into ( ) ;
1588
}
1589
}
1590
let kept = fdb:: append_log( & app.db, job.id, & batch.records, MAX_RECORDS as i64 ) .await? ;
1591
if kept < batch.records.len ( ) & & job.log_count < MAX_RECORDS as i64 {
1592
tracing:: info! ( " flow: job {} reached the log limit" , job.id) ;
1593
}
1594
Ok ( StatusCode:: NO_CONTENT )
1595
}
1596
a3c8288b Andy McCall 1 month ago ↶
1597
1598
pub const MAX_ARTIFACT_BYTES : usize = 512 < < 20 ;
1599
1600
1601
1602
1603
pub async fn artifact (
1604
State( app ) : State< Shared> ,
1605
headers : HeaderMap,
1606
Path( (id , name ) ) : Path< ( i64 , String) > ,
1607
body: axum:: body:: Bytes,
1608
) -> Result< Response, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
1609
runner_auth ( & app, & headers) .await? ;
a3c8288b Andy McCall 1 month ago ↶
1610
let job = running_job ( & app, id) .await? ;
1611
let spec: flow_core:: Job =
1612
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
1613
let Some ( out) = spec.outputs.iter ( ) .find ( | o | o.name = = name ) else {
1614
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " job {} declares no artifact {name:?} " , job.name) ) ) ;
1615
} ;
1616
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1617
let row =
1618
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
1619
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
a36bab6b Andy McCall 27 days ago ↶
1620
1621
1622
let report = match out.kind {
1623
flow_core:: OutputKind:: Report => Some ( report_manifest ( & app, & repo, & name, & body) .await? ) ,
1624
flow_core:: OutputKind:: Artifact => None ,
1625
} ;
a3c8288b Andy McCall 1 month ago ↶
1626
let content = blake3:: hash( & body) .to_hex ( ) .to_string ( ) ;
1627
let size = body.len ( ) as i64 ;
a36bab6b Andy McCall 27 days ago ↶
1628
let artifact = report.is_none ( ) ;
1629
let report_bytes = ( artifact & & name = = crate :: coverage:: ARTIFACT ) .then ( | | body.clone ( ) ) ;
1630
let junit_bytes = ( artifact & & name = = crate :: junit:: ARTIFACT ) .then ( | | body.clone ( ) ) ;
a3c8288b Andy McCall 1 month ago ↶
1631
let blob = app
1632
.repos
1633
.write ( & repo, move | r, _ | Ok ( r.store_blob ( & body) ? ) )
1634
.await
1635
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " keep the artifact: {e} " ) ) ) ? ;
1636
let a = fdb:: Artifact { job_id: job.id, name, path: out.path.clone ( ) , blob: blob.to_hex ( ) , size, content, created_ms: db:: now_ms( ) } ;
1637
fdb:: add_artifact( & app.db, & a) .await? ;
a36bab6b Andy McCall 27 days ago ↶
1638
if let Some ( ( manifest, declaration) ) = report {
1639
let origin = crate :: reports:: Origin {
1640
repo_id: row.id,
1641
run_id: run.id,
1642
run_number: run.number,
1643
job_id: job.id,
1644
job: & job.name,
1645
bookmark: & run.bookmark,
1646
state: & run.state,
1647
} ;
1648
let said = match declaration {
1649
Ok ( d) => match crate :: reports:: keep( & app, & origin, & a.name, & d, & manifest, & a.blob) .await {
c8df33a6 Andy McCall 26 days ago ↶
1650
Ok ( id) => {
1651
report_threshold_checks ( & app, & row, & run, & a.name, id, & d.metrics) .await;
a36bab6b Andy McCall 27 days ago ↶
1652
let s = if d.metrics.len ( ) = = 1 { " " } else { " s" } ;
1653
format! ( " report {} : {} \" {} \" , {} metric{s} " , a.name, d.kind, d.title, d.metrics.len ( ) )
1654
}
1655
Err ( e) => format! ( " report {} : not indexed: {e} " , a.name) ,
1656
} ,
1657
Err ( why) => {
1658
let d = crate :: reports:: Declaration:: unreadable( & a.name, & why) ;
1659
crate :: reports:: keep( & app, & origin, & a.name, & d, & manifest, & a.blob) .await.ok ( ) ;
1660
format! ( " report {} : report.toml: {why} " , a.name)
1661
}
1662
} ;
1663
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
1664
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
1665
}
554ccacf Andy McCall 1 month ago ↶
1666
1667
1668
if let Some ( bytes) = report_bytes {
1669
let said = match read_coverage ( & app, & repo, & row, & run, & job, & a.blob, bytes) .await {
1670
Ok ( text) | Err ( text) => text,
1671
} ;
1672
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
1673
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
1674
}
0b203bbf Andy McCall 1 month ago ↶
1675
1676
if let Some ( bytes) = junit_bytes {
1677
let said = match read_junit ( & app, & row, & run, & job, & a.blob, bytes) .await {
1678
Ok ( text) | Err ( text) => text,
1679
} ;
1680
let record = flow_core:: api:: Record { t: 0 , stream: " flow" .into ( ) , text: said } ;
1681
fdb:: append_log( & app.db, job.id, & [ record] , MAX_RECORDS as i64 ) .await.ok ( ) ;
1682
}
a3c8288b Andy McCall 1 month ago ↶
1683
Ok ( Json( serde_json:: json! ( { " blob" : a.blob } ) ) .into_response ( ) )
a36bab6b Andy McCall 27 days ago ↶
1684
}
1685
c8df33a6 Andy McCall 26 days ago ↶
1686
1687
1688
1689
async fn report_threshold_checks (
1690
app : & Shared,
1691
row : & db:: repos:: RepoRow,
1692
run : & fdb:: Run,
1693
name : & str ,
1694
id : i64 ,
1695
metrics : & [crate:: reports:: Metric],
1696
) {
1697
let url = format! ( " {} /{} /{} /reports/{name} ?id={id} " , app.config.base_url ( ) , row.owner, row.name) ;
1698
for m in metrics.iter ( ) .filter ( | m | m.fail_above.is_some ( ) | | m.fail_below.is_some ( ) ) {
1699
let unit = if m.unit.is_empty ( ) { String :: new( ) } else { format! ( " {} " , m.unit) } ;
1700
let above = m.fail_above.filter ( | a | m.value > * a ) ;
1701
let below = m.fail_below.filter ( | b | m.value < * b ) ;
1702
let ( status, summary) = match ( above, below) {
1703
( Some ( a) , _ ) => ( " failed" , format! ( " {} {unit} , above {a} " , m.value) ) ,
1704
( _ , Some ( b) ) => ( " failed" , format! ( " {} {unit} , below {b} " , m.value) ) ,
1705
_ => {
1706
let limits: Vec< String> = [ m.fail_above.map ( | a | format! ( " at most {a} " ) ) , m.fail_below.map ( | b | format! ( " at least {b} " ) ) ]
1707
.into_iter ( )
1708
.flatten ( )
1709
.collect ( ) ;
1710
( " passed" , format! ( " {} {unit} ({} )" , m.value, limits.join ( " , " ) ) )
1711
}
1712
} ;
1713
let check = format! ( " report/{name} /{} " , m.key) ;
1714
if let Ok ( c) = db:: hooks:: upsert_check(
1715
& app.db,
1716
row.id,
1717
db:: hooks:: CheckReport { state: & run.state, name: & check, status, summary: & summary, url: & url, reporter: None , seal: " " } ,
1718
)
1719
.await
1720
{
1721
let api = crate :: api:: check_of( & c) ;
1722
app.events.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
1723
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
1724
}
1725
}
1726
}
1727
a36bab6b Andy McCall 27 days ago ↶
1728
1729
1730
1731
async fn report_manifest (
1732
app : & Shared,
1733
repo : & RepoName,
1734
name : & str ,
1735
body : & [u8 ],
1736
) -> Result< ( crate:: reports:: Manifest, Result< crate:: reports:: Declaration, String> ) , ApiError> {
1737
let manifest = crate :: reports:: parse_manifest( body) .map_err ( | e | ApiError( StatusCode:: BAD_REQUEST , e) ) ? ;
1738
let refs: Vec< ( String, oxy_core:: hash:: ObjectRef) > =
1739
manifest.files.iter ( ) .filter_map ( | f | Some ( ( f.path.clone ( ) , oxy_core:: hash:: ObjectRef:: from_hex( & f.blob) ? ) ) ) .collect ( ) ;
1740
let declaration = manifest.blob ( " report.toml" ) .and_then ( oxy_core:: hash:: ObjectRef:: from_hex) ;
1741
let found = app
1742
.repos
1743
.read ( repo, move | r, _ | {
1744
if let Some ( ( path, _ ) ) = refs.iter ( ) .find ( | ( _, b ) | ! r.store ( ) .has ( b) ) {
1745
return Ok ( Err ( path.clone ( ) ) ) ;
1746
}
1747
Ok ( Ok ( declaration.map ( | b | r.read_blob ( & b) ) .transpose ( ) ? ) )
1748
} )
1749
.await
1750
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " read the report: {e} " ) ) ) ? ;
1751
let toml = found.map_err ( | path | ApiError( StatusCode:: BAD_REQUEST , format! ( " report file {path} was not uploaded; upload it first" ) ) ) ? ;
1752
let declaration = crate :: reports:: parse_declaration( name, & toml.unwrap_or_default ( ) ) ;
1753
Ok ( ( manifest, declaration) )
1754
}
1755
1756
1757
1758
1759
1760
pub async fn report_file (
1761
State( app ) : State< Shared> ,
1762
headers : HeaderMap,
1763
Path( (id , name ) ) : Path< ( i64 , String) > ,
1764
body: axum:: body:: Bytes,
1765
) -> Result< Response, ApiError> {
1766
runner_auth ( & app, & headers) .await? ;
1767
let job = running_job ( & app, id) .await? ;
1768
let spec: flow_core:: Job =
1769
serde_json:: from_str( & job.spec) .map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " stored job: {e} " ) ) ) ? ;
1770
if ! spec.outputs.iter ( ) .any ( | o | o.name = = name & & o.kind = = flow_core:: OutputKind:: Report ) {
1771
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " job {} declares no report {name:?} " , job.name) ) ) ;
1772
}
1773
let path = headers.get ( " x-flow-report-path" ) .and_then ( | v | v.to_str ( ) .ok ( ) ) .unwrap_or_default ( ) ;
1774
if ! crate :: reports:: path_ok( path) {
1775
return Err ( ApiError( StatusCode:: BAD_REQUEST , format! ( " report file {path:?} : give its path in X-Flow-Report-Path" ) ) ) ;
1776
}
1777
let run = fdb:: run_by_id( & app.db, job.run_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such run" .into ( ) ) ) ? ;
1778
let row =
1779
db:: repos:: get_by_id( & app.db, run.repo_id) .await? .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " no such repository" .into ( ) ) ) ? ;
1780
let repo = RepoName:: new( & row.owner, & row.name) .ok_or_else ( | | ApiError( StatusCode:: NOT_FOUND , " bad repository name" .into ( ) ) ) ? ;
1781
let blob = app
1782
.repos
1783
.write ( & repo, move | r, _ | Ok ( r.store_blob ( & body) ? ) )
1784
.await
1785
.map_err ( | e | ApiError( StatusCode:: INTERNAL_SERVER_ERROR , format! ( " keep the report file: {e} " ) ) ) ? ;
1786
Ok ( Json( serde_json:: json! ( { " blob" : blob.to_hex ( ) } ) ) .into_response ( ) )
0b203bbf Andy McCall 1 month ago ↶
1787
}
1788
1789
1790
async fn read_junit (
1791
app : & Shared,
1792
row : & db:: repos:: RepoRow,
1793
run : & fdb:: Run,
1794
job : & fdb:: Job,
1795
blob : & str ,
1796
bytes : axum:: body:: Bytes,
1797
) -> Result< String, String> {
1798
if bytes.len ( ) > crate :: junit:: MAX_REPORT {
1799
return Err ( format! ( " tests: the report is {} MiB; at most {} MiB is read" , bytes.len ( ) > > 20 , crate :: junit:: MAX_REPORT > > 20 ) ) ;
1800
}
1801
let text = String :: from_utf8( bytes.to_vec ( ) ) .map_err ( | _| " tests: the report is not UTF-8 text (JUnit XML is expected)" .to_string ( ) ) ? ;
1802
let cases = tokio:: task:: spawn_blocking( move | | crate :: junit:: parse( & text) )
1803
.await
1804
.map_err ( | e | format! ( " tests: {e} " ) ) ?
1805
.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
1806
let origin = crate :: junit:: Origin {
1807
repo_id: row.id,
1808
run_id: run.id,
1809
run_number: run.number,
1810
job_id: job.id,
1811
job: & job.name,
1812
bookmark: & run.bookmark,
1813
state: & run.state,
1814
blob,
1815
} ;
1816
crate :: junit:: keep( app, & origin, & cases) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
1817
let r = crate :: junit:: for_run( & app.db, run.id)
1818
.await
1819
.map_err ( | e | format! ( " tests: {e} " ) ) ?
1820
.into_iter ( )
1821
.find ( | r | r.job_id = = job.id )
1822
.ok_or_else ( | | " tests: the report was not kept" .to_string ( ) ) ? ;
3d5c825a Andy McCall 1 month ago ↶
1823
1824
1825
1826
let base = crate :: junit:: baseline( & app.db, & r, & row.default_bookmark) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ;
1827
let newly = match & base {
1828
Some ( b) if r.broken ( ) > 0 => crate :: junit:: newly_failing( & app.db, r.id, b.id) .await.map_err ( | e | format! ( " tests: {e} " ) ) ? ,
1829
_ => vec! [ ] ,
1830
} ;
1831
let ( status, summary) = if r.broken ( ) = = 0 {
1832
( " passed" , format! ( " {} passed" , r.passed) )
1833
} else if let Some ( b) = & base {
1834
let names: Vec< & str > = newly.iter ( ) .take ( 3 ) .map ( String :: as_str) .collect ( ) ;
1835
let more = if newly.len ( ) > 3 { format! ( " , +{} " , newly.len ( ) - 3 ) } else { String :: new( ) } ;
1836
let new = if newly.is_empty ( ) {
1837
format! ( " none new against {} run #{} " , row.default_bookmark, b.run_number)
1838
} else {
1839
format! ( " {} new against {} : {} {more} " , newly.len ( ) , row.default_bookmark, names.join ( " , " ) )
1840
} ;
1841
( " failed" , format! ( " {} failing; {new} " , r.broken ( ) ) )
1842
} else {
1843
( " failed" , format! ( " {} failing" , r.broken ( ) ) )
1844
} ;
1845
let url = format! ( " {} /{} /{} /tests?report={} " , app.config.base_url ( ) , row.owner, row.name, r.id) ;
1846
if let Ok ( c) = db:: hooks:: upsert_check(
1847
& app.db,
1848
row.id,
1849
db:: hooks:: CheckReport { state: & run.state, name: " tests" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
1850
)
1851
.await
1852
{
1853
let api = crate :: api:: check_of( & c) ;
1854
app.events.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
1855
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
1856
}
0b203bbf Andy McCall 1 month ago ↶
1857
Ok ( format! ( " tests: {} in {} " , r.summary ( ) , human_ms ( r.time_ms) ) )
554ccacf Andy McCall 1 month ago ↶
1858
}
1859
1860
1861
async fn read_coverage (
1862
app : & Shared,
1863
repo : & RepoName,
1864
row : & db:: repos:: RepoRow,
1865
run : & fdb:: Run,
1866
job : & fdb:: Job,
1867
blob : & str ,
1868
bytes : axum:: body:: Bytes,
1869
) -> Result< String, String> {
1870
if bytes.len ( ) > crate :: coverage:: MAX_REPORT {
1871
return Err ( format! (
1872
" coverage: the report is {} MiB; at most {} MiB is read" ,
1873
bytes.len ( ) > > 20 ,
1874
crate :: coverage:: MAX_REPORT > > 20
1875
) ) ;
1876
}
7fff30c4 Andy McCall 1 month ago ↶
1877
let text = String :: from_utf8( bytes.to_vec ( ) )
1878
.map_err ( | _| " coverage: the report is not text (LCOV or Cobertura XML is expected)" .to_string ( ) ) ? ;
1879
let ( files, outside, format) = tokio:: task:: spawn_blocking( move | | crate :: coverage:: parse( & text) )
554ccacf Andy McCall 1 month ago ↶
1880
.await
1881
.map_err ( | e | format! ( " coverage: {e} " ) ) ?
1882
.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
1883
let heads: Vec< ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( ChangeHash:: from_hex) .collect ( ) ;
1884
let in_state: std:: collections:: HashSet< String> = app
1885
.repos
1886
.read ( repo, move | r, cache| Ok ( cache.load ( r, & heads) ? .snapshot.files.keys ( ) .cloned ( ) .collect ( ) ) )
1887
.await
1888
.map_err ( | e | format! ( " coverage: the run's files could not be read: {e} " ) ) ? ;
1889
let heads: Vec< String> = run.head_list ( ) .into_iter ( ) .map ( String :: from) .collect ( ) ;
1890
let origin = crate :: coverage:: Origin {
1891
repo_id: row.id,
40dd3f86 Andy McCall 26 days ago ↶
1892
run_id: Some ( run.id) ,
554ccacf Andy McCall 1 month ago ↶
1893
run_number: run.number,
40dd3f86 Andy McCall 26 days ago ↶
1894
job_id: Some ( job.id) ,
1895
uploader: " " ,
554ccacf Andy McCall 1 month ago ↶
1896
job: & job.name,
1897
bookmark: & run.bookmark,
1898
state: & run.state,
1899
heads: & heads,
1900
blob,
7fff30c4 Andy McCall 1 month ago ↶
1901
format,
554ccacf Andy McCall 1 month ago ↶
1902
} ;
1903
let id = crate :: coverage:: keep( app, & origin, files, outside, & in_state) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
1904
let Some ( r) = crate :: coverage:: report( & app.db, row.id, id) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? else {
1905
return Err ( " coverage: the report was not kept" .into ( ) ) ;
1906
} ;
7edaff01 Andy McCall 1 month ago ↶
1907
1908
1909
let base = crate :: coverage:: baseline( & app.db, & r, & row.default_bookmark) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
1910
let rules = crate :: coverage:: rules( & app.db, row.id) .await.map_err ( | e | format! ( " coverage: {e} " ) ) ? ;
1911
let ( status, summary) = crate :: coverage:: verdict( & r, base.as_ref ( ) , & row.default_bookmark, rules) ;
1912
let url = format! ( " {} /{} /{} /coverage?report={} " , app.config.base_url ( ) , row.owner, row.name, r.id) ;
1913
if let Ok ( c) = db:: hooks:: upsert_check(
1914
& app.db,
1915
row.id,
1916
db:: hooks:: CheckReport { state: & run.state, name: " coverage" , status, summary: & summary, url: & url, reporter: None , seal: " " } ,
1917
)
1918
.await
1919
{
1920
let api = crate :: api:: check_of( & c) ;
1921
app.events.proposal ( row.id, hub_api:: ProposalEvent { number: 0 , kind: " check" .into ( ) , check: Some ( api.clone ( ) ) } ) ;
1922
crate :: hooks:: emit( app, row, " check" , " flow" , serde_json:: json! ( { " check" : api } ) ) .await;
1923
}
554ccacf Andy McCall 1 month ago ↶
1924
let pct = r.percent ( ) .map ( | p | format! ( " {p:.1} %" ) ) .unwrap_or_else ( | | " no lines" .into ( ) ) ;
1925
let dropped =
1926
if r.dropped > 0 { format! ( " ({} named files are not in this state and were left out)" , r.dropped) } else { String :: new( ) } ;
1927
Ok ( format! ( " coverage: {pct} of lines ({} of {} ) in {} files{dropped} " , r.lines_hit, r.lines_found, r.files) )
a3c8288b Andy McCall 1 month ago ↶
1928
}
1929
42fa9554 Andy McCall 1 month ago ↶
1930
1931
pub async fn finish (
1932
State( app ) : State< Shared> ,
1933
headers : HeaderMap,
1934
Path( id ) : Path< i64 > ,
1935
Json( f ) : Json< Finish> ,
1936
) -> Result< StatusCode, ApiError> {
b6b7e4cf Andy McCall 1 month ago ↶
1937
let me = runner_auth ( & app, & headers) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1938
let job = running_job ( & app, id) .await? ;
1939
let status = match ( f.exit_code, & f.error) {
1940
( Some ( 0 ) , None ) => " passed" ,
1941
( Some ( _ ) , _ ) => " failed" ,
1942
( None , _ ) => " error" ,
1943
} ;
1944
let ( receipt, receipt_hash) = match & f.receipt {
1945
Some ( r) if r.job_name = = job.name & & Some ( r.exit_code as i32 ) = = f.exit_code => {
d29070fa Andy McCall 1 month ago ↶
1946
1947
1948
1949
let kept: Vec< String> =
1950
if r.exit_code = = 0 { fdb:: artifacts( & app.db, job.id) .await? .into_iter ( ) .map ( | a | a.blob ) .collect ( ) } else { vec! [ ] } ;
a3c8288b Andy McCall 1 month ago ↶
1951
if r.outputs.iter ( ) .map ( hex:: encode) .collect:: < Vec< _ > > ( ) ! = kept {
1952
return Err ( ApiError( StatusCode:: BAD_REQUEST , " the receipt names other outputs than the artifacts uploaded" .into ( ) ) ) ;
1953
}
42fa9554 Andy McCall 1 month ago ↶
1954
( serde_json:: to_string( r) .unwrap_or_default ( ) , hex:: encode( r.hash ( ) ) )
1955
}
1956
Some ( _ ) => return Err ( ApiError( StatusCode:: BAD_REQUEST , " the receipt does not match the job" .into ( ) ) ) ,
1957
None => ( String :: new( ) , String :: new( ) ) ,
1958
} ;
1959
let error = f.error.clone ( ) .unwrap_or_default ( ) ;
44a3d2eb Andy McCall 26 days ago ↶
1960
fdb:: set_peaks( & app.db, job.id, f.peak_disk_mib, f.peak_memory_mib) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1961
let changed = fdb:: finish(
1962
& app.db,
1963
job.id,
1964
fdb:: Outcome { status, exit_code: f.exit_code.map ( i64 :: from) , error: & error, receipt: & receipt, receipt_hash: & receipt_hash } ,
1965
)
1966
.await? ;
1967
if let Some ( run) = fdb:: run_by_id( & app.db, job.run_id) .await? {
1968
if let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? {
cc19697f Andy McCall 1 month ago ↶
1969
if let ( Some ( r) , Some ( signed) ) = ( & f.receipt, & f.signed_receipt) {
b6b7e4cf Andy McCall 1 month ago ↶
1970
let ( attest, signer, note) = match keep_receipt ( & app, & me, & row, & run, r, signed) .await {
cc19697f Andy McCall 1 month ago ↶
1971
Ok ( ( attest, signer) ) => ( attest, signer, String :: new( ) ) ,
1972
Err ( e) => {
1973
tracing:: warn! ( " flow: job {} receipt not kept: {e}" , job.id) ;
1974
( String :: new( ) , String :: new( ) , e)
1975
}
1976
} ;
1977
fdb:: set_receipt( & app.db, job.id, & attest, & signer, & note) .await? ;
1978
}
42fa9554 Andy McCall 1 month ago ↶
1979
}
1980
}
070a7b64 Andy McCall 1 month ago ↶
1981
report_changed ( & app, job.run_id, changed) .await? ;
42fa9554 Andy McCall 1 month ago ↶
1982
Ok ( StatusCode:: NO_CONTENT )
070a7b64 Andy McCall 1 month ago ↶
1983
}
1984
1985
1986
async fn report_changed ( app : & Shared, run_id : i64 , changed : Vec< fdb:: Job> ) -> sqlx:: Result< ( ) > {
1987
let Some ( run) = fdb:: run_by_id( & app.db, run_id) .await? else {
1988
return Ok ( ( ) ) ;
1989
} ;
1990
let Some ( row) = db:: repos:: get_by_id( & app.db, run.repo_id) .await? else {
1991
return Ok ( ( ) ) ;
1992
} ;
4a4400c5 Andy McCall 23 days ago ↶
1993
let mut errored_on: Vec< i64 > = Vec :: new( ) ;
dc59d1b5 Andy McCall 21 days ago ↶
1994
let some_passed = changed.iter ( ) .any ( | j | j.status = = " passed" ) ;
7e01a766 Andy McCall 21 days ago ↶
1995
if ! changed.is_empty ( ) {
1996
app.events.live.publish ( & crate :: events:: topic:: run( run_id) , " jobs" ) ;
1997
}
070a7b64 Andy McCall 1 month ago ↶
1998
for j in changed {
1999
let summary = match j.status.as_str ( ) {
2000
" passed" => format! ( " passed in {} " , duration ( & j) ) ,
2001
" failed" => format! ( " exit {} after {} " , j.exit_code.unwrap_or ( - 1 ) , duration ( & j) ) ,
2002
" skipped" => " skipped: a dependency did not pass" .into ( ) ,
745d225d Andy McCall 1 month ago ↶
2003
" cancelled" => j.error.clone ( ) ,
070a7b64 Andy McCall 1 month ago ↶
2004
_ => format! ( " could not run: {} " , j.error) ,
2005
} ;
2006
report_check ( app, & row, & run.state, run.number, & j.name, check_status ( & j.status) , & summary) .await;
4a4400c5 Andy McCall 23 days ago ↶
2007
if j.status = = " error" {
2008
if let Some ( r) = runner_of ( app, j.id) .await {
2009
errored_on.push ( r) ;
2010
}
2011
}
2012
}
2013
errored_on.dedup ( ) ;
2014
for r in errored_on {
2015
drain_if_erroring ( app, r) .await;
dc59d1b5 Andy McCall 21 days ago ↶
2016
}
2017
if some_passed {
2018
tell_approvers ( app, & row, & run) .await;
4a4400c5 Andy McCall 23 days ago ↶
2019
}
2020
if let Some ( done) = fdb:: run_by_id( & app.db, run_id) .await? {
2021
if done.status = = " failed" & & ! done.live ( ) {
2022
tell_failed ( app, & row, & done) .await;
2023
}
bff9106e Andy McCall 23 days ago ↶
2024
2025
if done.status = = " passed" {
2026
let before: Option< String> = sqlx:: query_scalar(
5d90e0e0 Andy McCall 21 days ago
2027
" SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND id < $3 AND status IN ('passed', 'failed')
bff9106e Andy McCall 23 days ago ↶
2028
ORDER BY id DESC LIMIT 1" ,
2029
)
2030
.bind ( done.repo_id)
2031
.bind ( & done.bookmark)
2032
.bind ( done.id)
2033
.fetch_optional ( & app.db)
2034
.await? ;
2035
2036
let url = format! ( " /{} /{} /flow/{} " , row.owner, row.name, done.number) ;
5d90e0e0 Andy McCall 21 days ago
2037
let failed_here: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM notifications WHERE kind = 'flow.failed' AND url = $1" )
bff9106e Andy McCall 23 days ago ↶
2038
.bind ( & url)
2039
.fetch_one ( & app.db)
2040
.await? ;
2041
if before.as_deref ( ) = = Some ( " failed" ) | | failed_here > 0 {
2042
tell_fixed ( app, & row, & done) .await;
2043
}
2044
}
070a7b64 Andy McCall 1 month ago ↶
2045
}
a7e6e8e6 Andy McCall 25 days ago ↶
2046
2047
2048
if let Some ( done) = fdb:: run_by_id( & app.db, run_id) .await? {
2049
if done.status = = " passed" & & done.bookmark = = row.default_bookmark {
2050
let heads: Vec< ChangeHash> = done.head_list ( ) .iter ( ) .filter_map ( | h | ChangeHash:: from_hex( h) ) .collect ( ) ;
2051
let ( app, row) = ( app.clone ( ) , row.clone ( ) ) ;
2052
tokio:: spawn( async move { crate :: diffusion:: propagate( & app, & row, heads, true ) .await } ) ;
2053
}
f4d6ef41 Andy McCall 24 days ago ↶
2054
2055
2056
if let Some ( path) = done.bookmark.strip_prefix ( " diffusion/" ) .filter ( | _| done.status = = " passed" ) {
2057
tokio:: spawn( crate :: diffusion:: try_land( app.clone ( ) , row.clone ( ) , path.to_string ( ) ) ) ;
2058
}
a7e6e8e6 Andy McCall 25 days ago ↶
2059
}
070a7b64 Andy McCall 1 month ago ↶
2060
Ok ( ( ) )
4a4400c5 Andy McCall 23 days ago ↶
2061
}
2062
2063
2064
2065
pub const ACCOUNT : & str = " oxygenflow" ;
2066
2067
async fn flow_account ( pool : & db:: Pool) -> Option< db:: accounts:: Account> {
2068
if let Ok ( Some ( a) ) = db:: accounts:: account_by_name( pool, ACCOUNT ) .await {
2069
return Some ( a) ;
2070
}
2071
db:: accounts:: create_account( pool, ACCOUNT , " oxygenflow@localhost" , " !" , " " , false ) .await.ok ( )
2072
}
2073
2074
async fn runner_of ( app : & Shared, job_id : i64 ) -> Option< i64 > {
5d90e0e0 Andy McCall 21 days ago
2075
sqlx:: query_scalar( " SELECT runner_id FROM flow_jobs WHERE id = $1" ) .bind ( job_id) .fetch_optional ( & app.db) .await.ok ( ) .flatten ( ) .flatten ( )
4a4400c5 Andy McCall 23 days ago ↶
2076
}
2077
2078
2079
2080
2081
async fn tell_failed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
2082
let Some ( me) = flow_account ( & app.db) .await else { return } ;
2083
let jobs = fdb:: jobs( & app.db, run.id) .await.unwrap_or_default ( ) ;
2084
let broken: Vec< String> = jobs.iter ( ) .filter ( | j | matches! ( j.status.as_str ( ) , " failed" | " error" ) ) .map ( | j | j.name.clone ( ) ) .collect ( ) ;
2085
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
2086
let on = if run.bookmark.is_empty ( ) { String :: new( ) } else { format! ( " on {} " , run.bookmark) } ;
2087
crate :: notify:: deliver(
2088
app,
2089
crate :: notify:: Event {
2090
kind: " flow.failed" ,
2091
title: format! ( " {} /{} : run #{} {on} failed ({} )" , row.owner, row.name, run.number, broken.join ( " , " ) ) ,
bff9106e Andy McCall 23 days ago ↶
2092
url: format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ,
2093
actor: & me,
2094
repo_id: row.id,
2095
thread: None ,
2096
mention_text: " " ,
2097
direct: pusher.into_iter ( ) .map ( | a | a.id ) .collect ( ) ,
2098
} ,
2099
)
2100
.await;
2101
}
2102
2103
2104
2105
async fn tell_fixed ( app : & Shared, row : & db:: repos:: RepoRow, run : & fdb:: Run) {
2106
let Some ( me) = flow_account ( & app.db) .await else { return } ;
2107
let pusher = db:: accounts:: account_by_name( & app.db, & run.actor) .await.ok ( ) .flatten ( ) ;
2108
let on = if run.bookmark.is_empty ( ) { String :: new( ) } else { format! ( " on {} " , run.bookmark) } ;
2109
crate :: notify:: deliver(
2110
app,
2111
crate :: notify:: Event {
2112
kind: " flow.fixed" ,
2113
title: format! ( " {} /{} : run #{} {on} passed: fixed" , row.owner, row.name, run.number) ,
4a4400c5 Andy McCall 23 days ago ↶
2114
url: format! ( " /{} /{} /flow/{} " , row.owner, row.name, run.number) ,
2115
actor: & me,
2116
repo_id: row.id,
2117
thread: None ,
2118
mention_text: " " ,
2119
direct: pusher.into_iter ( ) .map ( | a | a.id ) .collect ( ) ,
2120
} ,
2121
)
2122
.await;
2123
}
2124
2125
2126
2127
2128
async fn drain_if_erroring ( app : & Shared, runner_id : i64 ) {
2129
let n = app.config.flow.drain_after_errors;
2130
let Ok ( Some ( last) ) = db:: runners:: erroring( & app.db, runner_id, n) .await else { return } ;
2131
let Ok ( Some ( r) ) = db:: runners:: get( & app.db, runner_id) .await else { return } ;
2132
if r.state ! = " active" | | db:: runners:: set_state( & app.db, runner_id, " draining" ) .await.is_err ( ) {
2133
return ;
2134
}
2135
tracing:: warn! ( " flow: runner {} drained after {n} errors in a row (last: {last})" , r.name) ;
2136
let Some ( me) = flow_account ( & app.db) .await else { return } ;
ea14c4ec Andy McCall 21 days ago ↶
2137
let admins: Vec< i64 > = sqlx:: query_scalar( " SELECT id FROM accounts WHERE is_admin = TRUE" ) .fetch_all ( & app.db) .await.unwrap_or_default ( ) ;
4a4400c5 Andy McCall 23 days ago ↶
2138
crate :: notify:: deliver(
2139
app,
2140
crate :: notify:: Event {
2141
kind: " flow.runner_drained" ,
2142
title: format! ( " Runner {} drained: its last {n} jobs ended in errors (last: {last} )" , r.name) ,
2143
url: format! ( " /admin/runners/{runner_id} " ) ,
2144
actor: & me,
2145
repo_id: 0 ,
2146
thread: None ,
2147
mention_text: " " ,
2148
direct: admins,
2149
} ,
2150
)
2151
.await;
070a7b64 Andy McCall 1 month ago ↶
2152
}
2153
5dead1f4 Andy McCall 1 month ago ↶
2154
2155
2156
pub struct Limits {
2157
2158
pub default_s : u64 ,
2159
2160
pub max_s : u64 ,
2161
2162
2163
2164
2165
pub grace_ms : i64 ,
2166
}
2167
2168
pub const SETTING_DEFAULT : & str = " flow.job_timeout_minutes" ;
2169
pub const SETTING_MAX : & str = " flow.job_timeout_max_minutes" ;
2170
pub const SETTING_GRACE : & str = " flow.lost_grace_minutes" ;
2171
2172
pub async fn limits ( app : & Shared) -> Limits {
2173
let minutes = | key : & 'static str | async move {
2174
db:: accounts:: setting( & app.db, key) .await.ok ( ) .flatten ( ) .and_then ( | v | v.parse:: < u64 > ( ) .ok ( ) ) .filter ( | m | * m > 0 )
2175
} ;
2176
let default_m = minutes ( SETTING_DEFAULT ) .await.unwrap_or ( app.config.flow.job_timeout_minutes.max ( 1 ) ) ;
2177
let max_m = minutes ( SETTING_MAX ) .await.unwrap_or ( default_m.max ( 6 * 60 ) ) .max ( default_m) ;
2178
let grace_m = minutes ( SETTING_GRACE ) .await.unwrap_or ( 5 ) ;
2179
Limits { default_s: default_m * 60 , max_s: max_m * 60 , grace_ms: grace_m as i64 * 60_000 }
2180
}
070a7b64 Andy McCall 1 month ago ↶
2181
5dead1f4 Andy McCall 1 month ago ↶
2182
070a7b64 Andy McCall 1 month ago ↶
2183
2184
2185
pub async fn reap_lost_jobs ( app : & Shared) -> sqlx:: Result< usize > {
5dead1f4 Andy McCall 1 month ago ↶
2186
let limits = limits ( app) .await;
2187
let lost = fdb:: running_past_limit( & app.db, db:: now_ms( ) , limits.default_s, limits.grace_ms) .await? ;
070a7b64 Andy McCall 1 month ago ↶
2188
let n = lost.len ( ) ;
2189
for job in lost {
2190
let error = format! (
2191
" lost: still running {} minutes after it started, past the time limit; the runner{} stopped reporting it" ,
2192
( db:: now_ms( ) - job.started_ms.unwrap_or ( 0 ) ) / 60_000 ,
2193
if job.runner.is_empty ( ) { String :: new( ) } else { format! ( " {} " , job.runner) } ,
2194
) ;
2195
let changed =
2196
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
2197
.await? ;
2198
tracing:: warn! ( " flow: job {} {error}" , job.id) ;
2199
report_changed ( app, job.run_id, changed) .await? ;
2200
}
2201
Ok ( n)
eef677c4 Andy McCall 1 month ago ↶
2202
}
2203
2204
2205
2206
2207
async fn end_jobs_not_held ( app : & Shared, runner : & db:: runners:: Runner, holding : & [i64 ]) -> sqlx:: Result< ( ) > {
2208
for job in fdb:: running_on( & app.db, runner.id) .await? {
2209
if holding.contains ( & job.id) {
2210
continue ;
2211
}
2212
let error = format! ( " lost: the runner {} no longer holds this job (it restarted, or never received it)" , runner.name) ;
2213
let changed =
2214
fdb:: finish( & app.db, job.id, fdb:: Outcome { status: " error" , exit_code: None , error: & error, receipt: " " , receipt_hash: " " } )
2215
.await? ;
2216
tracing:: warn! ( " flow: job {} {error}" , job.id) ;
2217
report_changed ( app, job.run_id, changed) .await? ;
2218
}
2219
Ok ( ( ) )
348f8c4b Andy McCall 1 month ago ↶
2220
}
2221
2222
2223
2224
2225
pub async fn run_due ( app : & Shared, when : flow_core:: pipeline:: When) -> usize {
2226
let Ok ( rows) = db:: repos:: list( & app.db) .await else { return 0 } ;
2227
let mut started = 0 ;
2228
for row in rows.into_iter ( ) .filter ( | r | enabled ( app, r) ) {
2229
let Some ( repo) = RepoName:: new( & row.owner, & row.name) else { continue } ;
2230
let b = row.default_bookmark.clone ( ) ;
2231
let found = app
2232
.repos
2233
.read ( & repo, move | r, _ | {
2234
let Some ( st) = r.view ( ) ? .bookmarks.get ( & b) .copied ( ) else { return Ok ( None ) } ;
2235
Ok ( Some ( ( st, r.store ( ) .get_state ( & st) ? .heads) ) )
2236
} )
2237
.await;
2238
let Ok ( Some ( ( state, heads) ) ) = found else { continue } ;
2239
if heads.is_empty ( ) {
2240
continue ;
2241
}
0650d94e Andy McCall 26 days ago ↶
2242
match start_run_at ( app, & row, & row.default_bookmark, & state.to_hex ( ) , & heads, & row.owner, " schedule" , None , Some ( when) , & [ ] ) .await {
348f8c4b Andy McCall 1 month ago ↶
2243
Ok ( Some ( _ ) ) => started + = 1 ,
2244
Ok ( None ) => { }
2245
Err ( e) => tracing:: warn! ( " flow: scheduled run of {}/{}: {e}" , row.owner, row.name) ,
2246
}
2247
}
2248
started
2249
}
2250
2251
2252
pub fn when_now ( ) -> flow_core:: pipeline:: When {
2253
let now = time:: OffsetDateTime:: now_utc( ) ;
2254
flow_core:: pipeline:: When {
2255
minute: now.minute ( ) as u32 ,
2256
hour: now.hour ( ) as u32 ,
2257
day: now.day ( ) as u32 ,
2258
month: u8 :: from( now.month ( ) ) as u32 ,
2259
weekday: now.weekday ( ) .number_days_from_sunday ( ) as u32 ,
2260
}
2261
}
2262
2263
2264
pub async fn scheduler ( app : Shared) {
2265
let mut last = None ;
2266
loop {
2267
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 15 ) ) .await;
2268
let minute = time:: OffsetDateTime:: now_utc( ) .unix_timestamp ( ) / 60 ;
2269
if last = = Some ( minute) {
2270
continue ;
2271
}
2272
last = Some ( minute) ;
2273
let n = run_due ( & app, when_now ( ) ) .await;
2274
if n > 0 {
2275
tracing:: info! ( " flow: {n} scheduled run(s) started" ) ;
2276
}
2277
}
070a7b64 Andy McCall 1 month ago ↶
2278
}
2279
2280
2281
pub async fn reaper ( app : Shared) {
2282
loop {
2283
tokio:: time:: sleep( std:: time:: Duration:: from_secs( 60 ) ) .await;
2284
if let Err ( e) = reap_lost_jobs ( & app) .await {
2285
tracing:: warn! ( " flow: looking for lost jobs: {e}" ) ;
417184c7 Andy McCall 22 days ago ↶
2286
}
2287
match expire_approvals ( & app, db:: now_ms( ) ) .await {
2288
Ok ( n) if n > 0 => tracing:: info! ( " flow: {n} approvals expired" ) ,
2289
Ok ( _ ) => { }
2290
Err ( e) => tracing:: warn! ( " flow: expiring approvals: {e}" ) ,
070a7b64 Andy McCall 1 month ago ↶
2291
}
2292
}
cc19697f Andy McCall 1 month ago ↶
2293
}
2294
2295
2296
2297
2298
2299
2300
pub async fn keep_receipt (
2301
app : & Shared,
b6b7e4cf Andy McCall 1 month ago ↶
2302
runner : & db:: runners:: Runner,
cc19697f Andy McCall 1 month ago ↶
2303
row : & db:: repos:: RepoRow,
2304
run : & fdb:: Run,
2305
receipt : & flow_core:: Receipt,
2306
signed_hex : & str ,
2307
) -> Result< ( String, String) , String> {
2308
use oxy_core:: receipt:: SignedReceipt;
2309
let bytes = hex:: decode( signed_hex) .map_err ( | _| " the signed receipt is not hex" .to_string ( ) ) ? ;
2310
let sr = SignedReceipt:: decode( & bytes) .map_err ( | e | format! ( " not a signed receipt: {e} " ) ) ? ;
2311
sr.verify ( ) .map_err ( | e | format! ( " it does not verify: {e} " ) ) ? ;
2312
if sr.receipt.hash ( ) .0 ! = receipt.hash ( ) {
2313
return Err ( " it signs another receipt than the job's" .into ( ) ) ;
2314
}
2315
let heads: Vec< oxy_core:: hash:: ChangeHash> = run.head_list ( ) .into_iter ( ) .filter_map ( oxy_core:: hash:: ChangeHash:: from_hex) .collect ( ) ;
2316
let state = oxy_core:: hash:: hash_object( oxy_core:: hash:: ObjectType:: State, & oxy_core:: objects:: StateObject:: new( heads) .encode ( ) ) ;
2317
if sr.state ( ) ! = state {
2318
return Err ( " it is for another state than the run's" .into ( ) ) ;
2319
}
2c31915f Andy McCall 20 days ago ↶
2320
let signer = match & sr.certificate {
2321
2322
2323
Some ( c) => {
2324
let cert = & c.cert.cert;
2325
if cert.issuer ! = app.hub_key.key | | c.issuer.hash ( ) ! = app.hub_key.key {
2326
return Err ( " its certificate was not issued by this hub" .into ( ) ) ;
2327
}
2328
let subject = format! ( " repo:{} /{} :bookmark:{} " , row.owner, row.name, run.bookmark) ;
2329
if cert.subject ! = subject {
2330
return Err ( format! ( " its certificate is for {} , not {subject} " , cert.subject) ) ;
2331
}
2332
if cert.claim ( " run" ) ! = Some ( run.number.to_string ( ) .as_str ( ) ) | | cert.claim ( " job" ) ! = Some ( sr.receipt.job_name.as_str ( ) ) {
2333
return Err ( " its certificate is for another run or job" .into ( ) ) ;
2334
}
2335
2336
if ! cert.covers ( oxy_core:: identity:: Scope:: SEAL , db:: now_ms( ) as u64 ) {
2337
return Err ( " its certificate does not cover now: certify the key just before signing the receipt" .into ( ) ) ;
2338
}
2339
format! ( " {} (short-lived key)" , runner.name)
2340
}
2341
None => {
2342
let key = sr.runner.hash ( ) .to_hex ( ) ;
2343
let bound = db:: runners:: bind_key( & app.db, runner.id, & key, & sr.runner.name) .await.map_err ( | e | e.to_string ( ) ) ? ;
2344
if bound ! = key {
2345
return Err ( format! ( " signed by {} , not the key this runner signs with ({} )" , & key[ .. 12 ] , & bound[ .. 12. min ( bound.len ( ) ) ] ) ) ;
2346
}
2347
sr.runner.name.clone ( )
2348
}
2349
} ;
cc19697f Andy McCall 1 month ago ↶
2350
let name = RepoName:: new( & row.owner, & row.name) .ok_or ( " bad repository name" ) ? ;
965b950f Andy McCall 20 days ago ↶
2351
let ( rid, signers) = ( sr.attest.repo, sr.sigs.iter ( ) .map ( | s | s.key ) .collect:: < Vec< _ > > ( ) ) ;
2352
let cert = sr.certificate.as_ref ( ) .map ( | c | ( c.cert.cert.hash ( ) , c.cert.cert.issuer) ) ;
cc19697f Andy McCall 1 month ago ↶
2353
let attest = app.repos.write ( & name, move | repo, _ | Ok ( repo.add_receipt ( & sr) ? ) ) .await.map_err ( | e | e.to_string ( ) ) ? ;
965b950f Andy McCall 20 days ago ↶
2354
2355
2356
let mut entries = vec! [ crate :: tlog:: Pending { kind: " receipt" , repo: rid, repo_row: Some ( row.id) , object: attest, signers } ] ;
2357
if let Some ( ( c, issuer) ) = cert {
2358
entries.push ( crate :: tlog:: Pending { kind: " certificate" , repo: rid, repo_row: Some ( row.id) , object: c, signers: vec! [ issuer] } ) ;
2359
}
2360
crate :: tlog:: log( app, entries) .await;
cc19697f Andy McCall 1 month ago ↶
2361
Ok ( ( attest.to_hex ( ) , signer) )
42fa9554 Andy McCall 1 month ago ↶
2362
}
2363
2364
2365
pub fn duration ( j : & fdb:: Job) -> String {
2366
match ( j.started_ms, j.finished_ms) {
2367
( Some ( a) , Some ( b) ) => human_ms ( b - a) ,
2368
( Some ( a) , None ) => human_ms ( db:: now_ms( ) - a) ,
2369
_ => " –" .into ( ) ,
2370
}
2371
}
2372
2373
pub fn human_ms ( ms : i64 ) -> String {
2374
let ms = ms.max ( 0 ) ;
2375
if ms < 1000 {
2376
format! ( " {ms} ms" )
2377
} else if ms < 60_000 {
2378
format! ( " {:.1} s" , ms as f64 / 1000. 0 )
2379
} else {
2380
format! ( " {} min {} s" , ms / 60_000 , ( ms % 60_000 ) / 1000 )
2381
}
2382
}
bf3d0754 Andy McCall 1 month ago ↶
2383
2384
2385
2386
2387
# [ derive ( Clone, Debug, Default, PartialEq, Eq ) ]
2388
pub struct QueueInfo {
2389
2390
pub position : Option< usize > ,
2391
2392
pub running : Vec< String> ,
2393
pub ahead : Vec< String> ,
2394
2395
pub hidden : usize ,
2396
}
2397
2398
impl QueueInfo {
2399
2400
pub fn place ( & self ) -> Option< String> {
2401
self .position.map ( | n | {
2402
let suffix = match ( n % 10 , n % 100 ) {
2403
( 1 , 11 ) | ( 2 , 12 ) | ( 3 , 13 ) => " th" ,
2404
( 1 , _ ) => " st" ,
2405
( 2 , _ ) => " nd" ,
2406
( 3 , _ ) => " rd" ,
2407
_ => " th" ,
2408
} ;
2409
format! ( " {n} {suffix} in line" )
2410
} )
2411
}
2412
}
2413
2414
2415
pub fn queue_info ( queue : & [fdb:: QueueEntry], repo_id : i64 , run_id : i64 ) -> QueueInfo {
2416
let mut info = QueueInfo:: default( ) ;
2417
let first = queue.iter ( ) .filter ( | e | e.status = = " queued" ) .position ( | e | e.run_id = = run_id ) ;
2418
info.position = first.map ( | i | i + 1 ) ;
2419
let label = | e : & fdb:: QueueEntry| format! ( " {} /{} #{} {} " , e.owner, e.name, e.run_number, e.job) ;
2420
let mut queued_seen = 0 ;
2421
for e in queue {
2422
if e.run_id = = run_id {
2423
continue ;
2424
}
2425
let ahead = match e.status.as_str ( ) {
2426
" running" => true ,
2427
_ => {
2428
queued_seen + = 1 ;
2429
first.is_some_and ( | f | queued_seen < = f )
2430
}
2431
} ;
2432
if ! ahead {
2433
continue ;
2434
}
2435
if e.repo_id ! = repo_id & & e.visibility ! = " public" {
2436
info.hidden + = 1 ;
2437
} else if e.status = = " running" {
2438
info.running.push ( label ( e) ) ;
2439
} else {
2440
info.ahead.push ( label ( e) ) ;
2441
}
2442
}
2443
info
2444
}
2445
2446
# [ cfg ( test ) ]
2447
mod queue_tests {
2448
use super :: * ;
2449
2450
fn e ( job_id : i64 , run_id : i64 , repo_id : i64 , vis : & str , job : & str , status : & str ) -> fdb:: QueueEntry {
2451
fdb:: QueueEntry {
2452
job_id,
2453
run_id,
2454
repo_id,
2455
owner: " o" .into ( ) ,
2456
name: format! ( " r{repo_id} " ) ,
2457
visibility: vis.into ( ) ,
2458
run_number: run_id,
2459
job: job.into ( ) ,
2460
status: status.into ( ) ,
b66e70b7 Andy McCall 1 month ago ↶
2461
runner: String :: new( ) ,
c4c87f48 Andy McCall 1 month ago ↶
2462
spec: String :: new( ) ,
2463
sandbox: String :: new( ) ,
e9b9619c Andy McCall 21 days ago ↶
2464
started_ms: None ,
107eaba5 Andy McCall 21 days ago ↶
2465
priority_rank: 2 ,
2466
priority_note: String :: new( ) ,
bf3d0754 Andy McCall 1 month ago ↶
2467
}
2468
}
2469
2470
# [ test ]
2471
fn a_run_learns_its_place_and_what_is_ahead_of_it ( ) {
2472
2473
let q = vec! [
2474
e ( 1 , 10 , 1 , " public" , " build" , " running" ) ,
2475
e ( 2 , 11 , 2 , " private" , " test" , " running" ) ,
2476
e ( 3 , 10 , 1 , " public" , " test" , " queued" ) ,
2477
e ( 4 , 12 , 3 , " private" , " lint" , " queued" ) ,
2478
e ( 5 , 13 , 4 , " public" , " build" , " queued" ) ,
2479
e ( 6 , 13 , 4 , " public" , " test" , " queued" ) ,
2480
e ( 7 , 14 , 5 , " public" , " late" , " queued" ) ,
2481
] ;
2482
let info = queue_info ( & q, 4 , 13 ) ;
2483
assert_eq! ( info.position, Some ( 3 ) ) ;
2484
assert_eq! ( info.place ( ) .as_deref ( ) , Some ( " 3rd in line" ) ) ;
2485
assert_eq! ( info.running, [ " o/r1 #10 build" ] ) ;
2486
assert_eq! ( info.ahead, [ " o/r1 #10 test" ] , " queued ahead, not behind" ) ;
2487
assert_eq! ( info.hidden, 2 , " private repositories are counted, not named" ) ;
2488
2489
assert_eq! ( queue_info ( & q, 3 , 12 ) .ahead, [ " o/r1 #10 test" ] ) ;
2490
assert_eq! ( queue_info ( & q, 3 , 12 ) .position, Some ( 2 ) ) ;
2491
2492
assert_eq! ( queue_info ( & q, 1 , 99 ) .position, None ) ;
2493
let ordinal = | n | QueueInfo { position: Some ( n) , .. Default :: default( ) } .place ( ) .unwrap ( ) ;
2494
assert_eq! (
2495
[ ordinal ( 1 ) , ordinal ( 2 ) , ordinal ( 11 ) , ordinal ( 22 ) , ordinal ( 113 ) ] ,
2496
[ " 1st in line" , " 2nd in line" , " 11th in line" , " 22nd in line" , " 113th in line" ]
4d2e44d7 Andy McCall 1 month ago ↶
2497
) ;
2498
}
2499
}
2500
2501
# [ cfg ( test ) ]
2502
mod placement_tests {
2503
use super :: * ;
2504
2505
# [ test ]
2506
fn a_waiting_job_says_what_each_runner_lacks ( ) {
2507
use crate :: db:: runners:: Runner;
2508
let rootfs = " ab" .repeat ( 32 ) ;
2509
let job = fdb:: Job {
2510
id: 7 ,
2511
runtime: " syscall" .into ( ) ,
2512
sandbox: serde_json:: json! ( { " rootfs" : rootfs, " memory_mib" : 4096 , " cpus" : 2 } ) .to_string ( ) ,
2513
spec: serde_json:: json! ( { " name" : " build" } ) .to_string ( ) ,
2514
.. Default :: default( )
2515
} ;
2516
let now = db:: now_ms( ) ;
2517
let runner = | id : i64 , name : & str | Runner {
2518
id,
2519
name: name.into ( ) ,
2520
state: " active" .into ( ) ,
2521
last_seen_ms: Some ( now) ,
2522
arch: " x86_64" .into ( ) ,
2523
tiers: " microvm,syscall" .into ( ) ,
2524
images: serde_json:: to_string( & [ & rootfs] ) .unwrap ( ) ,
2525
memory_mib: Some ( 16384 ) ,
2526
cpus: Some ( 8 ) ,
2527
.. Default :: default( )
2528
} ;
2529
let archs = std:: collections:: HashMap:: new( ) ;
92a797b2 Andy McCall 26 days ago ↶
2530
let why = | runners : & [Runner], declines : & [fdb:: DeclineRow]| {
2531
why_waiting ( & job, " andy" , 1 , runners, & archs, declines, now, & Default :: default( ) )
2532
} ;
4d2e44d7 Andy McCall 1 month ago ↶
2533
assert_eq! ( why ( & [ ] , & [ ] ) , " no runner is online" ) ;
2534
assert_eq! ( why ( & [ runner ( 1 , " earth-1" ) ] , & [ ] ) , " " , " one can take it: it only waits its turn" ) ;
2535
let microvm_only = Runner { tiers: " microvm" .into ( ) , .. runner ( 1 , " earth-1" ) } ;
2536
let riscv = Runner { arch: " riscv64" .into ( ) , .. runner ( 2 , " narvi" ) } ;
2537
let small = Runner { memory_mib: Some ( 2048 ) , .. runner ( 3 , " pi" ) } ;
2538
let other = Runner { scope_owner: " bob" .into ( ) , .. runner ( 4 , " bobs" ) } ;
2539
let offline = Runner { last_seen_ms: Some ( now - 3_600_000 ) , .. runner ( 5 , " gone" ) } ;
2540
assert_eq! (
2541
why ( & [ microvm_only, small, other, offline] , & [ ] ) ,
2542
" no runner can take it: earth-1 has no syscall tier (it offers microvm); pi has 2048 MiB; the job needs 4096 MiB; bobs serves other repositories"
2543
) ;
2544
let archs_known: std:: collections:: HashMap< String, String> = [ ( rootfs.clone ( ) , " x86_64" .to_string ( ) ) ] .into ( ) ;
2545
assert_eq! (
92a797b2 Andy McCall 26 days ago ↶
2546
why_waiting ( & job, " andy" , 1 , & [ riscv] , & archs_known, & [ ] , now, & Default :: default( ) ) ,
4d2e44d7 Andy McCall 1 month ago ↶
2547
" no runner can take it: narvi is riscv64; the job needs x86_64"
2548
) ;
2549
let without = Runner { images: " []" .into ( ) , .. runner ( 6 , " new" ) } ;
2550
assert_eq! ( why ( & [ without] , & [ ] ) , " no runner can take it: new does not hold image abababababab" ) ;
2551
let d = fdb:: DeclineRow {
2552
job_id: 7 ,
2553
runner_id: 1 ,
2554
runner: " earth-1" .into ( ) ,
2555
reason: " only 512 MiB of memory is free here" .into ( ) ,
2556
ms: now,
2557
} ;
2558
assert_eq! (
2559
why ( & [ runner ( 1 , " earth-1" ) ] , std:: slice:: from_ref( & d) ) ,
2560
" no runner can take it: earth-1 declined it: only 512 MiB of memory is free here"
bf3d0754 Andy McCall 1 month ago ↶
2561
) ;
4d2e44d7 Andy McCall 1 month ago ↶
2562
let old = fdb:: DeclineRow { ms: now - fdb:: DECLINE_HOLDS_MS - 1 , .. d } ;
2563
assert_eq! ( why ( & [ runner ( 1 , " earth-1" ) ] , & [ old] ) , " " , " an old decline no longer holds" ) ;
bf3d0754 Andy McCall 1 month ago ↶
2564
}
2565
}