diff --git a/.github/workflows/cloud-deploy-relay-asia-topology.yml b/.github/workflows/cloud-deploy-relay-asia-topology.yml index 07fbc68056e..3e8929376be 100644 --- a/.github/workflows/cloud-deploy-relay-asia-topology.yml +++ b/.github/workflows/cloud-deploy-relay-asia-topology.yml @@ -78,6 +78,7 @@ jobs: production:production-gce-c30) ;; production:production-gce-c31) ;; production:production-gce-c32,production-gce-c33) target_region=us-central1 ;; + production:production-gce-c34) ;; *) echo "cell-ids do not match the reviewed environment topology" >&2; exit 1 ;; esac echo "TARGET_REGION=${target_region}" >> "${GITHUB_ENV}" diff --git a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml index af66e918386..913b6a24b17 100644 --- a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml +++ b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml @@ -194,7 +194,7 @@ jobs: EXPECTED_REGION=us-central1 EXPECTED_DATABASE_POOL_MAX= ;; - c27|c28|c29|c30|c31) + c27|c28|c29|c30|c31|c34) EXPECTED_HARD_CAP=3000 EXPECTED_REGION=asia-east2 EXPECTED_DATABASE_POOL_MAX=16 diff --git a/cloud/dev/scripts/operate-relay-asia-admission.mjs b/cloud/dev/scripts/operate-relay-asia-admission.mjs index d5736db3ed2..c79323a9b39 100644 --- a/cloud/dev/scripts/operate-relay-asia-admission.mjs +++ b/cloud/dev/scripts/operate-relay-asia-admission.mjs @@ -21,7 +21,7 @@ const SHAPES = { domain: 'relay.onorca.dev', allCells: [ 'production-gce-c27', 'production-gce-c28', 'production-gce-c29', 'production-gce-c30', - 'production-gce-c31', 'production-gce-c32', 'production-gce-c33' + 'production-gce-c31', 'production-gce-c32', 'production-gce-c33', 'production-gce-c34' ], // The launch set was registered together; each later cell registers alone beside it. registrationWaves: [ @@ -29,8 +29,10 @@ const SHAPES = { ['production-gce-c30'], ['production-gce-c31'], ['production-gce-c32'], - ['production-gce-c33'] + ['production-gce-c33'], + ['production-gce-c34'] ], + // C34 is a migration-only spare: no promotion wave until a reviewed change adds one. promotionWaves: [ ['production-gce-c27'], ['production-gce-c28', 'production-gce-c29'], diff --git a/cloud/dev/scripts/operate-relay-asia-admission.test.mjs b/cloud/dev/scripts/operate-relay-asia-admission.test.mjs index ac5aa89ba9f..a1b8eb3853c 100644 --- a/cloud/dev/scripts/operate-relay-asia-admission.test.mjs +++ b/cloud/dev/scripts/operate-relay-asia-admission.test.mjs @@ -553,11 +553,13 @@ test('accepts only reviewed Asia admission waves', () => { ['rollback', 'production-gce-c30'], ['rollback', 'production-gce-c31'], ['rollback', 'production-gce-c27,production-gce-c28,production-gce-c29'], - ['rollback', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33'], + ['rollback', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33,production-gce-c34'], ...['production-gce-c32', 'production-gce-c33'].flatMap((cellId) => [ 'inspect', 'verify', 'register', 'registered', 'promote', 'recover-promotion', 'rollback' ].map((mode) => [mode, cellId])), - ['inspect', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33'] + ...['inspect', 'verify', 'register', 'registered', 'rollback'] + .map((mode) => [mode, 'production-gce-c34']), + ['inspect', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33,production-gce-c34'] ] for (const [mode, cellIds] of accepted) { assert.deepEqual( @@ -586,7 +588,12 @@ test('accepts only reviewed Asia admission waves', () => { ['promote', 'production-gce-c27,production-gce-c30'], ['promote', 'production-gce-c28,production-gce-c29,production-gce-c30'], ['promote', 'production-gce-c30,production-gce-c31'], + // The C34 spare stays migration-only: no reviewed promotion wave names it. ['promote', 'production-gce-c34'], + ['recover-promotion', 'production-gce-c34'], + ['register', 'production-gce-c33,production-gce-c34'], + ['inspect', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33'], + ['rollback', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30,production-gce-c31,production-gce-c32,production-gce-c33'], ['rollback', 'production-gce-c27,production-gce-c30'], ['rollback', 'production-gce-c30,production-gce-c31'], ['rollback', 'production-gce-c27,production-gce-c28,production-gce-c29,production-gce-c30'], @@ -648,6 +655,29 @@ test('registers C31 alone beside the general C27-C30', async () => { assert.deepEqual(subject.selector().membership.general, general) }) +test('registers the C34 spare alone as migration-only in asia-east2', async () => { + const general = [...launchCells, 'production-gce-c30', 'production-gce-c31'] + const subject = harness({ + generation: 17, + membership: { + existingOnly: [], + migrationOnly: [], + general: [...general, 'production-gce-c32', 'production-gce-c33'] + } + }) + const result = await operateRelayAsiaAdmission({ + environment: 'production', mode: 'register', cells: ['production-gce-c34'], + expectedGeneration: 17, imageDigest: digest, attemptId: 'asia_register_c34', token: 'not-logged' + }, subject) + const request = subject.requests.find(({ path }) => path.endsWith('/add-migration-cells')) + assert.deepEqual(request.body.cells, [{ + cellId: 'production-gce-c34', cellUrl: 'https://c34.relay.onorca.dev', region: 'asia-east2', + capacityRequests: 6_000, connectionHardCap: 3_000, connectionUnobservedBound: 60 + }]) + assert.deepEqual(result.states, { 'production-gce-c34': 'migration-only' }) + assert.equal(subject.selector().membership.general.includes('production-gce-c34'), false) +}) + const usRegions = { 'production-gce-c32': 'us-central1', 'production-gce-c33': 'us-central1' } test('registers C32 and C33 one at a time in us-central1 at the Asia shape', async () => { diff --git a/cloud/dev/scripts/prepare-relay-asia-topology-input.mjs b/cloud/dev/scripts/prepare-relay-asia-topology-input.mjs index 5d00dbacf1a..6bbb65c50ce 100644 --- a/cloud/dev/scripts/prepare-relay-asia-topology-input.mjs +++ b/cloud/dev/scripts/prepare-relay-asia-topology-input.mjs @@ -20,7 +20,8 @@ const SHAPES = { 'production-gce-c30': 'asia-east2-a', 'production-gce-c31': 'asia-east2-b', 'production-gce-c32': 'us-central1-a', - 'production-gce-c33': 'us-central1-b' + 'production-gce-c33': 'us-central1-b', + 'production-gce-c34': 'asia-east2-c' }, // The launch set, then each later additive cell; a plan targets one wave, never live cells. waves: [ @@ -28,7 +29,8 @@ const SHAPES = { ['production-gce-c30'], ['production-gce-c31'], // Declared together, so they plan together: a lone C32 plan would hit C33's missing template. - ['production-gce-c32', 'production-gce-c33'] + ['production-gce-c32', 'production-gce-c33'], + ['production-gce-c34'] ] } } diff --git a/cloud/dev/scripts/prepare-relay-asia-topology-input.test.mjs b/cloud/dev/scripts/prepare-relay-asia-topology-input.test.mjs index 0790560a318..906c8bb1ace 100644 --- a/cloud/dev/scripts/prepare-relay-asia-topology-input.test.mjs +++ b/cloud/dev/scripts/prepare-relay-asia-topology-input.test.mjs @@ -14,7 +14,8 @@ const productionCells = () => Object.fromEntries([ [30, 'asia-east2-a'], [31, 'asia-east2-b'], [32, 'us-central1-a'], - [33, 'us-central1-b'] + [33, 'us-central1-b'], + [34, 'asia-east2-c'] ].map(([ordinal, zone]) => [`production-gce-c${ordinal}`, { hostname: `c${ordinal}`, region: zone.slice(0, -2), zone, machine_type: 'e2-standard-4', boot_disk_gb: 30, @@ -57,6 +58,14 @@ test('accepts the additive C31 wave in the next zone of the rotation', () => { assert.equal(result.relay_gce_cells['production-gce-c31'].zone, 'asia-east2-b') }) +test('accepts the additive C34 spare wave in asia-east2-c at the Asia pool', () => { + const result = prepareRelayAsiaTopologyInput({ existingCells: productionCells(), + existingAdditionalRegions: additionalRegions, environment: 'production', + cellIds: 'production-gce-c34', image }) + assert.equal(result.relay_gce_cells['production-gce-c34'].zone, 'asia-east2-c') + assert.equal(result.relay_gce_cells['production-gce-c34'].database_pool_max, 16) +}) + test('accepts the additive US C32+C33 wave at the default pool only', () => { const cellIds = 'production-gce-c32,production-gce-c33' for (const [cellId, zone] of [ @@ -103,7 +112,8 @@ test('matches every committed production Asia cell entry', () => { 'production-gce-c27,production-gce-c28,production-gce-c29', 'production-gce-c30', 'production-gce-c31', - 'production-gce-c32,production-gce-c33' + 'production-gce-c32,production-gce-c33', + 'production-gce-c34' ]) { const committedImage = committed[wave.split(',')[0]].image assert.doesNotThrow(() => prepareRelayAsiaTopologyInput({ @@ -116,8 +126,8 @@ test('matches every committed production Asia cell entry', () => { environment: 'production', cellIds: 'production-gce-c30', image }), /differs from the reviewed topology/) - // The US cells launch on the newest digest, the one C31 launched on. - for (const cellId of ['production-gce-c32', 'production-gce-c33']) { + // The US cells and the C34 spare launch on the newest cell digest, the one C31 launched on. + for (const cellId of ['production-gce-c32', 'production-gce-c33', 'production-gce-c34']) { assert.equal(committed[cellId].image, committed['production-gce-c31'].image, cellId) } }) @@ -152,7 +162,8 @@ test('rejects an uncommitted subnet or cell, partial wave, wrong image, and drif 'production-gce-c30,production-gce-c31', 'production-gce-c32', 'production-gce-c33', - 'production-gce-c34' + 'production-gce-c33,production-gce-c34', + 'production-gce-c35' ]) { assert.throws(() => prepareRelayAsiaTopologyInput({ existingCells: productionCells(), existingAdditionalRegions: additionalRegions, diff --git a/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs b/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs index b53dbe40d26..d643f2e2178 100644 --- a/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs +++ b/cloud/dev/scripts/prepare-relay-production-capacity-canary.test.mjs @@ -101,7 +101,9 @@ describe('production Relay capacity cell admission', () => { // Migration-only canaries: the US-only capacity rollout never touches them either. 'production-gce-c17', 'production-gce-c18', // US cells at the Asia shape: the 1,000-cap capacity rollout never touches them. - 'production-gce-c32', 'production-gce-c33' + 'production-gce-c32', 'production-gce-c33', + // The migration-only Asia spare. + 'production-gce-c34' ]) { const hostname = cellId.slice('production-gce-'.length) assert.deepEqual(parseProductionCapacityCellArguments([ @@ -118,7 +120,7 @@ describe('production Relay capacity cell admission', () => { paceWindowMs: 0 }) } - for (const cellId of ['production-gce-c12', 'production-gce-c34']) { + for (const cellId of ['production-gce-c12', 'production-gce-c35']) { const hostname = cellId.slice('production-gce-'.length) assert.throws(() => parseProductionCapacityCellArguments([ '--director-origin', 'https://relay.onorca.dev', diff --git a/cloud/dev/scripts/probe-relay-rehome-trust.mjs b/cloud/dev/scripts/probe-relay-rehome-trust.mjs index 32ac89e4e6d..79dddc7e921 100644 --- a/cloud/dev/scripts/probe-relay-rehome-trust.mjs +++ b/cloud/dev/scripts/probe-relay-rehome-trust.mjs @@ -2,9 +2,9 @@ import { pathToFileURL } from 'node:url' import { fetchAdminOnceMore } from './relay-admin-transient-retry.mjs' // Every cell that carries the rehome identity: the eighteen US cells and the -// five asia-east2 cells that drain mis-homed hosts back the other way. +// six asia-east2 cells that drain mis-homed hosts back the other way. const PRODUCTION_CELL = - /^production-gce-c(?:7|8|9|10|13|14|15|16|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33)$/ + /^production-gce-c(?:7|8|9|10|13|14|15|16|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34)$/ const DIRECTOR_ORIGIN = 'https://relay.onorca.dev' export function parseRehomeTrustProbeArguments(argv, environment = process.env) { diff --git a/cloud/dev/scripts/probe-relay-rehome-trust.test.mjs b/cloud/dev/scripts/probe-relay-rehome-trust.test.mjs index 932fa450112..c7d8b5e5d4b 100644 --- a/cloud/dev/scripts/probe-relay-rehome-trust.test.mjs +++ b/cloud/dev/scripts/probe-relay-rehome-trust.test.mjs @@ -116,7 +116,7 @@ test('fails when both trust-probe attempts return a transient 503', async () => test('approves the asia-east2 and US 3,000 rehome sources and still rejects unlisted cells', () => { for (const cellId of [ 'production-gce-c27', 'production-gce-c28', 'production-gce-c29', 'production-gce-c30', - 'production-gce-c31', 'production-gce-c32', 'production-gce-c33' + 'production-gce-c31', 'production-gce-c32', 'production-gce-c33', 'production-gce-c34' ]) { const parsed = parseRehomeTrustProbeArguments( argv.map((value) => (value === 'production-gce-c7' ? cellId : value)), @@ -124,7 +124,7 @@ test('approves the asia-east2 and US 3,000 rehome sources and still rejects unli ) assert.equal(parsed.cellId, cellId) } - for (const cellId of ['production-gce-c1', 'production-gce-c17', 'production-gce-c34']) { + for (const cellId of ['production-gce-c1', 'production-gce-c17', 'production-gce-c35']) { assert.throws( () => parseRehomeTrustProbeArguments( @@ -183,7 +183,7 @@ test('approves exactly the committed production rehome source cells', () => { const sources = new Set( [...tfvars.slice(start, tfvars.indexOf(']', start)).matchAll(/"([^"]+)"/g)].map(([, cell]) => cell) ) - assert.ok(sources.has('production-gce-c33')) + assert.ok(sources.has('production-gce-c34')) for (let ordinal = 1; ordinal <= 40; ordinal++) { const cellId = `production-gce-c${ordinal}` const approved = (() => { diff --git a/cloud/dev/scripts/relay-asia-topology-workflow.test.mjs b/cloud/dev/scripts/relay-asia-topology-workflow.test.mjs index 0a59d8d9939..8a6eba98b69 100644 --- a/cloud/dev/scripts/relay-asia-topology-workflow.test.mjs +++ b/cloud/dev/scripts/relay-asia-topology-workflow.test.mjs @@ -50,7 +50,8 @@ test('accepts only the reviewed Asia topology waves', () => { 'production:production-gce-c27,production-gce-c28,production-gce-c29', 'production:production-gce-c30', 'production:production-gce-c31', - 'production:production-gce-c32,production-gce-c33' + 'production:production-gce-c32,production-gce-c33', + 'production:production-gce-c34' ] ) }) diff --git a/cloud/dev/scripts/relay-cloud-sql-connection-budget.test.mjs b/cloud/dev/scripts/relay-cloud-sql-connection-budget.test.mjs index bade8de8f52..bc68bf49c69 100644 --- a/cloud/dev/scripts/relay-cloud-sql-connection-budget.test.mjs +++ b/cloud/dev/scripts/relay-cloud-sql-connection-budget.test.mjs @@ -7,12 +7,12 @@ import { } from './relay-cloud-sql-connection-budget.mjs' test('production shared consumers keep allowance and reserve below the ceiling', () => { - // cells: 22 pools at 10 (220, C32/C33 included) + the five asia-east2 pools at 16 (80). + // cells: 22 pools at 10 (220, C32/C33 included) + the six asia-east2 pools at 16 (96). const report = readRelayCloudSqlConnectionBudget() - assert.deepEqual(report.consumers, { cells: 300, directors: 15, auth: 20, api: 50 }) - assert.deepEqual(report.asia, { cells: 5, poolMax: 16 }) - assert.equal(report.configuredMaximum, 385) + assert.deepEqual(report.consumers, { cells: 316, directors: 15, auth: 20, api: 50 }) + assert.deepEqual(report.asia, { cells: 6, poolMax: 16 }) + assert.equal(report.configuredMaximum, 401) assert.equal(report.rolloutOverlap.relayDirectorCandidate, 30) assert.equal(report.rolloutOverlap.apiCandidate, 65) assert.equal(report.rolloutOverlap.authCandidate, 35) @@ -22,10 +22,10 @@ test('production shared consumers keep allowance and reserve below the ceiling', assert.equal(report.maintenanceAdminAllowance, 5) assert.equal(report.explicitReserve, 10) assert.equal(report.usableCeiling, 490) - assert.equal(report.operatingMaximum, 455) - assert.equal(report.remainingWithinUsableCeiling, 35) - assert.equal(report.budgetedTotal, 465) - assert.equal(report.unallocated, 35) + assert.equal(report.operatingMaximum, 471) + assert.equal(report.remainingWithinUsableCeiling, 19) + assert.equal(report.budgetedTotal, 481) + assert.equal(report.unallocated, 19) assert.equal(report.withinBudget, true) }) diff --git a/cloud/dev/scripts/relay-production-same-cap-wave.mjs b/cloud/dev/scripts/relay-production-same-cap-wave.mjs index 0906b7eed28..7313ce627d6 100644 --- a/cloud/dev/scripts/relay-production-same-cap-wave.mjs +++ b/cloud/dev/scripts/relay-production-same-cap-wave.mjs @@ -4,7 +4,10 @@ import { requireSameEvidenceCode } from './relay-evidence-code-provenance.mjs' // Migration-only by policy: zero hosts and no reservation, so a wave rolls one without // displacing anybody. It enters and must leave migration-only, never general. -export const SAME_CAP_MIGRATION_ONLY_CELLS = ['production-gce-c17', 'production-gce-c18'] +// C34 is an Asia spare that stays migration-only by policy. +export const SAME_CAP_MIGRATION_ONLY_CELLS = [ + 'production-gce-c17', 'production-gce-c18', 'production-gce-c34' +] export const SAME_CAP_CELLS = [ 'production-gce-c7', 'production-gce-c8', 'production-gce-c9', 'production-gce-c10', diff --git a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs index 5595a271d0b..c8d5ccb4f2b 100644 --- a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs +++ b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs @@ -63,7 +63,7 @@ test('requires one canary or a bounded reviewed batch', () => { rollbackDigest, confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} production-gce-c31` }).cells, ['production-gce-c31']) - for (const cellId of ['production-gce-c32', 'production-gce-c33']) { + for (const cellId of ['production-gce-c32', 'production-gce-c33', 'production-gce-c34']) { assert.deepEqual(validateSameCapWave({ mode: 'canary-apply', cellIds: cellId, @@ -74,10 +74,10 @@ test('requires one canary or a bounded reviewed batch', () => { } assert.throws(() => validateSameCapWave({ mode: 'canary-apply', - cellIds: 'production-gce-c34', + cellIds: 'production-gce-c35', targetDigest, rollbackDigest, - confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} production-gce-c34` + confirmation: `ROLL_RELAY_SAME_CAP ${targetDigest} production-gce-c35` }), /cells/) }) @@ -123,8 +123,11 @@ test('the wave workflow chains exactly ten serial cell jobs', () => { assert.doesNotMatch(dispatch, /\n cell_11:/) }) -test('lists only C17 and C18 as migration-only now that C32 and C33 are promoted', () => { - assert.deepEqual(SAME_CAP_MIGRATION_ONLY_CELLS, ['production-gce-c17', 'production-gce-c18']) +test('lists the C34 spare as migration-only beside C17 and C18, and C30-C33 as general', () => { + assert.deepEqual( + SAME_CAP_MIGRATION_ONLY_CELLS, + ['production-gce-c17', 'production-gce-c18', 'production-gce-c34'] + ) for (const cellId of [ 'production-gce-c30', 'production-gce-c31', 'production-gce-c32', 'production-gce-c33' ]) { diff --git a/cloud/dev/scripts/relay-same-cap-script-census.test.mjs b/cloud/dev/scripts/relay-same-cap-script-census.test.mjs index afcc1a84e2f..7f27742ce3f 100644 --- a/cloud/dev/scripts/relay-same-cap-script-census.test.mjs +++ b/cloud/dev/scripts/relay-same-cap-script-census.test.mjs @@ -263,7 +263,7 @@ describe('same-cap roll scripts accept every same-cap cell', () => { assert.equal(String(cellShape(cellId).cap), tfvarsHardCap(cellId), cellId) } assert.equal(resolveCellShape('production-gce-c12').status, 1) - assert.equal(resolveCellShape('production-gce-c34').status, 1) + assert.equal(resolveCellShape('production-gce-c35').status, 1) }) @@ -275,9 +275,10 @@ describe('same-cap roll scripts accept every same-cap cell', () => { const trusted = SAME_CAP_CELLS.filter((cell) => REHOME_SOURCE_CELLS.has(cell)) // Only a declared rehome source may roll at a trusted protocol at all; the job refuses // the rest before it plans, and the next test covers them at protocol 0. + // The C34 spare is a rehome source that stays migration-only. assert.deepEqual( SAME_CAP_CELLS.filter((cell) => !REHOME_SOURCE_CELLS.has(cell)), - SAME_CAP_MIGRATION_ONLY_CELLS + SAME_CAP_MIGRATION_ONLY_CELLS.filter((cell) => cell !== 'production-gce-c34') ) for (const [cellId, protocol] of trusted.flatMap((cell) => [[cell, 1], [cell, 3]])) { const { cap, pool } = cellShape(cellId) diff --git a/cloud/dev/scripts/validate-relay-asia-topology-plan.mjs b/cloud/dev/scripts/validate-relay-asia-topology-plan.mjs index 084960d396b..adcb7807fba 100644 --- a/cloud/dev/scripts/validate-relay-asia-topology-plan.mjs +++ b/cloud/dev/scripts/validate-relay-asia-topology-plan.mjs @@ -24,14 +24,16 @@ const CELL_SHAPES = { 'production-gce-c30': 'asia-east2-a', 'production-gce-c31': 'asia-east2-b', 'production-gce-c32': 'us-central1-a', - 'production-gce-c33': 'us-central1-b' + 'production-gce-c33': 'us-central1-b', + 'production-gce-c34': 'asia-east2-c' }, waves: [ ['production-gce-c27', 'production-gce-c28', 'production-gce-c29'], ['production-gce-c30'], ['production-gce-c31'], // Declared together, so they plan together: a lone C32 plan would hit C33's missing template. - ['production-gce-c32', 'production-gce-c33'] + ['production-gce-c32', 'production-gce-c33'], + ['production-gce-c34'] ] }, staging: { diff --git a/cloud/dev/scripts/validate-relay-asia-topology-plan.test.mjs b/cloud/dev/scripts/validate-relay-asia-topology-plan.test.mjs index 0e9f6e0a6a2..a4838aba046 100644 --- a/cloud/dev/scripts/validate-relay-asia-topology-plan.test.mjs +++ b/cloud/dev/scripts/validate-relay-asia-topology-plan.test.mjs @@ -161,6 +161,18 @@ test('accepts the additive production C31 wave only in asia-east2-b', () => { ) }) +test('accepts the additive production C34 spare wave only in asia-east2-c', () => { + const c34Config = { ...productionConfig, cells: ['production-gce-c34'] } + assert.deepEqual( + validateRelayAsiaTopologyPlan({ resource_changes: productionWavePlan('c34', 'asia-east2-c') }, c34Config), + { environment: 'production', cells: ['production-gce-c34'], changes: 4 } + ) + assert.throws( + () => validateRelayAsiaTopologyPlan({ resource_changes: productionWavePlan('c34') }, c34Config), + /fixed-one Asia MIG shape/ + ) +}) + // A US cell joins the root region: no additional-region network, no region label or line, and // the default pool emits no line, exactly as the startup template renders a root-region cell. function usCellPlan(hostname, zone) { @@ -269,7 +281,8 @@ test('accepts only a reviewed Asia topology wave', () => { 'production-gce-c27,production-gce-c28,production-gce-c29', 'production-gce-c29,production-gce-c27,production-gce-c28', 'production-gce-c30', - 'production-gce-c31' + 'production-gce-c31', + 'production-gce-c34' ]) { assert.doesNotThrow( () => parseRelayAsiaTopologyPlanArguments(argv('production', cellIds, productionImage)), @@ -286,7 +299,8 @@ test('accepts only a reviewed Asia topology wave', () => { 'production-gce-c32', 'production-gce-c33', 'production-gce-c32,production-gce-c33,production-gce-c34', - 'production-gce-c34' + 'production-gce-c33,production-gce-c34', + 'production-gce-c35' ]) { assert.throws( () => parseRelayAsiaTopologyPlanArguments(argv('production', cellIds, productionImage)), diff --git a/cloud/docs/relay-workflows.md b/cloud/docs/relay-workflows.md index 27b8bc7697b..0d2e501f1f1 100644 --- a/cloud/docs/relay-workflows.md +++ b/cloud/docs/relay-workflows.md @@ -218,6 +218,16 @@ to the general list. The shadow gate's fleet pool list tracks the 16-connection whether a US cell belongs there is decided at promotion, not assumed. Both were promoted to general on 2026-10-01, so the same-cap job now rolls them as general cells. They stay out of the fleet pool list because their pool is the US default of 10. + +C34 is an Asia spare at the C31 shape in `asia-east2-c`, so the six Asia cells spread 2/2/2. It is +its own topology wave and registers alone as migration-only, then the director is configured with +`cell-ids` set to C34. It has no promotion wave: the Asia admission script and workflow refuse +`promote` for it, and placement and regional rehome select only general cells. It is a +migration-only landing zone that only an explicit evacuation or migration naming it can target. +Do not name it in the multi-target `promote-general-cell` or `retire-migration-cell` modes, which +accept any migration-only cell. It is a declared rehome source, sits in the same-cap migration-only +list, and stays out of the fleet pool list. Promoting it later takes its own reviewed change adding a +promotion wave and canary entry. Rollback returns Asia cells to migration-only; it does not destroy the network or use existing-only. The production topology dispatch remains unavailable until the diff --git a/cloud/infra/terraform/README.md b/cloud/infra/terraform/README.md index cf447a88dd2..70001984c7b 100644 --- a/cloud/infra/terraform/README.md +++ b/cloud/infra/terraform/README.md @@ -332,7 +332,7 @@ cell templates/MIGs/backends, and exact shared URL-map host additions. It rejects deletes, replacements, loss of an existing host route, US-resource changes, and unrelated drift. Do not add production C27-C29 until the compatible image has been published and each entry can pin its immutable -digest. A later cell, such as C30 or C31, is its own reviewed wave. The shared URL map +digest. A later cell, such as C30, C31 or the C34 spare, is its own reviewed wave. The shared URL map pulls every live cell into its plan, so the workflow plans each live cell at the image its state template already serves, and the validator rejects any change to a cell outside the wave. US C32 and C33 use the same workflow at the same diff --git a/cloud/infra/terraform/environments/production.tfvars b/cloud/infra/terraform/environments/production.tfvars index baed00d496d..a3521914b20 100644 --- a/cloud/infra/terraform/environments/production.tfvars +++ b/cloud/infra/terraform/environments/production.tfvars @@ -450,6 +450,21 @@ relay_gce_cells = { connection_hard_cap = 3000 connection_unobserved_bound = 60 } + # Asia spare: registered migration-only as a drain landing zone; c completes the 2/2/2 zone spread. + "production-gce-c34" = { + hostname = "c34" + region = "asia-east2" + zone = "asia-east2-c" + machine_type = "e2-standard-4" + boot_disk_gb = 30 + boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-21" + capacity_requests = 6000 + database_pool_max = 16 # 176 ms from us-central1 Postgres saturates 10 (94-156 waiters). + image = "us-central1-docker.pkg.dev/onorca-cloud/orca-cloud/relay@sha256:f30b5cb1ec52b6b6145efecfa1b8be9e3d309403beffd8abcc64197a2087e269" + initially_enabled = false + connection_hard_cap = 3000 + connection_unobserved_bound = 60 + } } relay_region_rehome_source_cell_ids = [ @@ -476,7 +491,8 @@ relay_region_rehome_source_cell_ids = [ "production-gce-c30", "production-gce-c31", "production-gce-c32", - "production-gce-c33" + "production-gce-c33", + "production-gce-c34" ] # Slack #orca-relay-alerts, created out of band on 2026-08-05. Declared here because an apply