Commit

Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index

Every signed-in page asks billing for usage, the limit and entitlements, and status.g1t.sh times its price book. Each awaited query after query: entitlements made about 40 D1 round trips, recomputing the plan three times and the account four, so pages waited about two seconds on billing and the status page opened and closed "Billing slow" all day on 2026-10-06. - prices: three reads at once; plans priced from the rows already read. - usage: the totals and the five breakdowns at once (6 round trips to 1). - limit: account and plan read once (plan_kind_for, limit_with), then the limits row, the month, pending, the balance before and charged months in one wave (about 12-19 to 4-8). - entitlements: account and plan once, then everything else at once; pause_reason and the alerts reuse what was read (about 40 to 6-10). - A plan whose period is over is asked about at Stripe at most once an hour, not on every read. - 0026: ledger (workspace, created_at), so a month's reads are ranges. - repos git_operations: the month is a range on hour, so the (namespace, hour) key is used instead of a scan.

syntaqxcommitted Parente57ddeaBrowse files
10 files+470−2400/10 viewed
+1−0
909909 name = "g1t-billing"
910910 version = "0.1.0"
911911 dependencies = [
912+ "futures-util",
912913 "g1t-contracts",
913914 "g1t-kit",
914915 "getrandom 0.2.17",
+1−0
1414 serde.workspace = true
1515 serde_json.workspace = true
1616 worker.workspace = true
17+futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
1718 getrandom = { version = "0.2", features = ["js"] }
1819 hex = "0.4"
1920 sha2 = "0.10"
+3−0
1+-- The month's usage, the limit and the open-source pool read a workspace's
2+-- ledger from a date on; by workspace and time those are ranges, not scans.
3+CREATE INDEX IF NOT EXISTS ledger_by_workspace_time ON ledger (workspace, created_at);
+160−79
2727 //! stop. Runs already under way finish. g1t's own workspaces are watched
2828 //! but never paused.
2929
30+use futures_util::future::{try_join, try_join4, try_join5};
3031 use g1t_contracts::billing::{
31− ComputeKind, ConfirmSpikeArgs, SetCapsArgs, Entitlements, EntitlementsArgs, LimitState, PaidBy, PlanKind, Reservation,
32+ BillingAccount, ComputeKind, ConfirmSpikeArgs, SetCapsArgs, Entitlements, EntitlementsArgs, LimitState, PaidBy, PlanKind, Reservation,
3233 ReserveArgs, SettleArgs, Spike, UNLIMITED_MICROS, UsageAlert, RESERVATION_HOURS,
3334 };
3435 use g1t_contracts::time::rfc3339;
278279 /// The alerts a workspace has reached this month: its plan's included
279280 /// usage, its spend limit and g1t's ceiling, from 50%.
280281 pub(crate) async fn alerts_for(&self, workspace: &str) -> Result<Vec<UsageAlert>> {
281− let limit = self.limit_of(workspace).await?;
282− self.alerts_from(workspace, &limit).await
282+ let account = self.account_of(workspace).await?;
283+ let plan = self.plan_kind_for(workspace, &account).await?;
284+ let has_plan = plan != PlanKind::Free;
285+ let month = credits::month_of(&rfc3339(now_ms()));
286+ let (limit, used) = try_join(self.limit_with(workspace, &account, plan), async {
287+ if has_plan { self.allowance_used("plan_credit", workspace, &month).await } else { Ok(0) }
288+ })
289+ .await?;
290+ Ok(alerts_from(workspace, &limit, has_plan, used, self.plans.plan_included_micros))
283291 }
292+}
284293
285− /// The same, from a limit already worked out.
286− async fn alerts_from(&self, workspace: &str, limit: &g1t_contracts::billing::Limit) -> Result<Vec<UsageAlert>> {
287− let month = credits::month_of(&rfc3339(now_ms()));
288− let mut alerts = vec![];
289− if self.has_plan(workspace).await? && limit.trust != g1t_contracts::billing::Trust::Internal {
290− let used = self.allowance_used("plan_credit", workspace, &month).await?;
291− let included = self.plans.plan_included_micros;
292− let level = alert_level(used, included);
293− if level > 0 {
294− alerts.push(UsageAlert {
295− meter: "included".into(),
296− level,
297− used_micros: used,
298− limit_micros: included,
299− message: if level >= 100 {
300− format!("{workspace} has used all {} of this month's included usage. Usage from here is charged at cost plus 20%, up to your spend limit.", dollars(included))
301− } else {
302− format!("{workspace} has used {} of this month's {} included usage ({level}%). Past it, usage is charged at cost plus 20%, up to your spend limit.", dollars(used), dollars(included))
303− },
304− });
305− }
294+/// The alerts reached, from a limit already worked out, whether the
295+/// workspace has the plan, and what of its included usage it has used.
296+fn alerts_from(
297+ workspace: &str,
298+ limit: &g1t_contracts::billing::Limit,
299+ has_plan: bool,
300+ used: i64,
301+ included: i64,
302+) -> Vec<UsageAlert> {
303+ let mut alerts = vec![];
304+ if has_plan && limit.trust != g1t_contracts::billing::Trust::Internal {
305+ let level = alert_level(used, included);
306+ if level > 0 {
307+ alerts.push(UsageAlert {
308+ meter: "included".into(),
309+ level,
310+ used_micros: used,
311+ limit_micros: included,
312+ message: if level >= 100 {
313+ format!("{workspace} has used all {} of this month's included usage. Usage from here is charged at cost plus 20%, up to your spend limit.", dollars(included))
314+ } else {
315+ format!("{workspace} has used {} of this month's {} included usage ({level}%). Past it, usage is charged at cost plus 20%, up to your spend limit.", dollars(used), dollars(included))
316+ },
317+ });
306318 }
307− if let Some(spend_limit) = limit.spend_limit_micros {
308− let level = alert_level(limit.spent_micros, spend_limit);
309− if level > 0 {
310− alerts.push(UsageAlert {
311− meter: "spend_limit".into(),
312− level,
313− used_micros: limit.spent_micros,
314− limit_micros: spend_limit,
315− message: format!(
316− "{workspace} has spent {} of its {} monthly spend limit ({level}%). At the limit, new sandboxes, builds and agents stop until the month turns or an owner raises it.",
317− dollars(limit.spent_micros),
318− dollars(spend_limit)
319− ),
320− });
321− }
319+ }
320+ if let Some(spend_limit) = limit.spend_limit_micros {
321+ let level = alert_level(limit.spent_micros, spend_limit);
322+ if level > 0 {
323+ alerts.push(UsageAlert {
324+ meter: "spend_limit".into(),
325+ level,
326+ used_micros: limit.spent_micros,
327+ limit_micros: spend_limit,
328+ message: format!(
329+ "{workspace} has spent {} of its {} monthly spend limit ({level}%). At the limit, new sandboxes, builds and agents stop until the month turns or an owner raises it.",
330+ dollars(limit.spent_micros),
331+ dollars(spend_limit)
332+ ),
333+ });
322334 }
323− if let Some(ceiling) = limit.ceiling_micros.filter(|_| limit.trust != g1t_contracts::billing::Trust::New) {
324− let level = alert_level(limit.exposure_micros, ceiling);
325− if level > 0 {
326− alerts.push(UsageAlert {
327− meter: "ceiling".into(),
328− level,
329− used_micros: limit.exposure_micros,
330− limit_micros: ceiling,
331− message: format!(
332− "{workspace} has {} of usage not yet paid for, of the {} g1t allows ({level}%). With a card on file g1t charges it as the limit nears; prepaying raises it at once.",
333− dollars(limit.exposure_micros),
334− dollars(ceiling)
335− ),
336− });
337− }
335+ }
336+ if let Some(ceiling) = limit.ceiling_micros.filter(|_| limit.trust != g1t_contracts::billing::Trust::New) {
337+ let level = alert_level(limit.exposure_micros, ceiling);
338+ if level > 0 {
339+ alerts.push(UsageAlert {
340+ meter: "ceiling".into(),
341+ level,
342+ used_micros: limit.exposure_micros,
343+ limit_micros: ceiling,
344+ message: format!(
345+ "{workspace} has {} of usage not yet paid for, of the {} g1t allows ({level}%). With a card on file g1t charges it as the limit nears; prepaying raises it at once.",
346+ dollars(limit.exposure_micros),
347+ dollars(ceiling)
348+ ),
349+ });
338350 }
339− Ok(alerts)
340351 }
352+ alerts
353+}
341354
355+impl Billing {
342356 /// Whether a card check was done for the workspace.
343357 pub(crate) async fn card_checked(&self, workspace: &str) -> Result<bool> {
344358 Ok(self
380394 async fn pause_reason(
381395 &self,
382396 workspace: &str,
397+ account: &BillingAccount,
383398 plan: PlanKind,
384399 limit: Option<&g1t_contracts::billing::Limit>,
385400 ) -> Result<(Option<String>, Option<Spike>, Option<FailureCode>)> {
386− let account = self.account_of(workspace).await?;
387401 if let Some(hold) = account.allowances.hold.as_deref().filter(|h| !h.trim().is_empty()) {
388402 return Ok((Some(format!("g1t staff put a hold on new compute ({}).", hold.trim())), None, Some(FailureCode::Paused)));
389403 }
390404 // A comped account past its monthly budget (`budget`).
391− if let Some(why) = self.comped_stop(&account).await? {
405+ if let Some(why) = self.comped_stop(account).await? {
392406 return Ok((Some(why), None, Some(FailureCode::Paused)));
393407 }
394408 let spike = self.spike_pause(workspace, plan).await?;
428442 /// `entitlements`: what the workspace may do now.
429443 pub(crate) async fn entitlements(&self, a: EntitlementsArgs) -> Result<Entitlements> {
430444 let workspace = a.workspace.to_lowercase();
431− let plan = self.plan_kind(&workspace).await?;
432445 let account = self.account_of(&workspace).await?;
433− let limit = self.limit_of(&workspace).await?;
446+ let plan = self.plan_kind_for(&workspace, &account).await?;
447+ let has_plan = plan != PlanKind::Free;
434448 let now = rfc3339(now_ms());
435449 let month = credits::month_of(&now);
436− let verified = matches!(plan, PlanKind::Internal | PlanKind::Enterprise) || self.card_checked(&workspace).await?;
437− // A card checked while the month's pool was empty: granted once it
438− // has room.
439− let grant = match self.grant_of(&workspace).await? {
440− Some(grant) => Some(grant),
441− None if verified && plan == PlanKind::Free && self.trial_allowed(&workspace).await? => self.ensure_grant(&workspace).await?,
442− None => None,
450+ // Every signed-in page asks for this, so what does not need another
451+ // answer is read at once: the limit (and the pause, from it), the
452+ // trial, and the month's counts.
453+ let standing = async {
454+ let limit = self.limit_with(&workspace, &account, plan).await?;
455+ let pause = self.pause_reason(&workspace, &account, plan, Some(&limit)).await?;
456+ Ok::<_, worker::Error>((limit, pause))
457+ };
458+ let trial = async {
459+ let (checked, grant) = try_join(
460+ async {
461+ if matches!(plan, PlanKind::Internal | PlanKind::Enterprise) { Ok(true) } else { self.card_checked(&workspace).await }
462+ },
463+ self.grant_of(&workspace),
464+ )
465+ .await?;
466+ // A card checked while the month's pool was empty: granted once
467+ // it has room.
468+ let grant = match grant {
469+ Some(grant) => Some(grant),
470+ None if checked && plan == PlanKind::Free && self.trial_allowed(&workspace).await? => self.ensure_grant(&workspace).await?,
471+ None => None,
472+ };
473+ Ok::<_, worker::Error>((checked, grant))
443474 };
475+ let counts = try_join5(
476+ self.owner_caps(&workspace),
477+ self.private_storage(&workspace),
478+ self.oss_paid(&workspace, &month),
479+ self.held(&account.workspaces),
480+ async { if has_plan { self.allowance_used("plan_credit", &workspace, &month).await } else { Ok(0) } },
481+ );
482+ let more = try_join(self.allowance_used("build_seconds", &workspace, &month), self.git_operations_this_month(&workspace));
483+ let ((limit, (paused, spike, _)), (verified, grant), (owners, stored, oss, held, included_used), (build_seconds, git_operations)) =
484+ try_join4(standing, trial, counts, more).await?;
444485 let trial_left = grant.as_ref().map_or(0, |g| left(g.granted_micros, g.used_micros));
445486 let first_month = limit.first_month;
446487 let (max_agents, max_minutes) = caps(plan, first_month, trial_left > 0, account.allowances.max_concurrent_agents);
447− let (paused, spike, _) = self.pause_reason(&workspace, plan, Some(&limit)).await?;
448488 let ceiling = match plan {
449489 PlanKind::Free => 0,
450490 PlanKind::Internal => UNLIMITED_MICROS,
451491 _ => limit.ceiling_micros.unwrap_or(UNLIMITED_MICROS),
452492 };
453− let has_plan = plan != PlanKind::Free;
454− let owners = self.owner_caps(&workspace).await?;
455− let stored = self.private_storage(&workspace).await?;
456− let oss = self.oss_paid(&workspace, &month).await?;
493+ let alerts = alerts_from(&workspace, &limit, has_plan, included_used, self.plans.plan_included_micros);
457494 Ok(Entitlements {
458495 plan,
459496 compute: has_plan || trial_left > 0,
467504 ceiling_micros: ceiling,
468505 exposure_micros: limit.exposure_micros,
469506 paused,
470− held_micros: self.held(&account.workspaces).await?,
507+ held_micros: held,
471508 prepaid_micros: limit.prepaid_micros,
472509 included_micros: if has_plan { self.plans.plan_included_micros } else { 0 },
473− included_used_micros: if has_plan { self.allowance_used("plan_credit", &workspace, &month).await? } else { 0 },
510+ included_used_micros: included_used,
474511 audit_retention_days: self.plans.audit_days,
475512 free_private_storage_bytes: self.plans.free_storage_bytes,
476513 private_storage_bytes: stored,
477514 oss_paid_micros: oss,
478− build_seconds_used: self.allowance_used("build_seconds", &workspace, &month).await?.max(0) as u32,
479− git_operations: self.git_operations_this_month(&workspace).await?,
515+ build_seconds_used: build_seconds.max(0) as u32,
516+ git_operations,
480517 git_operations_included: self.plans.git_included,
481518 min_charge_micros: self.plans.min_charge_micros,
482519 spike,
483− alerts: self.alerts_from(&workspace, &limit).await?,
520+ alerts,
484521 workspace,
485522 })
486523 }
495532 if self.stripe.is_none() {
496533 return Ok(Outcome::Ok(Reservation { id: new_id("rsv", now), paid_by: PaidBy::OnDemand, held_micros: 0, expires_at }));
497534 }
498− let plan = self.plan_kind(&workspace).await?;
499535 let account = self.account_of(&workspace).await?;
536+ let plan = self.plan_kind_for(&workspace, &account).await?;
500537 // g1t's own caps (`budget`), in their own words: a comped account's
501538 // monthly budget, and the daily breaker.
502539 if let Some(why) = self.comped_stop(&account).await? {
505542 if let Some(why) = self.breaker_refuses(plan, &account, a.kind, a.hosted_model).await? {
506543 return Ok(Outcome::fail(FailureCode::Paused, why));
507544 }
508− let limit = if plan == PlanKind::Internal { None } else { Some(self.limit_of(&workspace).await?) };
509− let (paused, _, code) = self.pause_reason(&workspace, plan, limit.as_ref()).await?;
545+ let limit = if plan == PlanKind::Internal { None } else { Some(self.limit_with(&workspace, &account, plan).await?) };
546+ let (paused, _, code) = self.pause_reason(&workspace, &account, plan, limit.as_ref()).await?;
510547 if let (Some(why), Some(code)) = (paused, code) {
511548 return Ok(refusal(code, &workspace, a.kind, &why));
512549 }
832869 mod tests {
833870 use super::*;
834871
872+ fn limit(trust: g1t_contracts::billing::Trust, spent: i64, spend_limit: Option<i64>, exposure: i64, ceiling: Option<i64>) -> g1t_contracts::billing::Limit {
873+ g1t_contracts::billing::Limit {
874+ workspace: "acme".into(),
875+ account: "acc_acme".into(),
876+ account_name: "acme".into(),
877+ trust,
878+ exposure_micros: exposure,
879+ ceiling_micros: ceiling,
880+ trust_ceiling_micros: ceiling,
881+ spend_limit_micros: spend_limit,
882+ state: LimitState::Ok,
883+ message: None,
884+ spent_micros: spent,
885+ default_spend_limit: false,
886+ available_micros: None,
887+ growth: None,
888+ prepaid_micros: 0,
889+ max_ceiling_micros: None,
890+ raise_once_micros: None,
891+ raised_at: None,
892+ first_month: false,
893+ }
894+ }
895+
896+ #[test]
897+ fn alerts_come_from_the_answers_already_read() {
898+ use g1t_contracts::billing::Trust;
899+ let meters = |alerts: Vec<UsageAlert>| alerts.into_iter().map(|a| (a.meter, a.level)).collect::<Vec<_>>();
900+ // On the plan: included usage at 90%, the spend limit at 50%, the ceiling at 75%.
901+ let paid = limit(Trust::Paid, 100_000_000, Some(200_000_000), 75_000_000, Some(100_000_000));
902+ assert_eq!(
903+ meters(alerts_from("acme", &paid, true, 9_000_000, 10_000_000)),
904+ vec![("included".to_owned(), 90), ("spend_limit".to_owned(), 50), ("ceiling".to_owned(), 75)]
905+ );
906+ // Without the plan, what was used of the included usage is not an alert.
907+ assert_eq!(meters(alerts_from("acme", &paid, false, 9_000_000, 10_000_000)).len(), 2);
908+ // g1t's own workspaces have no included usage to warn of, and a new
909+ // workspace's ceiling is not one either.
910+ let internal = limit(Trust::Internal, 0, None, 0, None);
911+ assert!(alerts_from("acme", &internal, true, 10_000_000, 10_000_000).is_empty());
912+ let new = limit(Trust::New, 0, None, 3_000_000, Some(3_000_000));
913+ assert!(alerts_from("acme", &new, false, 0, 10_000_000).is_empty());
914+ }
915+
835916 fn paid(credit: i64, on_demand: i64) -> Room {
836917 Room { credit, trial: 0, oss: 0, on_demand: Some(on_demand) }
837918 }
+13−2
2727 //! is one D1 batch, which runs as a transaction, so two charges at once
2828 //! never take more than a budget holds.
2929
30−use g1t_contracts::billing::{ComputeKind, Feature, PlanKind, Pools, TermsKind, Trial, TrialArgs};
30+use g1t_contracts::billing::{BillingAccount, ComputeKind, Feature, PlanKind, Pools, TermsKind, Trial, TrialArgs};
3131 use g1t_contracts::time::rfc3339;
3232 use g1t_kit::now_ms;
3333 use serde::Deserialize;
261261 /// does not charge has nothing to gate.
262262 pub(crate) async fn plan_kind(&self, workspace: &str) -> Result<PlanKind> {
263263 let account = self.account_of(workspace).await?;
264+ self.plan_kind_for(workspace, &account).await
265+ }
266+
267+ /// The plan, from the account already read for the workspace.
268+ pub(crate) async fn plan_kind_for(&self, workspace: &str, account: &BillingAccount) -> Result<PlanKind> {
264269 if account.terms.kind == TermsKind::Comped {
265270 return Ok(PlanKind::Internal);
266271 }
270275 if self.stripe.is_none() || account.allowances.plan {
271276 return Ok(PlanKind::Paid);
272277 }
273− if self.plan_on(workspace, Feature::Plan).await? || self.plan_on(workspace, Feature::Deployments).await? {
278+ // Both subscriptions are asked for at once; either one is the plan.
279+ let (plan, deployments) = futures_util::future::try_join(
280+ self.plan_on(workspace, Feature::Plan),
281+ self.plan_on(workspace, Feature::Deployments),
282+ )
283+ .await?;
284+ if plan || deployments {
274285 return Ok(PlanKind::Paid);
275286 }
276287 Ok(PlanKind::Free)
+36−4
3333 period_end: Option<String>,
3434 started_by: String,
3535 started_at: String,
36+ updated_at: String,
37+}
38+
39+/// How long a plan's row is believed after it was last written, once its
40+/// period is over, before the processor is asked again.
41+const REFRESH_MS: u64 = 60 * 60 * 1000;
42+
43+/// Whether to ask the processor about a plan again: its period is over (or
44+/// unknown) and it is not canceled, and it was not written in the last hour.
45+/// Without the hour a plan the processor still shows as ended would be
46+/// asked about on every page.
47+fn needs_refresh(status: &str, period_end: Option<&str>, updated_at: &str, now_ms: u64) -> bool {
48+ let now = rfc3339(now_ms);
49+ let over = period_end.is_none_or(|end| end <= now.as_str()) && status != "canceled";
50+ over && updated_at <= rfc3339(now_ms.saturating_sub(REFRESH_MS)).as_str()
3651 }
3752
3853 #[derive(Deserialize)]
177192 async fn subscription_row(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
178193 self.db
179194 .prepare(
180− "SELECT feature, subscription_id, status, period_end, started_by, started_at
195+ "SELECT feature, subscription_id, status, period_end, started_by, started_at, updated_at
181196 FROM subscriptions WHERE workspace = ? AND feature = ?",
182197 )
183198 .bind(&[workspace.into(), feature.as_str().into()])?
220235 }
221236
222237 /// A workspace's plan for a feature, asking the processor again once
223− /// the period it last knew of is over.
238+ /// the period it last knew of is over, at most once an hour.
224239 async fn current(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
225240 let Some(row) = self.subscription_row(workspace, feature).await? else {
226241 return Ok(None);
227242 };
228− let stale = row.period_end.as_deref().is_none_or(|end| end <= rfc3339(now_ms()).as_str())
229− && row.status != "canceled";
243+ let stale = needs_refresh(&row.status, row.period_end.as_deref(), &row.updated_at, now_ms());
230244 if let (true, Some(stripe)) = (stale, &self.stripe) {
231245 match stripe.subscription(&row.subscription_id).await {
232246 Ok(subscription) => self.record(workspace, feature, &subscription, &row.started_by).await?,
615629 use super::*;
616630
617631 #[test]
632+ fn an_ended_plan_is_asked_about_at_most_once_an_hour() {
633+ let now = 1_791_000_000_000;
634+ let at = |ago_ms: u64| rfc3339(now - ago_ms);
635+ let ended = at(24 * 60 * 60 * 1000);
636+ // Ended, and last written a day ago: ask.
637+ assert!(needs_refresh("active", Some(&ended), &ended, now));
638+ // Ended, but written ten minutes ago: believe the row.
639+ assert!(!needs_refresh("active", Some(&ended), &at(10 * 60 * 1000), now));
640+ // An hour on, ask again.
641+ assert!(needs_refresh("active", Some(&ended), &at(REFRESH_MS), now));
642+ // No period known is the same as ended.
643+ assert!(needs_refresh("past_due", None, &ended, now));
644+ // A period still running, or a canceled plan, is never asked about.
645+ assert!(!needs_refresh("active", Some(&rfc3339(now + 1000)), &ended, now));
646+ assert!(!needs_refresh("canceled", Some(&ended), &ended, now));
647+ }
648+
649+ #[test]
618650 fn the_plan_text_quotes_a_build_minute_as_the_table_does() {
619651 // The price book's build second (16.44 millionths at cost, plus
620652 // 20%) is 19.73 millionths: a minute is 1,184 millionths, $0.0012,
+21−18
363363
364364 impl Billing {
365365 pub(crate) async fn prices(&self) -> Result<PriceBook> {
366− let prices = self
367− .db
368− .prepare("SELECT * FROM prices ORDER BY rowid")
369− .all()
370− .await?
371− .results::<PriceRow>()?;
372− let changes = self
373− .db
374− .prepare("SELECT * FROM price_changes ORDER BY created_at DESC LIMIT 20")
375− .all()
376− .await?
377− .results::<ChangeRow>()?;
378− let mut plans = Vec::new();
379− for feature in g1t_contracts::billing::Feature::ALL.iter() {
380− plans.push(self.plan(*feature).await?);
381− }
382− // Changes still to come first, so a rise is seen before it is charged.
383− let coming = self.coming_changes().await?;
366+ // Three reads at once; the plans are priced from the rows already
367+ // read, not one query per meter (status.g1t.sh times this call).
368+ let (prices, changes, coming) = futures_util::future::join3(
369+ async { self.db.prepare("SELECT * FROM prices ORDER BY rowid").all().await?.results::<PriceRow>() },
370+ async {
371+ self.db
372+ .prepare("SELECT * FROM price_changes ORDER BY created_at DESC LIMIT 20")
373+ .all()
374+ .await?
375+ .results::<ChangeRow>()
376+ },
377+ // Changes still to come first, so a rise is seen before it is charged.
378+ self.coming_changes(),
379+ )
380+ .await;
381+ let (prices, changes, coming) = (prices?, changes?, coming?);
382+ let book: std::collections::BTreeMap<&str, f64> = prices
383+ .iter()
384+ .map(|row| (row.meter.as_str(), Price::price_for(row.cost_micros, row.markup_percent)))
385+ .collect();
386+ let plans: Vec<_> = g1t_contracts::billing::Feature::ALL.iter().map(|_| self.plan_at(&book)).collect();
384387 Ok(PriceBook {
385388 prices: prices
386389 .into_iter()
+34−19
4848 use sha2::{Digest, Sha256};
4949 use worker::wasm_bindgen::JsValue;
5050 use worker::{Context, D1Database, Env, MessageBatch, MessageExt, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
51+use futures_util::future::{try_join, try_join5};
5152
5253 use stripe::Stripe;
5354
420421 runs: Option<u32>,
421422 added: Option<i64>,
422423 }
423− let totals = self
424− .db
425− .prepare(
426− "SELECT
427− -SUM(CASE WHEN kind = 'usage' THEN amount_micros END) AS spent,
428− SUM(CASE WHEN kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' THEN cost_micros END) AS cost,
429− SUM(CASE WHEN kind = 'usage' AND billed_to = 'workspace' THEN cost_micros END) AS provider,
430− SUM(CASE WHEN kind = 'usage' THEN 1 ELSE 0 END) AS runs,
431− SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS added
432− FROM ledger WHERE workspace = ?1 AND created_at >= ?2",
433− )
434− .bind(&[workspace.as_str().into(), a.since.as_str().into()])?
435− .first::<Totals>(None)
436− .await?;
424+ let totals = async {
425+ self.db
426+ .prepare(
427+ "SELECT
428+ -SUM(CASE WHEN kind = 'usage' THEN amount_micros END) AS spent,
429+ SUM(CASE WHEN kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' THEN cost_micros END) AS cost,
430+ SUM(CASE WHEN kind = 'usage' AND billed_to = 'workspace' THEN cost_micros END) AS provider,
431+ SUM(CASE WHEN kind = 'usage' THEN 1 ELSE 0 END) AS runs,
432+ SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS added
433+ FROM ledger WHERE workspace = ?1 AND created_at >= ?2",
434+ )
435+ .bind(&[workspace.as_str().into(), a.since.as_str().into()])?
436+ .first::<Totals>(None)
437+ .await
438+ };
439+ // The totals and the five slices read the same rows independently,
440+ // so they go to D1 at once: one round trip of waiting, not six.
441+ let (totals, (by_day, by_task, by_repo, by_pull, by_model)) = try_join(
442+ totals,
443+ try_join5(
444+ query(slices("substr(created_at, 1, 10) || '/' || COALESCE(task, 'other')", 400)),
445+ query(slices("task", 20)),
446+ query(slices("repo", 20)),
447+ query(slices("repo || '#' || number", 10)),
448+ query(slices("model", 10)),
449+ ),
450+ )
451+ .await?;
437452 let totals = totals.unwrap_or(Totals {
438453 spent: None,
439454 cost: None,
449464 free: self.free,
450465 runs: totals.runs.unwrap_or_default(),
451466 added_micros: totals.added.unwrap_or_default(),
452− by_day: query(slices("substr(created_at, 1, 10) || '/' || COALESCE(task, 'other')", 400)).await?,
453− by_task: query(slices("task", 20)).await?,
454− by_repo: query(slices("repo", 20)).await?,
455− by_pull: query(slices("repo || '#' || number", 10)).await?,
456− by_model: query(slices("model", 10)).await?,
467+ by_day,
468+ by_task,
469+ by_repo,
470+ by_pull,
471+ by_model,
457472 since: a.since,
458473 }))
459474 }
+143−110
3737 //! Usage counts at what it cost g1t or what it is charged, whichever is
3838 //! more. Test-mode payments are not money, so they do not raise trust.
3939
40+use futures_util::future::{try_join, try_join5, try_join_all};
4041 use g1t_contracts::billing::{
41− CheckLimitArgs, Limit, LimitArgs, LimitState, NotePendingArgs, PlanKind, SetSpendLimitArgs, TermsKind, Trust,
42+ BillingAccount, CheckLimitArgs, Limit, LimitArgs, LimitState, NotePendingArgs, PlanKind, SetSpendLimitArgs, TermsKind, Trust,
4243 };
4344 use g1t_contracts::time::rfc3339;
4445 use g1t_contracts::{FailureCode, Outcome, Role};
245246 pub(crate) async fn limit_of(&self, workspace: &str) -> Result<Limit> {
246247 let workspace = workspace.to_lowercase();
247248 let account = self.account_of(&workspace).await?;
248− let row = self
249− .db
250− .prepare(
251− "SELECT spend_limit_micros, spend_limit_full, autopay_failed_at, autopay_error,
252− max_ceiling_micros, granted_ceiling_micros, raised_at
253− FROM limits WHERE workspace = ?",
254− )
255− .bind(&[workspace.as_str().into()])?
256− .first::<LimitRow>(None)
257− .await?;
249+ let plan = self.plan_kind_for(&workspace, &account).await?;
250+ self.limit_with(&workspace, &account, plan).await
251+ }
252+
253+ /// The workspace's limit, from the account and plan already read for
254+ /// it, so a caller that has them does not read them again.
255+ pub(crate) async fn limit_with(&self, workspace: &str, account: &BillingAccount, plan: PlanKind) -> Result<Limit> {
256+ let workspace = workspace.to_lowercase();
258257 let now = rfc3339(now_ms());
259258 let month_start = format!("{}-01", &now[..7]);
260259 let marks = vec!["?"; account.workspaces.len().max(1)].join(", ");
263262 } else {
264263 account.workspaces.iter().map(|w| JsValue::from(w.as_str())).collect()
265264 };
265+ let row = async {
266+ self.db
267+ .prepare(
268+ "SELECT spend_limit_micros, spend_limit_full, autopay_failed_at, autopay_error,
269+ max_ceiling_micros, granted_ceiling_micros, raised_at
270+ FROM limits WHERE workspace = ?",
271+ )
272+ .bind(&[workspace.as_str().into()])?
273+ .first::<LimitRow>(None)
274+ .await
275+ };
266276 let mut with_month = members.clone();
267277 with_month.push(month_start.as_str().into());
268278 // Each usage entry at its cost to g1t or its charge, whichever is
270280 // What the plan's included usage, the trial, the open-source pool
271281 // or g1t itself paid for is not unpaid: those are budgets already
272282 // paid for.
273− let month = self
274− .db
275− .prepare(format!(
276− "SELECT
277− SUM(CASE WHEN kind = 'usage' THEN
278− CASE WHEN COALESCE(billed_to, 'g1t') = 'g1t'
279− THEN MAX(COALESCE(cost_micros, 0) - COALESCE(credit_micros, 0)
280− - COALESCE(trial_micros, 0) - COALESCE(oss_micros, 0)
281− - COALESCE(given_micros, 0),
282− -amount_micros)
283− ELSE -amount_micros END
284− END) AS used,
285− SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS paid
286− FROM ledger WHERE workspace IN ({marks}) AND created_at >= ?"
287− ))
288− .bind(&with_month)?
289− .first::<Month>(None)
290− .await?;
291− let (used, paid_month) = month.map_or((0, 0), |m| (m.used.unwrap_or(0), m.paid.unwrap_or(0)));
283+ let month = async {
284+ self.db
285+ .prepare(format!(
286+ "SELECT
287+ SUM(CASE WHEN kind = 'usage' THEN
288+ CASE WHEN COALESCE(billed_to, 'g1t') = 'g1t'
289+ THEN MAX(COALESCE(cost_micros, 0) - COALESCE(credit_micros, 0)
290+ - COALESCE(trial_micros, 0) - COALESCE(oss_micros, 0)
291+ - COALESCE(given_micros, 0),
292+ -amount_micros)
293+ ELSE -amount_micros END
294+ END) AS used,
295+ SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS paid
296+ FROM ledger WHERE workspace IN ({marks}) AND created_at >= ?"
297+ ))
298+ .bind(&with_month)?
299+ .first::<Month>(None)
300+ .await
301+ };
292302 // And what is metered but not charged until the month closes.
293303 let mut pending_args = members.clone();
294304 pending_args.push(month_start[..7].into());
295− let pending = self
296− .db
297− .prepare(format!(
298− "SELECT SUM(charge_micros) AS paid FROM pending_usage WHERE workspace IN ({marks}) AND month = ?"
299− ))
300− .bind(&pending_args)?
301− .first::<Paid>(None)
302− .await?
303− .and_then(|row| row.paid)
304− .unwrap_or(0);
305− let used = used + pending;
305+ let pending = async {
306+ Ok::<i64, worker::Error>(
307+ self.db
308+ .prepare(format!(
309+ "SELECT SUM(charge_micros) AS paid FROM pending_usage WHERE workspace IN ({marks}) AND month = ?"
310+ ))
311+ .bind(&pending_args)?
312+ .first::<Paid>(None)
313+ .await?
314+ .and_then(|row| row.paid)
315+ .unwrap_or(0),
316+ )
317+ };
306318 // Test-mode payments are not money: they pay nothing off.
307319 let live = self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live);
308320 // The balance the month started with: owed from before (so a new
311323 // do not.
312324 let mut before = members.clone();
313325 before.push(month_start.as_str().into());
314− let balance_before = self
315− .db
316− .prepare(format!(
317− "SELECT SUM(CASE WHEN kind = 'usage' THEN amount_micros
318− WHEN kind = 'top_up' AND ({live} = 1 OR reference LIKE 'crd%') THEN amount_micros
319− ELSE 0 END) AS paid
320− FROM ledger WHERE workspace IN ({marks}) AND created_at < ?",
321− live = u8::from(live)
322− ))
323− .bind(&before)?
324− .first::<Paid>(None)
325− .await?
326− .and_then(|row| row.paid)
327− .unwrap_or(0);
328− let (exposure, prepaid) = exposure(used, if live { paid_month } else { 0 }, balance_before);
329−
330− let plan = self.plan_kind(&workspace).await?;
331− let mut first_month = false;
332− let (trust, trust_ceiling) = match account.terms.kind {
333− TermsKind::Comped => (Trust::Internal, None),
334− _ if account.terms.ceiling_micros.is_some() => (Trust::Reviewed, account.terms.ceiling_micros),
335− _ => {
336− let paid = self.live_paid(&members).await?;
337− let established = if paid > 0 { self.established(&members).await? } else { None };
338− if plan == PlanKind::Free {
339− // Nothing on demand: only what a free workspace can owe.
340− (Trust::New, Some(self.ceilings.new))
341− } else {
342− first_month = plan == PlanKind::Paid && self.first_month(&workspace).await?;
343− let ceiling = paid_ceiling(&self.ceilings, self.plans.paid_start_micros, first_month, paid, established);
344− (if established.is_some() { Trust::Established } else { Trust::Paid }, Some(ceiling))
326+ let balance_before = async {
327+ Ok::<i64, worker::Error>(
328+ self.db
329+ .prepare(format!(
330+ "SELECT SUM(CASE WHEN kind = 'usage' THEN amount_micros
331+ WHEN kind = 'top_up' AND ({live} = 1 OR reference LIKE 'crd%') THEN amount_micros
332+ ELSE 0 END) AS paid
333+ FROM ledger WHERE workspace IN ({marks}) AND created_at < ?",
334+ live = u8::from(live)
335+ ))
336+ .bind(&before)?
337+ .first::<Paid>(None)
338+ .await?
339+ .and_then(|row| row.paid)
340+ .unwrap_or(0),
341+ )
342+ };
343+ // The trust ceiling, from what has been paid and how steadily. The
344+ // first month is asked for beside it, since neither needs the other.
345+ let trust = async {
346+ Ok::<_, worker::Error>(match account.terms.kind {
347+ TermsKind::Comped => (Trust::Internal, None, false),
348+ _ if account.terms.ceiling_micros.is_some() => (Trust::Reviewed, account.terms.ceiling_micros, false),
349+ _ => {
350+ let standing = async {
351+ let paid = self.live_paid(&members).await?;
352+ let established = if paid > 0 { self.established(&members).await? } else { None };
353+ Ok::<_, worker::Error>((paid, established))
354+ };
355+ let first = async {
356+ if plan == PlanKind::Paid { self.first_month(&workspace).await } else { Ok(false) }
357+ };
358+ let ((paid, established), first_month) = try_join(standing, first).await?;
359+ if plan == PlanKind::Free {
360+ // Nothing on demand: only what a free workspace can owe.
361+ (Trust::New, Some(self.ceilings.new), false)
362+ } else {
363+ let ceiling = paid_ceiling(&self.ceilings, self.plans.paid_start_micros, first_month, paid, established);
364+ (if established.is_some() { Trust::Established } else { Trust::Paid }, Some(ceiling), first_month)
365+ }
345366 }
346− }
367+ })
347368 };
369+ // This month's charges, and last month's, for the spend limit.
370+ let charged = self.charged_months(&members, &month_start);
371+ // None of these reads needs another's answer, so they go to D1 at
372+ // once: the limit is on every signed-in page.
373+ let ((row, month, pending, balance_before, (spent, last_month)), (trust, trust_ceiling, first_month)) =
374+ try_join(try_join5(row, month, pending, balance_before, charged), trust).await?;
375+ let (used, paid_month) = month.map_or((0, 0), |m| (m.used.unwrap_or(0), m.paid.unwrap_or(0)));
376+ let used = used + pending;
377+ let (exposure, prepaid) = exposure(used, if live { paid_month } else { 0 }, balance_before);
378+
348379 // A ceiling g1t granted is a floor under the trust ceiling.
349380 let granted = row.as_ref().and_then(|row| row.granted_ceiling_micros);
350381 let ceiling = trust_ceiling.map(|c| c.max(granted.unwrap_or(0)));
364395 .run()
365396 .await?;
366397 }
367− // This month's charges, and last month's, for the spend limit.
368− let (spent, last_month) = self.charged_months(&members, &month_start).await?;
369398 let spent = spent + pending;
370399 let raised_at = row.as_ref().and_then(|row| row.raised_at.clone());
371400 let self_serve = matches!(trust, Trust::New | Trust::Paid | Trust::Established) && plan != PlanKind::Free;
451480 };
452481 Ok(Limit {
453482 workspace,
454− account: account.id,
455− account_name: account.name,
483+ account: account.id.clone(),
484+ account_name: account.name.clone(),
456485 spent_micros: spent,
457486 default_spend_limit,
458487 available_micros: available,
547576 struct Count {
548577 n: Option<i64>,
549578 }
550− let troubled = self
551− .db
552− .prepare(format!(
553− "SELECT (SELECT COUNT(*) FROM ledger WHERE workspace IN ({marks}) AND disputed = 1)
554− + (SELECT COUNT(*) FROM limits WHERE workspace IN ({marks}) AND autopay_failed_at >= '{since}') AS n",
555− since = rfc3339(now_ms() - 90 * 24 * 60 * 60 * 1000)
556− ))
557− .bind(&[members, members].concat())?
558− .first::<Count>(None)
559− .await?
560− .and_then(|c| c.n)
561− .unwrap_or(0);
562− if troubled > 0 {
563− return Ok(None);
579+ let troubled = async {
580+ Ok::<i64, worker::Error>(
581+ self.db
582+ .prepare(format!(
583+ "SELECT (SELECT COUNT(*) FROM ledger WHERE workspace IN ({marks}) AND disputed = 1)
584+ + (SELECT COUNT(*) FROM limits WHERE workspace IN ({marks}) AND autopay_failed_at >= '{since}') AS n",
585+ since = rfc3339(now_ms() - 90 * 24 * 60 * 60 * 1000)
586+ ))
587+ .bind(&[members, members].concat())?
588+ .first::<Count>(None)
589+ .await?
590+ .and_then(|c| c.n)
591+ .unwrap_or(0),
592+ )
593+ };
594+ #[derive(Deserialize)]
595+ struct Month {
596+ charged: Option<i64>,
597+ unpaid: Option<i64>,
564598 }
565− let mut charged = vec![];
566− for month in &months {
567− #[derive(Deserialize)]
568− struct Month {
569− charged: Option<i64>,
570− unpaid: Option<i64>,
571− }
599+ let read_month = |month: &String| {
572600 let next = {
573601 let year: i32 = month[..4].parse().unwrap_or(1970);
574602 let number: u32 = month[5..7].parse().unwrap_or(1);
575603 if number == 12 { format!("{}-01", year + 1) } else { format!("{year}-{:02}", number + 1) }
576604 };
577− let row = self
578− .db
579− .prepare(format!(
580− "SELECT
581− (SELECT -SUM(amount_micros) FROM ledger WHERE kind = 'usage' AND workspace IN ({marks})
582− AND created_at >= '{month}-01' AND created_at < '{next}-01') AS charged,
583− (SELECT COUNT(*) FROM workspace_invoices WHERE workspace IN ({marks}) AND reason = 'month'
584− AND period = '{month}' AND status <> 'paid') AS unpaid"
585− ))
586− .bind(&[members, members].concat())?
587− .first::<Month>(None)
588− .await?;
605+ let sql = format!(
606+ "SELECT
607+ (SELECT -SUM(amount_micros) FROM ledger WHERE kind = 'usage' AND workspace IN ({marks})
608+ AND created_at >= '{month}-01' AND created_at < '{next}-01') AS charged,
609+ (SELECT COUNT(*) FROM workspace_invoices WHERE workspace IN ({marks}) AND reason = 'month'
610+ AND period = '{month}' AND status <> 'paid') AS unpaid"
611+ );
612+ async move { self.db.prepare(sql).bind(&[members, members].concat())?.first::<Month>(None).await }
613+ };
614+ // The check for trouble and the three months are read at once; the
615+ // answer is the one reading them in turn and stopping early gives.
616+ let (troubled, rows) = try_join(troubled, try_join_all(months.iter().map(read_month))).await?;
617+ if troubled > 0 {
618+ return Ok(None);
619+ }
620+ let mut charged = vec![];
621+ for row in rows {
589622 let Some(row) = row else { return Ok(None) };
590623 if row.unpaid.unwrap_or(0) > 0 {
591624 return Ok(None);
+58−8
3030
3131 use g1t_contracts::repos::{GitService, WorkspaceGitOperations};
3232 use serde::Deserialize;
33−use worker::wasm_bindgen::JsValue;
3433 use worker::{D1Database, Env, Fetcher, Response, Result};
3534
3635 /// What a request to g1t's git endpoints asks the store.
197196 Ok(())
198197 }
199198
199+/// The hours of `month` (`YYYY-MM`) from `since` on, as the range
200+/// `[from, until)` of hour keys; `None` for a month that is not one.
201+/// A range on `hour` is what the table's key can find; `substr` is not.
202+pub fn month_hours(month: &str, since: Option<&str>) -> Option<(String, String)> {
203+ let year: u32 = month.get(..4)?.parse().ok()?;
204+ let number: u32 = month.get(5..7)?.parse().ok()?;
205+ if month.len() != 7 || &month[4..5] != "-" || !(1..=12).contains(&number) {
206+ return None;
207+ }
208+ let until = if number == 12 { format!("{}-01-01", year + 1) } else { format!("{year}-{:02}-01", number + 1) };
209+ let start = format!("{month}-01");
210+ let from = match since {
211+ Some(since) if since > start.as_str() => since.to_owned(),
212+ _ => start,
213+ };
214+ Some((from, until))
215+}
216+
200217 /// Each workspace's operations in `month`, from `since` (an hour) on.
201218 pub async fn totals(db: &D1Database, month: &str, since: Option<&str>, namespace: Option<&str>) -> Result<Vec<WorkspaceGitOperations>> {
202219 #[derive(Deserialize)]
204221 namespace: String,
205222 operations: Option<f64>,
206223 }
207− Ok(db
208− .prepare(
209− "SELECT namespace, SUM(operations) AS operations FROM git_operations
210− WHERE substr(hour, 1, 7) = ?1 AND hour >= COALESCE(?2, '') AND (?3 IS NULL OR namespace = ?3)
211− GROUP BY namespace",
212− )
213− .bind(&[month.into(), since.map_or(JsValue::NULL, JsValue::from), namespace.map_or(JsValue::NULL, JsValue::from)])?
224+ let Some((from, until)) = month_hours(month, since) else { return Ok(vec![]) };
225+ // One workspace's is read by the table's key, namespace first; every
226+ // workspace's (billing's daily measure) reads the month's hours.
227+ let statement = match namespace {
228+ Some(namespace) => db
229+ .prepare(
230+ "SELECT namespace, SUM(operations) AS operations FROM git_operations
231+ WHERE namespace = ?1 AND hour >= ?2 AND hour < ?3
232+ GROUP BY namespace",
233+ )
234+ .bind(&[namespace.into(), from.as_str().into(), until.as_str().into()])?,
235+ None => db
236+ .prepare(
237+ "SELECT namespace, SUM(operations) AS operations FROM git_operations
238+ WHERE hour >= ?1 AND hour < ?2
239+ GROUP BY namespace",
240+ )
241+ .bind(&[from.as_str().into(), until.as_str().into()])?,
242+ };
243+ Ok(statement
214244 .all()
215245 .await?
216246 .results::<Row>()?
297327 assert_eq!(hour_key("2026-10-14T09:59:59.000Z"), "2026-10-14T09");
298328 }
299329
330+ #[test]
331+ fn a_months_hours_are_a_range_on_the_key() {
332+ let range = |month: &str, since: Option<&str>| month_hours(month, since);
333+ assert_eq!(range("2026-10", None), Some(("2026-10-01".to_owned(), "2026-11-01".to_owned())));
334+ assert_eq!(range("2026-12", None), Some(("2026-12-01".to_owned(), "2027-01-01".to_owned())));
335+ // `since` narrows the start, never widens it past the month.
336+ assert_eq!(range("2026-10", Some("2026-10-14T00")).map(|r| r.0), Some("2026-10-14T00".to_owned()));
337+ assert_eq!(range("2026-10", Some("2026-09-30T23")).map(|r| r.0), Some("2026-10-01".to_owned()));
338+ assert_eq!(range("2026-10", Some("")).map(|r| r.0), Some("2026-10-01".to_owned()));
339+ // Every hour of the month is in it, and none of the next or last,
340+ // as `substr(hour, 1, 7) = month` had it.
341+ let (from, until) = range("2026-10", None).unwrap();
342+ let inside = |hour: &str| hour >= from.as_str() && hour < until.as_str();
343+ assert!(inside("2026-10-01T00") && inside("2026-10-31T23"));
344+ assert!(!inside("2026-09-30T23") && !inside("2026-11-01T00"));
345+ assert_eq!(range("2026-13", None), None);
346+ assert_eq!(range("2026-1", None), None);
347+ assert_eq!(range("", None), None);
348+ }
349+
300350 fn pkt(payload: &str) -> Vec<u8> {
301351 format!("{:04x}{payload}", payload.len() + 4).into_bytes()
302352 }