b6b7e4cf Andy McCall 1 month ago ↶
1
2
3
4
use super :: { now_ms, Pool} ;
5
use sqlx:: FromRow;
6
4d2e44d7 Andy McCall 1 month ago ↶
7
# [ derive ( Clone, Debug, Default, FromRow ) ]
b6b7e4cf Andy McCall 1 month ago ↶
8
pub struct Runner {
9
pub id : i64 ,
10
pub name : String,
11
pub token_hash : String,
12
pub source : String,
13
pub key : String,
14
pub key_name : String,
15
pub arch : String,
16
pub os : String,
17
pub version : String,
18
pub labels : String,
19
pub max_jobs : i64 ,
20
pub memory_mib : Option< i64 > ,
21
pub cpus : Option< i64 > ,
22
pub state : String,
23
pub created_ms : i64 ,
24
pub created_by : String,
25
pub last_seen_ms : Option< i64 > ,
74be9796 Andy McCall 1 month ago ↶
26
27
pub scope_owner : String,
28
29
pub scope_repo_id : Option< i64 > ,
7f5f6c05 Andy McCall 1 month ago ↶
30
31
pub admin_labels : String,
32
33
pub timeout_minutes : Option< i64 > ,
92a797b2 Andy McCall 25 days ago ↶
34
35
pub fetches_images : bool ,
2fb70d01 Andy McCall 24 days ago ↶
36
37
38
pub free_disk_mib : Option< i64 > ,
39
pub load_x100 : Option< i64 > ,
40
pub speed : Option< i64 > ,
41
pub speed_class : Option< i64 > ,
42
pub kernel : String,
4d2e44d7 Andy McCall 1 month ago ↶
43
44
pub tiers : String,
45
46
pub images : String,
47
48
pub profile : String,
b6b7e4cf Andy McCall 1 month ago ↶
49
}
50
74be9796 Andy McCall 1 month ago ↶
51
52
# [ derive ( Clone, Debug, Default, PartialEq, Eq ) ]
53
pub struct Scope {
54
55
pub owner : String,
56
57
pub repo_id : Option< i64 > ,
58
}
59
60
impl Scope {
61
pub fn is_site ( & self ) -> bool {
62
self .owner.is_empty ( ) & & self .repo_id.is_none ( )
63
}
64
}
65
b6b7e4cf Andy McCall 1 month ago ↶
66
67
pub const ONLINE_MS : i64 = 90_000 ;
68
69
impl Runner {
70
pub fn online ( & self ) -> bool {
71
self .last_seen_ms.is_some_and ( | t | now_ms ( ) - t < ONLINE_MS )
72
}
73
74
75
pub fn status ( & self ) -> & 'static str {
76
match self .state.as_str ( ) {
77
" disabled" => " disabled" ,
78
" draining" => " draining" ,
79
_ if self .online ( ) => " online" ,
80
_ => " offline" ,
81
}
82
}
83
7f5f6c05 Andy McCall 1 month ago ↶
84
85
b6b7e4cf Andy McCall 1 month ago ↶
86
pub fn label_list ( & self ) -> Vec< & str > {
7f5f6c05 Andy McCall 1 month ago ↶
87
let mut out: Vec< & str > = self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( ) ;
88
for l in self .admin_label_list ( ) {
89
if ! out.contains ( & l) {
90
out.push ( l) ;
91
}
92
}
93
out
94
}
95
96
pub fn admin_label_list ( & self ) -> Vec< & str > {
97
self .admin_labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
98
}
99
100
101
pub fn reported_label_list ( & self ) -> Vec< & str > {
b6b7e4cf Andy McCall 1 month ago ↶
102
self .labels.split ( ' ,' ) .filter ( | l | ! l.is_empty ( ) ) .collect ( )
103
}
74be9796 Andy McCall 1 month ago ↶
104
105
pub fn scope ( & self ) -> Scope {
106
Scope { owner: self .scope_owner.clone ( ) , repo_id: self .scope_repo_id }
107
}
b6b7e4cf Andy McCall 1 month ago ↶
108
}
109
110
pub async fn by_token ( pool : & Pool, token_hash : & str ) -> sqlx:: Result< Option< Runner> > {
111
sqlx:: query_as( " SELECT * FROM flow_runners WHERE token_hash = ?" ) .bind ( token_hash) .fetch_optional ( pool) .await
112
}
113
114
pub async fn get ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Runner> > {
115
sqlx:: query_as( " SELECT * FROM flow_runners WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
116
}
117
118
pub async fn list ( pool : & Pool) -> sqlx:: Result< Vec< Runner> > {
119
sqlx:: query_as( " SELECT * FROM flow_runners ORDER BY name, id" ) .fetch_all ( pool) .await
120
}
121
122
123
pub async fn configured ( pool : & Pool, token_hash : & str , name : & str ) -> sqlx:: Result< Runner> {
124
sqlx:: query( " INSERT OR IGNORE INTO flow_runners (name, token_hash, source, created_ms) VALUES (?, ?, 'config', ?)" )
125
.bind ( name)
126
.bind ( token_hash)
127
.bind ( now_ms ( ) )
128
.execute ( pool)
129
.await? ;
130
Ok ( by_token ( pool, token_hash) .await? .expect ( " just made" ) )
131
}
132
133
134
7f5f6c05 Andy McCall 1 month ago ↶
135
136
pub async fn set_limits ( pool : & Pool, id : i64 , admin_labels : & str , timeout_minutes : Option< i64 > ) -> sqlx:: Result< ( ) > {
137
sqlx:: query( " UPDATE flow_runners SET admin_labels = ?, timeout_minutes = ? WHERE id = ?" )
138
.bind ( admin_labels)
139
.bind ( timeout_minutes)
140
.bind ( id)
141
.execute ( pool)
142
.await? ;
143
Ok ( ( ) )
144
}
145
b6b7e4cf Andy McCall 1 month ago ↶
146
pub async fn seen ( pool : & Pool, id : i64 , name : Option< & str > , info : Option< & flow_core:: api:: RunnerInfo> ) -> sqlx:: Result< ( ) > {
147
let now = now_ms ( ) ;
148
match info {
149
Some ( i) => {
150
sqlx:: query(
151
" UPDATE flow_runners SET arch = ?, os = ?, version = ?, labels = ?, max_jobs = ?, memory_mib = ?, cpus = ?, last_seen_ms = ? WHERE id = ?" ,
152
)
153
.bind ( & i.arch)
154
.bind ( & i.os)
155
.bind ( & i.version)
156
.bind ( i.labels.join ( " ," ) )
157
.bind ( i.max_jobs as i64 )
158
.bind ( i.memory_mib.map ( i64 :: from) )
159
.bind ( i.cpus.map ( i64 :: from) )
160
.bind ( now)
161
.bind ( id)
162
.execute ( pool)
163
.await? ;
164
}
165
None => {
166
sqlx:: query( " UPDATE flow_runners SET last_seen_ms = ? WHERE id = ?" ) .bind ( now) .bind ( id) .execute ( pool) .await? ;
167
}
168
}
169
if let Some ( n) = name {
170
sqlx:: query( " UPDATE flow_runners SET name = ? WHERE id = ? AND source = 'config'" ) .bind ( n) .bind ( id) .execute ( pool) .await? ;
171
}
4d2e44d7 Andy McCall 1 month ago ↶
172
Ok ( ( ) )
173
}
174
2fb70d01 Andy McCall 24 days ago ↶
175
176
pub async fn set_facts ( pool : & Pool, id : i64 , f : & flow_core:: api:: Facts) -> sqlx:: Result< ( ) > {
177
sqlx:: query( " UPDATE flow_runners SET free_disk_mib = ?, load_x100 = ?, speed = ?, speed_class = ?, kernel = ? WHERE id = ?" )
178
.bind ( f.free_disk_mib.map ( | v | v.min ( i64 :: MAX as u64 ) as i64 ) )
179
.bind ( f.load_x100.map ( i64 :: from) )
180
.bind ( f.speed.map ( i64 :: from) )
181
.bind ( f.speed_class.map ( i64 :: from) )
182
.bind ( f.kernel.clone ( ) .unwrap_or_default ( ) )
183
.bind ( id)
184
.execute ( pool)
185
.await? ;
186
Ok ( ( ) )
187
}
188
189
impl Runner {
190
191
192
pub fn facts_line ( & self ) -> String {
193
let mut out = Vec :: new( ) ;
194
if let Some ( s) = self .speed {
195
out.push ( format! ( " speed {s} (class {} )" , self .speed_class.unwrap_or ( 0 ) ) ) ;
196
}
197
if let Some ( l) = self .load_x100 {
198
out.push ( format! ( " load {:.2} " , l as f64 / 100. 0 ) ) ;
199
}
200
if let Some ( d) = self .free_disk_mib {
201
out.push ( if d > = 1024 { format! ( " {} GiB free" , d / 1024 ) } else { format! ( " {d} MiB free" ) } ) ;
202
}
203
out.join ( " · " )
204
}
205
}
206
4d2e44d7 Andy McCall 1 month ago ↶
207
208
209
92a797b2 Andy McCall 25 days ago ↶
210
pub async fn set_offer (
211
pool : & Pool,
212
id : i64 ,
213
tiers : & [String],
214
images : Option< & [ String] > ,
215
fetches_images : bool ,
216
profile : & str ,
217
) -> sqlx:: Result< ( ) > {
4d2e44d7 Andy McCall 1 month ago ↶
218
let images = images.map ( | i | serde_json:: to_string( i) .unwrap_or_default ( ) ) .unwrap_or_default ( ) ;
92a797b2 Andy McCall 25 days ago ↶
219
sqlx:: query( " UPDATE flow_runners SET tiers = ?, images = ?, fetches_images = ?, profile = ? WHERE id = ?" )
4d2e44d7 Andy McCall 1 month ago ↶
220
.bind ( tiers.join ( " ," ) )
221
.bind ( images)
92a797b2 Andy McCall 25 days ago ↶
222
.bind ( fetches_images)
4d2e44d7 Andy McCall 1 month ago ↶
223
.bind ( profile)
224
.bind ( id)
225
.execute ( pool)
226
.await? ;
b6b7e4cf Andy McCall 1 month ago ↶
227
Ok ( ( ) )
4d2e44d7 Andy McCall 1 month ago ↶
228
}
229
230
impl Runner {
231
pub fn tier_list ( & self ) -> Vec< & str > {
232
self .tiers.split ( ' ,' ) .filter ( | t | ! t.is_empty ( ) ) .collect ( )
233
}
234
235
236
pub fn image_list ( & self ) -> Option< Vec< String> > {
237
if self .images.is_empty ( ) {
238
None
239
} else {
240
serde_json:: from_str( & self .images) .ok ( )
241
}
242
}
b6b7e4cf Andy McCall 1 month ago ↶
243
}
244
245
pub async fn set_state ( pool : & Pool, id : i64 , state : & str ) -> sqlx:: Result< ( ) > {
4a4400c5 Andy McCall 22 days ago ↶
246
sqlx:: query( " UPDATE flow_runners SET state = ?, state_ms = ? WHERE id = ?" )
247
.bind ( state)
248
.bind ( super :: now_ms( ) )
249
.bind ( id)
250
.execute ( pool)
251
.await? ;
b6b7e4cf Andy McCall 1 month ago ↶
252
Ok ( ( ) )
4a4400c5 Andy McCall 22 days ago ↶
253
}
254
255
256
257
pub async fn erroring ( pool : & Pool, id : i64 , n : u32 ) -> sqlx:: Result< Option< String> > {
258
if n = = 0 {
259
return Ok ( None ) ;
260
}
261
let last: Vec< ( String, String) > = sqlx:: query_as(
262
" SELECT j.status, j.error FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id
263
WHERE j.runner_id = ? AND j.finished_ms IS NOT NULL AND j.finished_ms >= r.state_ms
264
ORDER BY j.finished_ms DESC, j.id DESC LIMIT ?" ,
265
)
266
.bind ( id)
267
.bind ( n as i64 )
268
.fetch_all ( pool)
269
.await? ;
270
Ok ( ( last.len ( ) = = n as usize & & last.iter ( ) .all ( | ( s , _) | s = = " error" ) ) .then ( | | last[ 0 ] .1. clone ( ) ) )
b6b7e4cf Andy McCall 1 month ago ↶
271
}
272
273
274
pub async fn remove ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
275
sqlx:: query( " DELETE FROM flow_runners WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
276
Ok ( ( ) )
277
}
278
279
280
281
pub async fn bind_key ( pool : & Pool, id : i64 , key : & str , name : & str ) -> sqlx:: Result< String> {
282
sqlx:: query( " UPDATE flow_runners SET key = ?, key_name = ? WHERE id = ? AND key = ''" )
283
.bind ( key)
284
.bind ( name)
285
.bind ( id)
286
.execute ( pool)
287
.await? ;
288
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE id = ?" ) .bind ( id) .fetch_one ( pool) .await
289
}
290
291
292
pub async fn keys ( pool : & Pool) -> sqlx:: Result< Vec< String> > {
293
sqlx:: query_scalar( " SELECT key FROM flow_runners WHERE key != ''" ) .fetch_all ( pool) .await
294
}
295
74be9796 Andy McCall 1 month ago ↶
296
297
pub async fn add_registration ( pool : & Pool, token_hash : & str , by : & str , scope : & Scope) -> sqlx:: Result< ( ) > {
b6b7e4cf Andy McCall 1 month ago ↶
298
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
299
sqlx:: query(
300
" INSERT INTO flow_runner_registrations (token_hash, created_by, created_ms, expires_ms, scope_owner, scope_repo_id) VALUES (?, ?, ?, ?, ?, ?)" ,
301
)
302
.bind ( token_hash)
303
.bind ( by)
304
.bind ( now)
305
.bind ( now + 24 * 3_600_000 )
306
.bind ( & scope.owner)
307
.bind ( scope.repo_id)
308
.execute ( pool)
309
.await? ;
310
Ok ( ( ) )
311
}
312
7974adb0 Andy McCall 1 month ago ↶
313
314
315
pub async fn serving ( pool : & Pool, owner : & str , repo_id : i64 ) -> sqlx:: Result< Vec< Runner> > {
316
sqlx:: query_as(
317
" SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) \
318
OR (scope_owner = ? AND scope_repo_id IS NULL) OR scope_repo_id = ? ORDER BY name" ,
319
)
320
.bind ( owner)
321
.bind ( repo_id)
322
.fetch_all ( pool)
323
.await
324
}
325
326
327
328
pub async fn serving_owner ( pool : & Pool, owner : & str ) -> sqlx:: Result< Vec< Runner> > {
329
sqlx:: query_as( " SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) OR scope_owner = ? ORDER BY name" )
330
.bind ( owner)
331
.fetch_all ( pool)
332
.await
333
}
334
74be9796 Andy McCall 1 month ago ↶
335
336
pub async fn set_scope ( pool : & Pool, id : i64 , scope : & Scope) -> sqlx:: Result< ( ) > {
337
sqlx:: query( " UPDATE flow_runners SET scope_owner = ?, scope_repo_id = ? WHERE id = ?" )
338
.bind ( & scope.owner)
339
.bind ( scope.repo_id)
340
.bind ( id)
b6b7e4cf Andy McCall 1 month ago ↶
341
.execute ( pool)
342
.await? ;
343
Ok ( ( ) )
344
}
345
346
347
pub async fn pending_registrations ( pool : & Pool) -> sqlx:: Result< Vec< ( String, i64 , i64 ) > > {
348
sqlx:: query_as( " SELECT created_by, created_ms, expires_ms FROM flow_runner_registrations WHERE used_ms IS NULL AND expires_ms > ? ORDER BY created_ms DESC" )
349
.bind ( now_ms ( ) )
350
.fetch_all ( pool)
351
.await
352
}
353
354
355
356
# [ allow ( clippy::too_many_arguments ) ]
357
pub async fn register (
358
pool : & Pool,
359
registration : & str ,
360
name : & str ,
361
token_hash : & str ,
362
key : & str ,
363
key_name : & str ,
364
by_default : & str ,
365
) -> sqlx:: Result< Option< Runner> > {
366
let mut tx = super :: begin_write( pool) .await? ;
367
let now = now_ms ( ) ;
74be9796 Andy McCall 1 month ago ↶
368
let found: Option< ( String, String, Option< i64 > ) > = sqlx:: query_as(
369
" SELECT created_by, scope_owner, scope_repo_id FROM flow_runner_registrations WHERE token_hash = ? AND used_ms IS NULL AND expires_ms > ?" ,
370
)
371
.bind ( registration)
372
.bind ( now)
373
.fetch_optional ( & mut * tx)
374
.await? ;
375
let Some ( ( who, scope_owner, scope_repo_id) ) = found else { return Ok ( None ) } ;
b6b7e4cf Andy McCall 1 month ago ↶
376
let id: i64 = sqlx:: query_scalar(
74be9796 Andy McCall 1 month ago ↶
377
" INSERT INTO flow_runners (name, token_hash, source, key, key_name, created_ms, created_by, scope_owner, scope_repo_id) VALUES (?, ?, 'registered', ?, ?, ?, ?, ?, ?) RETURNING id" ,
b6b7e4cf Andy McCall 1 month ago ↶
378
)
379
.bind ( name)
380
.bind ( token_hash)
381
.bind ( key)
382
.bind ( key_name)
383
.bind ( now)
384
.bind ( if who.is_empty ( ) { by_default } else { & who } )
74be9796 Andy McCall 1 month ago ↶
385
.bind ( scope_owner)
386
.bind ( scope_repo_id)
b6b7e4cf Andy McCall 1 month ago ↶
387
.fetch_one ( & mut * tx)
388
.await? ;
389
sqlx:: query( " UPDATE flow_runner_registrations SET used_ms = ?, runner_id = ? WHERE token_hash = ?" )
390
.bind ( now)
391
.bind ( id)
392
.bind ( registration)
393
.execute ( & mut * tx)
394
.await? ;
395
tx.commit ( ) .await? ;
396
get ( pool, id) .await
397
}
398
399
400
# [ derive ( Clone, Debug, FromRow ) ]
401
pub struct RunnerJob {
402
pub owner : String,
403
pub repo : String,
404
pub run_number : i64 ,
405
pub job : String,
406
pub status : String,
407
pub started_ms : Option< i64 > ,
408
pub finished_ms : Option< i64 > ,
409
pub receipt_attest : String,
410
}
411
412
413
pub async fn history ( pool : & Pool, id : i64 , limit : i64 , before : Option< i64 > ) -> sqlx:: Result< Vec< RunnerJob> > {
414
sqlx:: query_as(
415
" SELECT p.owner AS owner, p.name AS repo, r.number AS run_number, j.name AS job, j.status AS status, j.started_ms AS started_ms, \
416
j.finished_ms AS finished_ms, j.receipt_attest AS receipt_attest \
417
FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id \
418
WHERE j.runner_id = ? AND (? IS NULL OR j.started_ms < ?) ORDER BY j.started_ms DESC LIMIT ?" ,
419
)
420
.bind ( id)
421
.bind ( before)
422
.bind ( before)
423
.bind ( limit)
424
.fetch_all ( pool)
425
.await
426
}
427
428
429
pub async fn totals ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( i64 , i64 , i64 ) > {
430
sqlx:: query_as(
431
" SELECT COUNT(*), COALESCE(SUM(status = 'passed'), 0), COALESCE(SUM(finished_ms - started_ms), 0) \
432
FROM flow_jobs WHERE runner_id = ? AND finished_ms IS NOT NULL" ,
433
)
434
.bind ( id)
435
.fetch_one ( pool)
436
.await
437
}
438
439
440
pub async fn running ( pool : & Pool) -> sqlx:: Result< Vec< ( i64 , i64 ) > > {
441
sqlx:: query_as( " SELECT runner_id, COUNT(*) FROM flow_jobs WHERE status = 'running' AND runner_id IS NOT NULL GROUP BY runner_id" )
442
.fetch_all ( pool)
443
.await
444
}
445
446
pub async fn set_job_runner ( pool : & Pool, job_id : i64 , runner_id : i64 ) -> sqlx:: Result< ( ) > {
447
sqlx:: query( " UPDATE flow_jobs SET runner_id = ? WHERE id = ?" ) .bind ( runner_id) .bind ( job_id) .execute ( pool) .await? ;
448
Ok ( ( ) )
449
}