Every sandbox on g1t's runners has 20 GB of disk, the most Cloudflare Containers give one, as much or more than a GitHub-hosted ubuntu-latest's 14 GB: the standard machine (ubuntu-latest and every other hosted label, and all other work) becomes a custom instance type of 1 vCPU, 4 GiB and 20 GB, where standard-1 had half a vCPU and 8 GB, and g1t-2core a custom 2 vCPU, 8 GiB and 20 GB, where standard-3 had 16 GB; g1t-4core stays standard-4, which already had 20 GB. 1 vCPU is the smallest a custom type may have, and Cloudflare bills CPU only as it is used, so the doubled vCPU costs only what a job uses of it. The contracts' instance table, its TypeScript mirror and the billing keeper's standard sandbox say the same sizes; the price scales, worked out as before from Cloudflare's list prices with every vCPU busy and the Durable Object behind each, are now 1.9 for g1t-2core and 3.4 for g1t-4core, against the larger standard machine, and the fallbacks for a sandbox second (33 millionths of a dollar with its vCPU busy) and a build second (16) follow it. A test holds wrangler.jsonc's sizes to the contracts', and another the scales to the list prices. The docs' machine tables, the larger machines' prices and the Cloudflare deploy guide say so.
14 files+136−570/14 viewed
| 319 | 319 | | Limit | Value | | |
| 320 | 320 | | --- | --- | | |
| 321 | 321 | | Largest machine | 4 vCPUs, 12 GiB of memory, 20 GB of disk (`g1t-4core`). No GPUs. | | |
| 322 | + | | Disk | 20 GB on every machine, the most Cloudflare Containers give one. | | |
| 322 | 323 | | One job on g1t's machines | 60 minutes. On a self-hosted runner, 24 hours. | | |
| 323 | 324 | | One cache entry | 2 GiB, compressed. A larger one is not saved. | | |
| 324 | 325 | | A repository's caches | 10 GiB together. Past it, the entries restored longest ago are removed. | |
| 73 | 73 | ||
| 74 | 74 | | `runs-on` | Class | Size | | |
| 75 | 75 | | --- | --- | --- | | |
| 76 | − | | Any other hosted label, and every other kind of work | `AttemptSandbox` | Cloudflare's `standard-1` | | |
| 77 | − | | `g1t-2core` | `Sandbox2Core` | 2 vCPU, 8 GiB memory, 16 GB disk | | |
| 78 | − | | `g1t-4core` | `Sandbox4Core` | 4 vCPU, 12 GiB memory, 20 GB disk | | |
| 76 | + | | Any other hosted label, and every other kind of work | `AttemptSandbox` | 1 vCPU, 4 GiB memory, 20 GB disk (a custom instance type) | | |
| 77 | + | | `g1t-2core` | `Sandbox2Core` | 2 vCPU, 8 GiB memory, 20 GB disk (a custom instance type) | | |
| 78 | + | | `g1t-4core` | `Sandbox4Core` | 4 vCPU, 12 GiB memory, 20 GB disk (Cloudflare's `standard-4`) | | |
| 79 | + | ||
| 80 | + | Each has the 20 GB of disk Containers allow at most. The named instance | |
| 81 | + | types with 1 and 2 vCPU come with 8 and 16 GB, so those two are custom | |
| 82 | + | instance types (`instance_type` with `vcpu`, `memory_mib` and `disk_mb` in | |
| 83 | + | `services/runner/wrangler.jsonc`); 1 vCPU is the smallest a custom type may | |
| 84 | + | have. | |
| 79 | 85 | ||
| 80 | 86 | Every sandbox starts from one image: `services/runner/base/Dockerfile`, | |
| 81 | 87 | which has Node, Python, Go, Rust, Java, .NET, Ruby and Docker, with the |
| 542 | 542 | ||
| 543 | 543 | | `runs-on` | vCPUs | Memory | Disk | | |
| 544 | 544 | | --- | --- | --- | --- | | |
| 545 | − | | `ubuntu-latest`, or any other Linux label | 0.5 | 4 GiB | 8 GB | | |
| 546 | − | | `g1t-2core` | 2 | 8 GiB | 16 GB | | |
| 545 | + | | `ubuntu-latest`, or any other Linux label | 1 | 4 GiB | 20 GB | | |
| 546 | + | | `g1t-2core` | 2 | 8 GiB | 20 GB | | |
| 547 | 547 | | `g1t-4core` | 4 | 12 GiB | 20 GB | | |
| 548 | 548 | ||
| 549 | 549 | ```yaml |
| 305 | 305 | minutes, so a deploy never cuts one short; an agent run whose guardrails | |
| 306 | 306 | allow more than two hours can be. | |
| 307 | 307 | ||
| 308 | − | Jobs choose larger machines with `runs-on: g1t-2core` or `g1t-4core`. Your | |
| 309 | − | account's Containers limits must allow `standard-4`. | |
| 308 | + | Jobs choose larger machines with `runs-on: g1t-2core` or `g1t-4core`. | |
| 309 | + | Every sandbox has 20 GB of disk: the standard one and `g1t-2core` are | |
| 310 | + | custom instance types (1 vCPU and 4 GiB, and 2 vCPU and 8 GiB), and | |
| 311 | + | `g1t-4core` is `standard-4`. Your account's Containers limits must allow | |
| 312 | + | all three. | |
| 310 | 313 | ||
| 311 | 314 | Set the runner Worker's `DOCKER` variable to `off` to give workflow jobs no | |
| 312 | 315 | Docker Engine. |
| 140 | 140 | | What | Unit | Costs g1t | You pay | | |
| 141 | 141 | | --- | --- | --- | --- | | |
| 142 | 142 | | Sandbox time (workflows, checks, the merge queue) | Second | About $0.001 a minute | About $0.0012 a minute | | |
| 143 | − | | [Larger machines](#workflow-jobs-on-larger-machines) for workflow jobs (`g1t-2core`, `g1t-4core`) | Second | About 2.8 and 5.1 times a sandbox second | Cost + 20% | | |
| 143 | + | | [Larger machines](#workflow-jobs-on-larger-machines) for workflow jobs (`g1t-2core`, `g1t-4core`) | Second | About 1.9 and 3.4 times a sandbox second | Cost + 20% | | |
| 144 | 144 | | Deploy builds | Second | About $0.001 a minute | About $0.0012 a minute | | |
| 145 | 145 | | App requests | Million | $0.30 | $0.36 | | |
| 146 | 146 | | App CPU time | Million CPU milliseconds | $0.02 | $0.024 | | |
| ⋯ | |||
| 356 | 356 | ||
| 357 | 357 | A workflow job whose `runs-on` names a [larger machine](/guides/actions/#machine-sizes) | |
| 358 | 358 | runs on a larger Cloudflare Containers instance, and its seconds cost what | |
| 359 | − | that instance costs g1t, plus 20%: | |
| 359 | + | that instance costs g1t, plus 20%. The standard machine they are compared | |
| 360 | + | with has 1 vCPU, 4 GiB and 20 GB: | |
| 360 | 361 | ||
| 361 | 362 | | `runs-on` | Instance | Its memory and disk, against the standard machine's | A second with its vCPUs as busy as an average sandbox's | | |
| 362 | 363 | | --- | --- | --- | --- | | |
| 363 | − | | `g1t-2core` | 2 vCPU, 8 GiB, 16 GB | 1.9 times | About 2.8 times a sandbox second | | |
| 364 | − | | `g1t-4core` | 4 vCPU, 12 GiB, 20 GB | 2.7 times | About 5.1 times a sandbox second | | |
| 364 | + | | `g1t-2core` | 2 vCPU, 8 GiB, 20 GB | 1.8 times | About 1.9 times a sandbox second | | |
| 365 | + | | `g1t-4core` | 4 vCPU, 12 GiB, 20 GB | 2.5 times | About 3.4 times a sandbox second | | |
| 365 | 366 | ||
| 366 | 367 | A job that reports its own CPU is priced on it: its memory and disk for | |
| 367 | 368 | every second, and the vCPU-seconds it used. One that does not is priced | |
| 1038 | 1038 | } | |
| 1039 | 1039 | ||
| 1040 | 1040 | /// A size of machine g1t runs workflow jobs on, asked for by a label in | |
| 1041 | − | /// `runs-on`. Each is a Cloudflare Containers instance type; it costs what | |
| 1042 | − | /// that instance costs g1t, plus the margin, like any sandbox time. | |
| 1041 | + | /// `runs-on`. Each is a Cloudflare Containers instance type (named, or a | |
| 1042 | + | /// custom one: services/runner/wrangler.jsonc), every one with the 20 GB of | |
| 1043 | + | /// disk Containers allow at most; it costs what that instance costs g1t, | |
| 1044 | + | /// plus the margin, like any sandbox time. | |
| 1043 | 1045 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 1044 | 1046 | pub struct InstanceType { | |
| 1045 | 1047 | /// The `runs-on` label, or `standard` for the default. | |
| 1046 | 1048 | pub label: &'static str, | |
| 1047 | − | /// The Containers instance type. | |
| 1049 | + | /// The Containers instance type: a named one, or `custom` for one | |
| 1050 | + | /// sized by its vCPU, memory and disk. | |
| 1048 | 1051 | pub container: &'static str, | |
| 1049 | 1052 | pub vcpu: f64, | |
| 1050 | 1053 | pub memory_gib: f64, | |
| ⋯ | |||
| 1052 | 1055 | /// What a second of it costs g1t as a multiple of the standard | |
| 1053 | 1056 | /// machine's, with its vCPUs as busy (Cloudflare's list prices: | |
| 1054 | 1057 | /// memory $0.0000025 a GiB-second, disk $0.00000007 a GB-second, vCPU | |
| 1055 | − | /// $0.00002 a second). Used to reserve before a job starts, and to | |
| 1056 | − | /// price a job that did not report its own CPU. | |
| 1058 | + | /// $0.00002 a second, and the Durable Object behind it at 128 MB, | |
| 1059 | + | /// $0.0000125 a GB-second), to one decimal. Used to reserve before a | |
| 1060 | + | /// job starts, and to price a job that did not report its own CPU. | |
| 1057 | 1061 | pub price_scale: f64, | |
| 1058 | 1062 | } | |
| 1059 | 1063 | ||
| 1060 | 1064 | /// The default: what `ubuntu-latest` and every other hosted label get. | |
| 1061 | 1065 | pub const STANDARD_INSTANCE: InstanceType = | |
| 1062 | − | InstanceType { label: "standard", container: "standard-1", vcpu: 0.5, memory_gib: 4.0, disk_gb: 8.0, price_scale: 1.0 }; | |
| 1066 | + | InstanceType { label: "standard", container: "custom", vcpu: 1.0, memory_gib: 4.0, disk_gb: 20.0, price_scale: 1.0 }; | |
| 1063 | 1067 | ||
| 1064 | 1068 | /// Every machine a workflow job can ask for, the default first. | |
| 1065 | 1069 | pub const INSTANCE_TYPES: [InstanceType; 3] = [ | |
| 1066 | 1070 | STANDARD_INSTANCE, | |
| 1067 | − | InstanceType { label: "g1t-2core", container: "standard-3", vcpu: 2.0, memory_gib: 8.0, disk_gb: 16.0, price_scale: 2.8 }, | |
| 1068 | − | InstanceType { label: "g1t-4core", container: "standard-4", vcpu: 4.0, memory_gib: 12.0, disk_gb: 20.0, price_scale: 5.1 }, | |
| 1071 | + | InstanceType { label: "g1t-2core", container: "custom", vcpu: 2.0, memory_gib: 8.0, disk_gb: 20.0, price_scale: 1.9 }, | |
| 1072 | + | InstanceType { label: "g1t-4core", container: "standard-4", vcpu: 4.0, memory_gib: 12.0, disk_gb: 20.0, price_scale: 3.4 }, | |
| 1069 | 1073 | ]; | |
| 1070 | 1074 | ||
| 1071 | 1075 | /// The machine a job's `runs-on` labels ask for: the largest named, or the | |
| ⋯ | |||
| 1495 | 1499 | ||
| 1496 | 1500 | #[test] | |
| 1497 | 1501 | fn runs_on_picks_the_machine() { | |
| 1498 | − | assert_eq!(instance_for(&labels(&["ubuntu-latest"])).container, "standard-1"); | |
| 1502 | + | assert_eq!(instance_for(&labels(&["ubuntu-latest"])), STANDARD_INSTANCE); | |
| 1499 | 1503 | assert_eq!(instance_for(&labels(&[])).label, "standard"); | |
| 1500 | 1504 | assert_eq!(instance_for(&labels(&["g1t-4core"])).container, "standard-4"); | |
| 1501 | − | assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"])).container, "standard-3"); | |
| 1505 | + | assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"])).label, "g1t-2core"); | |
| 1502 | 1506 | // Both named: the larger. | |
| 1503 | 1507 | assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"])).label, "g1t-4core"); | |
| 1504 | 1508 | assert_eq!(instance_named("g1t-4core").map(|i| i.vcpu), Some(4.0)); | |
| ⋯ | |||
| 1507 | 1511 | } | |
| 1508 | 1512 | ||
| 1509 | 1513 | #[test] | |
| 1514 | + | fn every_machine_has_the_most_disk_containers_allow_and_fits_a_custom_type() { | |
| 1515 | + | for instance in INSTANCE_TYPES { | |
| 1516 | + | assert_eq!(instance.disk_gb, 20.0, "{}", instance.label); | |
| 1517 | + | // Custom instance types: 1 to 4 vCPU, at most 12 GiB, at least | |
| 1518 | + | // 3 GiB a vCPU. | |
| 1519 | + | assert!((1.0..=4.0).contains(&instance.vcpu), "{}", instance.label); | |
| 1520 | + | assert!(instance.memory_gib <= 12.0 && instance.memory_gib >= 3.0 * instance.vcpu, "{}", instance.label); | |
| 1521 | + | } | |
| 1522 | + | } | |
| 1523 | + | ||
| 1524 | + | #[test] | |
| 1525 | + | fn price_scales_follow_the_list_prices_with_every_vcpu_busy() { | |
| 1526 | + | // Memory, disk and vCPU at Cloudflare's list prices, and the | |
| 1527 | + | // Durable Object behind the container (128 MB), in micros a second. | |
| 1528 | + | let busy = |i: &InstanceType| i.memory_gib * 2.5 + i.disk_gb * 0.07 + i.vcpu * 20.0 + 0.125 * 12.5; | |
| 1529 | + | for instance in INSTANCE_TYPES { | |
| 1530 | + | let scale = busy(&instance) / busy(&STANDARD_INSTANCE); | |
| 1531 | + | assert!((instance.price_scale - (scale * 10.0).round() / 10.0).abs() < 1e-9, "{}: {scale}", instance.label); | |
| 1532 | + | } | |
| 1533 | + | } | |
| 1534 | + | ||
| 1535 | + | #[test] | |
| 1510 | 1536 | fn start_args_from_older_callers_read() { | |
| 1511 | 1537 | let args: StartJobArgs = serde_json::from_value(serde_json::json!({ | |
| 1512 | 1538 | "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30 | |
| 385 | 385 | pub const MICROS_PER_MILLION_REQUESTS: i64 = 300_000; | |
| 386 | 386 | /// $0.02 per million CPU milliseconds. | |
| 387 | 387 | pub const MICROS_PER_MILLION_CPU_MS: i64 = 20_000; | |
| 388 | − | /// What one second of a build's sandbox costs g1t (Cloudflare | |
| 389 | − | /// Containers, standard-1: half a vCPU, 4 GiB, 8 GB disk), rounded up, | |
| 390 | − | /// as the price keeper measured it on 2026-10-05 (14.5). Only a | |
| 391 | − | /// fallback: billing charges builds at the price book's `build_second`, | |
| 392 | − | /// which the keeper keeps current. | |
| 393 | − | pub const MICROS_PER_BUILD_SECOND: i64 = 15; | |
| 388 | + | /// What one second of a build's sandbox costs g1t (the standard | |
| 389 | + | /// sandbox, `actions::STANDARD_INSTANCE`), rounded up: the price keeper | |
| 390 | + | /// measured 14.5 on 2026-10-05, when it had 8 GB of disk, and its 20 GB | |
| 391 | + | /// now add 0.84. Only a fallback: billing charges builds at the price | |
| 392 | + | /// book's `build_second`, which the keeper keeps current. | |
| 393 | + | pub const MICROS_PER_BUILD_SECOND: i64 = 16; | |
| 394 | 394 | /// What one custom hostname costs g1t a month (Cloudflare for SaaS): | |
| 395 | 395 | /// $0.10. | |
| 396 | 396 | pub const MICROS_PER_DOMAIN_MONTH: i64 = 100_000; |
| 249 | 249 | ||
| 250 | 250 | /** | |
| 251 | 251 | * What one second of a sandbox costs g1t, in millionths of a dollar, when | |
| 252 | − | * the price book cannot be read: Containers standard-1 with its Durable | |
| 253 | − | * Object, rounded up. | |
| 252 | + | * the price book cannot be read: the standard sandbox (1 vCPU, 4 GiB, 20 GB) | |
| 253 | + | * with its vCPU busy and its Durable Object, 32.96 at list prices, rounded | |
| 254 | + | * up. | |
| 254 | 255 | */ | |
| 255 | − | export const FALLBACK_SANDBOX_MICROS_PER_SECOND = 25; | |
| 256 | + | export const FALLBACK_SANDBOX_MICROS_PER_SECOND = 33; | |
| 256 | 257 | ||
| 257 | 258 | /** A sandbox for `minutes` at `microsPerSecond`. */ | |
| 258 | 259 | export function sandboxEstimateMicros(minutes: number, microsPerSecond: number): number { |
| 1 | 1 | /** | |
| 2 | 2 | * Sizes of machine g1t runs workflow jobs on, asked for by a label in | |
| 3 | 3 | * `runs-on`. Mirrors `g1t_contracts::actions::INSTANCE_TYPES`. Each is a | |
| 4 | − | * Cloudflare Containers instance type, and costs what that instance costs | |
| 5 | − | * g1t, plus the margin, like any sandbox time. | |
| 4 | + | * Cloudflare Containers instance type (named, or a custom one sized in | |
| 5 | + | * services/runner/wrangler.jsonc), every one with the 20 GB of disk | |
| 6 | + | * Containers allow at most, and costs what that instance costs g1t, plus | |
| 7 | + | * the margin, like any sandbox time. | |
| 6 | 8 | */ | |
| 7 | 9 | export type InstanceType = { | |
| 8 | 10 | /** The `runs-on` label, or `standard` for the default. */ | |
| 9 | 11 | label: string; | |
| 10 | − | /** The Containers instance type. */ | |
| 12 | + | /** The Containers instance type: a named one, or `custom`. */ | |
| 11 | 13 | container: string; | |
| 12 | 14 | vcpu: number; | |
| 13 | 15 | memoryGib: number; | |
| ⋯ | |||
| 24 | 26 | /** The default: what `ubuntu-latest` and every other hosted label get. */ | |
| 25 | 27 | export const STANDARD_INSTANCE: InstanceType = { | |
| 26 | 28 | label: "standard", | |
| 27 | − | container: "standard-1", | |
| 28 | − | vcpu: 0.5, | |
| 29 | + | container: "custom", | |
| 30 | + | vcpu: 1, | |
| 29 | 31 | memoryGib: 4, | |
| 30 | − | diskGb: 8, | |
| 32 | + | diskGb: 20, | |
| 31 | 33 | estimateScale: 1, | |
| 32 | 34 | }; | |
| 33 | 35 | ||
| 34 | 36 | /** | |
| 35 | 37 | * Every machine a job can ask for, the default first. The scales follow | |
| 36 | 38 | * Cloudflare's list prices: memory $0.0000025 a GiB-second, disk | |
| 37 | − | * $0.00000007 a GB-second, vCPU $0.00002 a second. | |
| 39 | + | * $0.00000007 a GB-second, vCPU $0.00002 a second, and the Durable Object | |
| 40 | + | * behind each at 128 MB, $0.0000125 a GB-second; to one decimal. | |
| 38 | 41 | */ | |
| 39 | 42 | export const INSTANCE_TYPES: readonly InstanceType[] = [ | |
| 40 | 43 | STANDARD_INSTANCE, | |
| 41 | − | { label: "g1t-2core", container: "standard-3", vcpu: 2, memoryGib: 8, diskGb: 16, estimateScale: 2.8 }, | |
| 42 | − | { label: "g1t-4core", container: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20, estimateScale: 5.1 }, | |
| 44 | + | { label: "g1t-2core", container: "custom", vcpu: 2, memoryGib: 8, diskGb: 20, estimateScale: 1.9 }, | |
| 45 | + | { label: "g1t-4core", container: "standard-4", vcpu: 4, memoryGib: 12, diskGb: 20, estimateScale: 3.4 }, | |
| 43 | 46 | ]; | |
| 44 | 47 | ||
| 45 | 48 | /** An instance type by its label; the standard one for anything else. */ | |
| 854 | 854 | ||
| 855 | 855 | #[test] | |
| 856 | 856 | fn every_build_second_is_metered_at_cost_plus_the_margin() { | |
| 857 | − | // A 5-minute build at 15 millionths a second costs g1t 4,500, and | |
| 857 | + | // A 5-minute build at 16 millionths a second costs g1t 4,800, and | |
| 858 | 858 | // is charged at cost plus 20%, from the first second: there are no | |
| 859 | 859 | // included build minutes, only the plan's included usage. | |
| 860 | 860 | let cost = 300 * costs::MICROS_PER_BUILD_SECOND; | |
| 861 | − | assert_eq!(cost, 4_500); | |
| 862 | − | assert_eq!(crate::credits::with_margin(cost, 20), 5_400); | |
| 861 | + | assert_eq!(cost, 4_800); | |
| 862 | + | assert_eq!(crate::credits::with_margin(cost, 20), 5_760); | |
| 863 | 863 | } | |
| 864 | 864 | ||
| 865 | 865 | #[test] |
| 187 | 187 | memory_byte_seconds: f64, | |
| 188 | 188 | } | |
| 189 | 189 | ||
| 190 | − | /// g1t's sandboxes: Containers' standard-1, half a vCPU, 4 GiB, 8 GB disk. | |
| 190 | + | /// g1t's standard sandbox (`STANDARD_INSTANCE` in the contracts): a custom | |
| 191 | + | /// Containers instance type of 1 vCPU, 4 GiB and 20 GB of disk. | |
| 191 | 192 | const SANDBOX_GIB: f64 = 4.0; | |
| 192 | − | const SANDBOX_DISK_GB: f64 = 8.0; | |
| 193 | + | const SANDBOX_DISK_GB: f64 = 20.0; | |
| 193 | 194 | const GIB: f64 = 1024.0 * 1024.0 * 1024.0; | |
| 194 | 195 | /// The Durable Object behind each container is billed for as long as the | |
| 195 | 196 | /// container runs, at 128 MB. | |
| ⋯ | |||
| 565 | 566 | let usage = ContainerUsage { cpu_seconds: 720.0, memory_byte_seconds: 3600.0 * 4.0 * GIB }; | |
| 566 | 567 | let micros = | |
| 567 | 568 | sandbox_second_micros(usage, LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_VCPU_SECOND, LIST_DO_GB_SECOND).unwrap(); | |
| 568 | − | // 4 x 2.5 + 8 x 0.07 + 0.125 x 12.5 + 0.2 x 20 = 16.1225 | |
| 569 | − | assert!((micros - 16.1225).abs() < 1e-9, "{micros}"); | |
| 570 | − | // The Durable Object adds about 11% to the second it left out. | |
| 571 | − | assert!((sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND) - 12.1225).abs() < 1e-9); | |
| 569 | + | // 4 x 2.5 + 20 x 0.07 + 0.125 x 12.5 + 0.2 x 20 = 16.9625 | |
| 570 | + | assert!((micros - 16.9625).abs() < 1e-9, "{micros}"); | |
| 571 | + | // The Durable Object adds about 14% to the second it left out. | |
| 572 | + | assert!((sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND) - 12.9625).abs() < 1e-9); | |
| 572 | 573 | // Too little use to say anything. | |
| 573 | 574 | assert!(sandbox_second_micros(ContainerUsage { cpu_seconds: 1.0, memory_byte_seconds: GIB }, 1.0, 1.0, 1.0, 1.0).is_none()); | |
| 574 | 575 | } | |
| ⋯ | |||
| 577 | 578 | fn a_run_that_reports_its_cpu_is_priced_on_it() { | |
| 578 | 579 | let base = sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND); | |
| 579 | 580 | let vcpu = LIST_VCPU_SECOND * MICROS_PER_DOLLAR as f64; | |
| 580 | − | // A 10-minute cargo build that kept its half vCPU busy throughout. | |
| 581 | + | // A 10-minute cargo build that kept half its vCPU busy throughout. | |
| 581 | 582 | let heavy = run_cost(600, 300.0, base, vcpu); | |
| 582 | − | assert!((heavy - (600.0 * 12.1225 + 300.0 * 20.0)).abs() < 1e-6); | |
| 583 | + | assert!((heavy - (600.0 * 12.9625 + 300.0 * 20.0)).abs() < 1e-6); | |
| 583 | 584 | // The same ten minutes, mostly idle, costs less. | |
| 584 | 585 | let light = run_cost(600, 30.0, base, vcpu); | |
| 585 | 586 | assert!(light < heavy); | |
| ⋯ | |||
| 589 | 590 | assert_eq!(run_cost(0, -1.0, base, vcpu), 0.0); | |
| 590 | 591 | // A larger machine's base: its memory and disk, not its CPU. | |
| 591 | 592 | assert!((base_scale(SANDBOX_GIB, SANDBOX_DISK_GB) - 1.0).abs() < 1e-12); | |
| 592 | − | assert!((base_scale(12.0, 20.0) - 2.72).abs() < 0.01, "{}", base_scale(12.0, 20.0)); | |
| 593 | − | assert!((base_scale(8.0, 16.0) - 1.87).abs() < 0.01, "{}", base_scale(8.0, 16.0)); | |
| 593 | + | assert!((base_scale(12.0, 20.0) - 2.54).abs() < 0.01, "{}", base_scale(12.0, 20.0)); | |
| 594 | + | assert!((base_scale(8.0, 20.0) - 1.77).abs() < 0.01, "{}", base_scale(8.0, 20.0)); | |
| 595 | + | // Each machine in the contracts, priced from its own memory and disk. | |
| 596 | + | for instance in g1t_contracts::actions::INSTANCE_TYPES { | |
| 597 | + | let scale = base_scale(instance.memory_gib, instance.disk_gb); | |
| 598 | + | assert!(scale >= 1.0 && scale < instance.price_scale.max(1.0) + 1e-9, "{}: {scale}", instance.label); | |
| 599 | + | } | |
| 594 | 600 | } | |
| 595 | 601 | ||
| 596 | 602 | #[test] | |
| 228 | 228 | const gate = new ComputeGate(billing({}).binding, memory(), quiet); | |
| 229 | 229 | assert.equal(await gate.microsPerSecond(), 20); | |
| 230 | 230 | const down = new ComputeGate(billing({ fail: new Set(["prices"]) }).binding, memory(), quiet); | |
| 231 | − | assert.equal(await down.microsPerSecond(), 25); | |
| 231 | + | assert.equal(await down.microsPerSecond(), 33); | |
| 232 | 232 | }); | |
| 233 | 233 | ||
| 234 | 234 | // ---- Caps ------------------------------------------------------------------------------- |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | + | import { readFileSync } from "node:fs"; | |
| 2 | 3 | import { test } from "node:test"; | |
| 3 | 4 | ||
| 4 | 5 | import type { WorkflowDomain } from "@g1t/contracts"; | |
| 5 | 6 | ||
| 7 | + | import { INSTANCE_TYPES } from "../../../packages/contracts/src/instances.ts"; | |
| 8 | + | ||
| 6 | 9 | import { SANDBOX_BINDINGS, jobHosts, sandboxNamespace } from "./egress.ts"; | |
| 7 | 10 | ||
| 8 | 11 | const domains: WorkflowDomain[] = [ | |
| ⋯ | |||
| 42 | 45 | assert.equal(sandboxNamespace({ SANDBOX: "standard" }, "Sandbox4Core"), "standard"); | |
| 43 | 46 | assert.deepEqual(Object.keys(SANDBOX_BINDINGS), ["AttemptSandbox", "Sandbox2Core", "Sandbox4Core"]); | |
| 44 | 47 | }); | |
| 48 | + | ||
| 49 | + | test("each sandbox class is the size the contracts give its runs-on label", () => { | |
| 50 | + | type Size = { vcpu: number; memory_mib: number; disk_mb: number }; | |
| 51 | + | // wrangler.jsonc's comments are whole lines. | |
| 52 | + | const text = readFileSync(new URL("../wrangler.jsonc", import.meta.url), "utf8").replace(/^\s*\/\/.*$/gm, ""); | |
| 53 | + | const config = JSON.parse(text) as { containers: { class_name: string; instance_type: string | Size }[] }; | |
| 54 | + | // The named Cloudflare instance types a machine may be. | |
| 55 | + | const named: Record<string, Size> = { "standard-4": { vcpu: 4, memory_mib: 12 * 1024, disk_mb: 20_000 } }; | |
| 56 | + | const classes = Object.keys(SANDBOX_BINDINGS); | |
| 57 | + | assert.equal(INSTANCE_TYPES.length, classes.length); | |
| 58 | + | INSTANCE_TYPES.forEach((instance, i) => { | |
| 59 | + | const type = config.containers.find((c) => c.class_name === classes[i])?.instance_type; | |
| 60 | + | const size = typeof type === "string" ? named[type] : type; | |
| 61 | + | assert.ok(size, `${classes[i]}: ${JSON.stringify(type)}`); | |
| 62 | + | assert.equal(typeof type === "string" ? type : "custom", instance.container, instance.label); | |
| 63 | + | assert.deepEqual( | |
| 64 | + | { vcpu: size.vcpu, memoryGib: size.memory_mib / 1024, diskGb: size.disk_mb / 1000 }, | |
| 65 | + | { vcpu: instance.vcpu, memoryGib: instance.memoryGib, diskGb: instance.diskGb }, | |
| 66 | + | instance.label, | |
| 67 | + | ); | |
| 68 | + | }); | |
| 69 | + | }); | |
| 5 | 5 | "compatibility_date": "2026-09-26", | |
| 6 | 6 | "main": "./src/index.ts", | |
| 7 | 7 | "workers_dev": false, | |
| 8 | − | // One image (the base plus the runner binary), on three machine sizes. | |
| 8 | + | // One image (the base plus the runner binary), on three machine sizes, | |
| 9 | + | // each with the 20 GB of disk Containers allow at most. The standard and | |
| 10 | + | // g1t-2core machines are custom instance types (1 vCPU is the smallest a | |
| 11 | + | // custom type may have, with at least 3 GiB of memory per vCPU), since | |
| 12 | + | // the named ones that size come with 8 and 16 GB. crates/contracts | |
| 13 | + | // (INSTANCE_TYPES) and services/billing (keeper.rs) say the same sizes. | |
| 9 | 14 | // scripts/deploy.mjs deploys with each image set to the reference it | |
| 10 | 15 | // built and pushed ("The runner's images" in | |
| 11 | 16 | // docs.g1t.sh/guides/deploy-to-cloudflare/), so a deploy builds | |
| ⋯ | |||
| 14 | 19 | { | |
| 15 | 20 | "class_name": "AttemptSandbox", | |
| 16 | 21 | "image": "./Dockerfile", | |
| 17 | − | "instance_type": "standard-1", | |
| 22 | + | // Workflow jobs with `runs-on: ubuntu-latest` (and every other | |
| 23 | + | // hosted label), and all other work: 1 vCPU, 4 GiB, 20 GB. | |
| 24 | + | "instance_type": { "vcpu": 1, "memory_mib": 4096, "disk_mb": 20000 }, | |
| 18 | 25 | "max_instances": 20, | |
| 19 | 26 | // A deploy replaces sandboxes. Ones that are busy are given this | |
| 20 | 27 | // long, in seconds, to finish first, so shipping g1t does not | |
| ⋯ | |||
| 22 | 29 | "rollout_active_grace_period": 7200 | |
| 23 | 30 | }, | |
| 24 | 31 | { | |
| 25 | − | // Workflow jobs with `runs-on: g1t-2core`: 2 vCPU, 8 GiB, 16 GB. | |
| 32 | + | // Workflow jobs with `runs-on: g1t-2core`: 2 vCPU, 8 GiB, 20 GB. | |
| 26 | 33 | "class_name": "Sandbox2Core", | |
| 27 | 34 | "image": "./Dockerfile", | |
| 28 | − | "instance_type": "standard-3", | |
| 35 | + | "instance_type": { "vcpu": 2, "memory_mib": 8192, "disk_mb": 20000 }, | |
| 29 | 36 | "max_instances": 10, | |
| 30 | 37 | "rollout_active_grace_period": 7200 | |
| 31 | 38 | }, | |