Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Self-hosted runners: a workspace on hold registers none, and a free workspace runs 4 of its workflow jobs on them at once. Runner registration had no billing check, and jobs on self-hosted runners skipped the workspace's concurrency limit, so a free workspace could run any number of jobs (each uploading artifacts and cache and writing logs) on runners of its own. Making a registration token now asks billing for the workspace's standing and refuses one while g1t staff hold it (Entitlements gains held). When a runner looks for work and its workspace already has FREE_SELF_HOSTED_JOBS (4, a wrangler var; 0 is no cap) jobs going on its runners, billing is asked whether it has the plan; without it, the workflow jobs wait until one finishes. Billing is asked only at the cap, so a runner's poll stays cheap, and when billing does not answer jobs go ahead. The self-hosted runners guide says so.

syntaqxcommitted Parent1220395Browse files
6 files+74−20/6 viewed
+5−1
701701 so you can see how much ran there.
702702
703703 - Workflow jobs on your runners need no plan and no card: they work on the
704− free tier.
704+ free tier. A workspace on the free plan runs **4** of its workflow jobs
705+ on its runners at once, however many runners it has; the rest stay
706+ queued until one finishes. On the g1t plan there is no such cap.
707+- A workspace g1t has put a hold on cannot register new runners: making a
708+ registration token answers that it is held.
705709 - Agent work on your runners still needs its model paid for: the g1t plan,
706710 the trial, the open-source pool on a public repository, or your own
707711 model provider, which makes the run free.
+4−0
16731673 /// spend spike is waiting for an owner to confirm it, or g1t staff put
16741674 /// a hold on it. None when it is not.
16751675 pub paused: Option<String>,
1676+ /// g1t staff put a hold on the account (sudo, Allowances): besides new
1677+ /// compute, nothing new is set up for it, such as a self-hosted runner.
1678+ #[serde(default)]
1679+ pub held: bool,
16761680 // What the workspace's plan gives it, for its pages.
16771681 /// What open reservations hold now.
16781682 #[serde(default)]
+2−0
278278 exposureMicros: number;
279279 /** Why new compute is paused, for the owner; null when it is not. */
280280 paused: string | null;
281+ /** g1t staff put a hold on the account: nothing new is set up for it, such as a self-hosted runner. */
282+ held?: boolean;
281283 /** What open reservations hold now. */
282284 heldMicros?: number;
283285 /** Paid in advance and not used yet. */
+35−0
8181 assert_eq!(shown_bytes(500 * 1024 * 1024), "500 MB");
8282 }
8383 }
84+
85+/// Whether a workspace without the plan may start one more job on its
86+/// self-hosted runners, with `running` going already. `cap` 0 is no cap.
87+pub fn self_hosted_room(running: u64, cap: u64) -> bool {
88+ cap == 0 || running < cap
89+}
90+
91+impl crate::Actions {
92+ /// The workspace's standing with billing: its plan, and any hold staff
93+ /// put on it. None when billing does not answer: callers let things
94+ /// through rather than stop everyone's work on billing's account.
95+ pub(crate) async fn standing(&self, workspace: &str) -> Option<g1t_contracts::billing::Entitlements> {
96+ let args = g1t_contracts::billing::EntitlementsArgs { workspace: workspace.to_lowercase() };
97+ match g1t_kit::call(&self.billing, "entitlements", &args).await {
98+ Ok(entitlements) => Some(entitlements),
99+ Err(error) => {
100+ worker::console_error!("actions: billing's entitlements for {workspace} failed: {error}");
101+ None
102+ }
103+ }
104+ }
105+}
106+
107+#[cfg(test)]
108+mod room_tests {
109+ use super::self_hosted_room;
110+
111+ #[test]
112+ fn free_workspaces_run_a_few_self_hosted_jobs_at_once() {
113+ assert!(self_hosted_room(0, 4));
114+ assert!(self_hosted_room(3, 4));
115+ assert!(!self_hosted_room(4, 4));
116+ assert!(self_hosted_room(400, 0));
117+ }
118+}
+27−1
423423 /// `create_registration_token`.
424424 pub async fn create_registration_token(&self, a: CreateRegistrationTokenArgs) -> Result<Outcome<RegistrationToken>> {
425425 let place = check!(self.runner_place(&a.actor, &a.owner, true).await?);
426+ // A workspace g1t staff put a hold on registers no new runners.
427+ if self.standing(&place.workspace).await.is_some_and(|standing| standing.held) {
428+ return Ok(fail(
429+ FailureCode::Paused,
430+ "g1t staff put a hold on this workspace, so it cannot register new runners. Write to hey@flagon.io if you think this is a mistake.",
431+ ));
432+ }
426433 let made = self
427434 .db
428435 .prepare("SELECT COUNT(*) AS n FROM runner_registrations WHERE workspace = ? AND created_at > ?")
11741181 job: JobRow,
11751182 run_repo: String,
11761183 }
1177− let queued = self.db.prepare(sql).bind(&binds)?.all().await?.results::<Queued>()?;
1184+ let mut queued = self.db.prepare(sql).bind(&binds)?.all().await?.results::<Queued>()?;
1185+ // A workspace without the plan runs FREE_SELF_HOSTED_JOBS of its jobs
1186+ // on its own runners at once (limits.rs). Billing is asked only once
1187+ // that many are going, so a runner's poll stays cheap.
1188+ if !queued.is_empty() && self.limits.free_self_hosted_jobs > 0 {
1189+ let running = self
1190+ .db
1191+ .prepare("SELECT COUNT(*) AS n FROM jobs WHERE status = 'in_progress' AND runner_id IS NOT NULL AND lower(namespace) = ?")
1192+ .bind(&[runner.workspace.as_str().into()])?
1193+ .first::<Count>(None)
1194+ .await?
1195+ .map_or(0, |c| c.n);
1196+ if !crate::limits::self_hosted_room(running as u64, self.limits.free_self_hosted_jobs)
1197+ && self.standing(&runner.workspace).await.is_some_and(|standing| !standing.has_plan)
1198+ {
1199+ // Its workflow jobs wait for one to finish; anything else the
1200+ // runner takes (below) is not held back.
1201+ queued.clear();
1202+ }
1203+ }
11781204 for Queued { job, run_repo } in queued {
11791205 let stored: Vec<String> = job.labels.as_deref().and_then(|l| serde_json::from_str(l).ok()).unwrap_or_default();
11801206 let wanted = Wanted::from_stored(&stored);
+1−0
502502 ceiling_micros: ceiling,
503503 exposure_micros: limit.exposure_micros,
504504 paused,
505+ held: account.allowances.hold.as_deref().is_some_and(|hold| !hold.trim().is_empty()),
505506 held_micros: held,
506507 prepaid_micros: limit.prepaid_micros,
507508 included_micros: if has_plan { self.plans.plan_included_micros } else { 0 },

Comments

Sign in to comment.