mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
refactor(push): remove redundant cloud storage and rollout coupling
This commit is contained in:
@@ -283,8 +283,6 @@ export const LEASED_WORKFLOWS = named([
|
||||
'operate-relay-production-rehome.yml',
|
||||
production({ leaseFiles: ['operate-relay-production-rehome-job.yml'] })
|
||||
],
|
||||
// The gateway applies its schema at startup, so its deploy revision is the schema step.
|
||||
['push-deploy.yml', production()],
|
||||
['deploy-relay-asia-topology.yml', eitherEnvironment()],
|
||||
['operate-relay-asia-admission.yml', eitherEnvironment()],
|
||||
['deploy-relay-staging.yml', staging()],
|
||||
@@ -300,6 +298,10 @@ export const LEASED_WORKFLOWS = named([
|
||||
])
|
||||
|
||||
export const NOT_A_CLOUD_SQL_CANDIDATE = named([
|
||||
[
|
||||
'push-deploy.yml',
|
||||
'Push attaches only to its dedicated database and serializes its own traffic changes under production-push-rollout and the durable push-rollout lease; push-gateway-workflow tests verify both.'
|
||||
],
|
||||
[
|
||||
'monitor-relay-production.yml',
|
||||
'Read-only. Its identity holds monitoring, logging, Cloud SQL and compute viewer roles only, and it runs `gcloud sql instances describe`, never a mutation. It consumes no connection budget, so the durable lease would only let monitoring block a rollout and a rollout block monitoring.'
|
||||
|
||||
@@ -12,7 +12,7 @@ import { readRelayWorkflow, relayWorkflowFile } from './relay-repository.mjs'
|
||||
|
||||
// Why: the push gateway holds the APNs key and is the only thing standing between a paired
|
||||
// phone and a silent notification pipeline. Its deploy is a blue/green rollout against the
|
||||
// shared Cloud SQL instance, and each of the guarantees below is one careless edit from gone.
|
||||
// dedicated Cloud SQL instance, and each of the guarantees below is one careless edit from gone.
|
||||
const WORKFLOW = 'push-deploy.yml'
|
||||
const workflow = readRelayWorkflow(WORKFLOW)
|
||||
const deploy = () => {
|
||||
@@ -60,15 +60,15 @@ test('Terraform trusts this exact workflow file on the production deploy provide
|
||||
assert.equal(relayWorkflowFile(WORKFLOW), 'cloud-push-deploy.yml')
|
||||
})
|
||||
|
||||
test('the rollout is serialized and leases the production Cloud SQL rollout lock', () => {
|
||||
test('the rollout is serialized and leases its dedicated push rollout lock', () => {
|
||||
const blocks = concurrencyBlocks(workflow)
|
||||
assert.equal(blocks.length, 1)
|
||||
assert.equal(blocks[0].group, 'production-cloud-sql-rollout')
|
||||
assert.equal(blocks[0].group, 'production-push-rollout')
|
||||
assert.equal(blocks[0].cancelInProgress, 'false')
|
||||
const steps = leaseSteps(workflow)
|
||||
assert.equal(steps.length, 1, 'exactly one lease step, held for the whole run')
|
||||
assert.equal(steps[0].bucket, 'onorca-cloud-terraform-state')
|
||||
assert.equal(steps[0].object, 'terraform/state/cloud-sql-rollout/production.lock')
|
||||
assert.equal(steps[0].object, 'terraform/state/push-rollout/production.lock')
|
||||
assert.equal(steps[0].release, undefined, 'release stays at its default for a single-job run')
|
||||
})
|
||||
|
||||
@@ -138,7 +138,7 @@ test('the database pool size is Terraform-owned and bounded at plan time', () =>
|
||||
assert.ok(block, 'the push service no longer declares a lifecycle block')
|
||||
assert.match(
|
||||
block[1],
|
||||
/var\.push_max_instances \* var\.push_database_pool_max <= 4/,
|
||||
/var\.push_max_instances \* var\.push_database_pool_max \* 3 <= 64/,
|
||||
'instances x pool must be bounded at plan time'
|
||||
)
|
||||
assert.match(
|
||||
@@ -313,3 +313,22 @@ test('push credentials cannot assume the shared Relay deploy identity', () => {
|
||||
test('dedicated database admits three simultaneous revision pools', () => {
|
||||
assert.match(terraform('push-gateway.tf'), /var\.push_max_instances \* var\.push_database_pool_max \* 3 <= 64/)
|
||||
})
|
||||
|
||||
test('push has only a dedicated database attachment and a narrowly scoped deployment lease', () => {
|
||||
const service = terraform('push-gateway.tf')
|
||||
const database = terraform('push-dedicated-database.tf')
|
||||
assert.match(service, /instances = \[google_sql_database_instance\.push_dedicated\[0\]\.connection_name\]/)
|
||||
assert.match(service, /secret\s*= google_secret_manager_secret\.push_dedicated_database_url\[0\]\.secret_id/)
|
||||
assert.match(service, /version = google_secret_manager_secret_version\.push_dedicated_database_url\[0\]\.version/)
|
||||
assert.doesNotMatch(service + database, /push_dedicated_database_(?:active|enabled)|local\.relay_database_connection_name|resource "google_sql_database" "push"/)
|
||||
assert.match(database, /tier\s*= "db-custom-2-7680"/)
|
||||
assert.match(database, /availability_type = "REGIONAL"/)
|
||||
assert.match(database, /deletion_protection\s*= true/)
|
||||
assert.match(database, /deletion_protection_enabled = true/)
|
||||
const identity = terraform('push-deploy-identity.tf')
|
||||
const lease = identity.match(/resource "google_storage_bucket_iam_member" "github_push_rollout_lease" \{([\s\S]*?)\n\}/)?.[1]
|
||||
assert.ok(lease)
|
||||
assert.match(lease, /member = local\.push_deploy_member/)
|
||||
assert.match(lease, /role\s*= "roles\/storage.objectAdmin"/)
|
||||
assert.match(lease, /resource.name == 'projects\/_\/buckets\/\$\{var.project_id\}-terraform-state\/objects\/terraform\/state\/push-rollout\/production.lock'/)
|
||||
})
|
||||
|
||||
@@ -33,13 +33,6 @@ function requiredInteger(source, pattern, label) {
|
||||
return value
|
||||
}
|
||||
|
||||
// A tfvars file states only what it overrides, so an absent key means the variable default holds.
|
||||
// Reading the default as the fallback keeps this honest either way.
|
||||
function overriddenInteger(override, overridePattern, source, pattern, label) {
|
||||
if (!overridePattern.test(override)) return requiredInteger(source, pattern, label)
|
||||
return requiredInteger(override, overridePattern, label)
|
||||
}
|
||||
|
||||
function productionCells(source, defaultPoolMax) {
|
||||
const fencedMatch = source.match(/relay_gce_fenced_cells\s*=\s*\[([^\]]*)\]/)
|
||||
if (!fencedMatch) throw new Error('could not read fenced Relay cells')
|
||||
@@ -59,13 +52,11 @@ function productionCells(source, defaultPoolMax) {
|
||||
}
|
||||
|
||||
export function calculateRelayCloudSqlConnectionBudget(inputs) {
|
||||
const pushDraw = inputs.pushInstances * inputs.pushPoolMax
|
||||
const consumers = {
|
||||
cells: inputs.cellPoolTotal + inputs.asiaCellCount * inputs.asiaPoolMax,
|
||||
directors: inputs.directorInstances * inputs.directorPoolMax,
|
||||
auth: inputs.authInstances * inputs.authPoolMax,
|
||||
api: inputs.apiInstances * inputs.apiPoolMax,
|
||||
push: pushDraw
|
||||
api: inputs.apiInstances * inputs.apiPoolMax
|
||||
}
|
||||
const configuredMaximum = Object.values(consumers).reduce((total, value) => total + value, 0)
|
||||
const retainedDirectorRollback = inputs.directorInstances * inputs.directorPoolMax
|
||||
@@ -73,8 +64,6 @@ export function calculateRelayCloudSqlConnectionBudget(inputs) {
|
||||
relayDirectorCandidate: retainedDirectorRollback * 2,
|
||||
apiCandidate: retainedDirectorRollback + inputs.apiInstances * inputs.apiPoolMax,
|
||||
authCandidate: retainedDirectorRollback + inputs.authInstances * inputs.authPoolMax,
|
||||
// Serving is already counted; validation/rejected and its successor add two pools.
|
||||
pushCandidate: retainedDirectorRollback + pushDraw * 2,
|
||||
relayCells: retainedDirectorRollback
|
||||
}
|
||||
const rolloutOverlap = Math.max(...Object.values(candidateOverlap))
|
||||
@@ -142,20 +131,6 @@ export function readRelayCloudSqlConnectionBudget({
|
||||
/variable\s+"relay_director_database_pool_max"[\s\S]*?default\s*=\s*(\d+)/,
|
||||
'director pool maximum'
|
||||
),
|
||||
// The mobile push gateway shares this instance. Its draw was invisible here until Terraform
|
||||
// declared the pool: docs/push-gateway.md, "Shape".
|
||||
pushInstances: overriddenInteger(
|
||||
productionTfvars,
|
||||
/^\s*push_max_instances\s*=\s*(\d+)/m,
|
||||
terraformVariables,
|
||||
/variable\s+"push_max_instances"[\s\S]*?default\s*=\s*(\d+)/,
|
||||
'push gateway instances'
|
||||
),
|
||||
pushPoolMax: requiredInteger(
|
||||
terraformVariables,
|
||||
/variable\s+"push_database_pool_max"[\s\S]*?default\s*=\s*(\d+)/,
|
||||
'push gateway pool maximum'
|
||||
),
|
||||
authInstances: apps.authInstances,
|
||||
authPoolMax: apps.authPoolMax,
|
||||
apiInstances: apps.apiInstances,
|
||||
|
||||
@@ -6,91 +6,28 @@ import {
|
||||
readRelayCloudSqlConnectionBudget
|
||||
} from './relay-cloud-sql-connection-budget.mjs'
|
||||
|
||||
// Why these numbers are this tight: the shared instance's 400 connections were already spoken
|
||||
// for, and the relay shape below leaves exactly five. The gateway is sized to fit in four, two
|
||||
// instances times a two-connection pool, and its rollout overlap of 23 stays under the API
|
||||
// candidate's 65, so the Math.max is the API candidate rather than the gateway.
|
||||
//
|
||||
// `Deploy Relay Asia Topology` gates on `withinBudget == true`, so the single remaining
|
||||
// connection is the whole margin. Anything that raises a pool or an instance count moves it.
|
||||
test('production plus the push gateway keeps allowance and reserve below the ceiling', () => {
|
||||
test('production shared consumers keep allowance and reserve below the ceiling', () => {
|
||||
const report = readRelayCloudSqlConnectionBudget()
|
||||
|
||||
assert.deepEqual(report.consumers, { cells: 230, directors: 15, auth: 20, api: 50, push: 4 })
|
||||
assert.deepEqual(report.consumers, { cells: 230, directors: 15, auth: 20, api: 50 })
|
||||
assert.deepEqual(report.asia, { cells: 3, poolMax: 10 })
|
||||
assert.equal(report.configuredMaximum, 319)
|
||||
assert.equal(report.configuredMaximum, 315)
|
||||
assert.equal(report.rolloutOverlap.relayDirectorCandidate, 30)
|
||||
assert.equal(report.rolloutOverlap.apiCandidate, 65)
|
||||
assert.equal(report.rolloutOverlap.authCandidate, 35)
|
||||
assert.equal(report.rolloutOverlap.pushCandidate, 23)
|
||||
assert.equal(report.rolloutOverlap.relayCells, 15)
|
||||
assert.equal(report.rolloutOverlap.retainedDirectorRollback, 15)
|
||||
// The gateway does not set the maximum; the API candidate does, as it did before it existed.
|
||||
assert.equal(report.rolloutOverlap.maximum, 65)
|
||||
assert.equal(report.maintenanceAdminAllowance, 5)
|
||||
assert.equal(report.explicitReserve, 10)
|
||||
assert.equal(report.usableCeiling, 390)
|
||||
assert.equal(report.operatingMaximum, 389)
|
||||
assert.equal(report.remainingWithinUsableCeiling, 1)
|
||||
assert.equal(report.budgetedTotal, 399)
|
||||
assert.equal(report.unallocated, 1)
|
||||
assert.equal(report.withinBudget, true)
|
||||
})
|
||||
|
||||
// Why: the same relay shape without a push gateway is the before picture, and it stood at five
|
||||
// connections clear. Holding it here keeps the gateway's cost visible as the four it takes,
|
||||
// rather than letting drift elsewhere in the budget hide inside the same margin.
|
||||
test('the same relay shape without the gateway stays inside the ceiling', () => {
|
||||
const report = calculateRelayCloudSqlConnectionBudget({
|
||||
cellPoolTotal: 200,
|
||||
asiaCellCount: 3,
|
||||
asiaPoolMax: 10,
|
||||
directorInstances: 5,
|
||||
directorPoolMax: 3,
|
||||
authInstances: 2,
|
||||
authPoolMax: 10,
|
||||
apiInstances: 10,
|
||||
apiPoolMax: 5,
|
||||
pushInstances: 0,
|
||||
pushPoolMax: 0,
|
||||
maxConnections: 400,
|
||||
maintenanceAdminAllowance: 5,
|
||||
explicitReserve: 10
|
||||
})
|
||||
|
||||
assert.equal(report.consumers.push, 0)
|
||||
assert.equal(report.rolloutOverlap.maximum, 65)
|
||||
assert.equal(report.operatingMaximum, 385)
|
||||
assert.equal(report.remainingWithinUsableCeiling, 5)
|
||||
assert.equal(report.budgetedTotal, 395)
|
||||
assert.equal(report.unallocated, 5)
|
||||
assert.equal(report.withinBudget, true)
|
||||
})
|
||||
|
||||
// Why: a tagged candidate is directly addressable and sits outside the service-wide cap, so all three
|
||||
// push revisions can reach the ceiling at once. The API and auth candidates add one copy; this
|
||||
// one adds two, like the director candidate.
|
||||
test('the push rollout scenario triples the gateway draw over the retained director', () => {
|
||||
const report = calculateRelayCloudSqlConnectionBudget({
|
||||
cellPoolTotal: 0,
|
||||
asiaCellCount: 0,
|
||||
asiaPoolMax: 0,
|
||||
directorInstances: 5,
|
||||
directorPoolMax: 3,
|
||||
authInstances: 0,
|
||||
authPoolMax: 0,
|
||||
apiInstances: 0,
|
||||
apiPoolMax: 0,
|
||||
pushInstances: 2,
|
||||
pushPoolMax: 2,
|
||||
maxConnections: 400,
|
||||
maintenanceAdminAllowance: 5,
|
||||
explicitReserve: 10
|
||||
})
|
||||
|
||||
assert.equal(report.consumers.push, 4)
|
||||
// Serving is in the base; overlap adds 15 retained director plus two 4-connection pools.
|
||||
assert.equal(report.rolloutOverlap.pushCandidate, 23)
|
||||
})
|
||||
|
||||
test('fails closed when pool growth consumes the explicit reserve', () => {
|
||||
const report = calculateRelayCloudSqlConnectionBudget({
|
||||
cellPoolTotal: 200,
|
||||
@@ -102,14 +39,12 @@ test('fails closed when pool growth consumes the explicit reserve', () => {
|
||||
authPoolMax: 10,
|
||||
apiInstances: 20,
|
||||
apiPoolMax: 5,
|
||||
pushInstances: 4,
|
||||
pushPoolMax: 10,
|
||||
maxConnections: 400,
|
||||
maintenanceAdminAllowance: 5,
|
||||
explicitReserve: 10
|
||||
})
|
||||
|
||||
assert.equal(report.operatingMaximum, 555)
|
||||
assert.equal(report.operatingMaximum, 515)
|
||||
assert.equal(report.withinBudget, false)
|
||||
})
|
||||
|
||||
@@ -141,15 +76,11 @@ test('excludes fenced cell pools and reads per-cell pool overrides', () => {
|
||||
})
|
||||
|
||||
assert.equal(report.consumers.cells, 14)
|
||||
// No push_max_instances in this tfvars, so the variable default of one instance holds.
|
||||
assert.equal(report.consumers.push, 2)
|
||||
assert.equal(report.operatingMaximum, 48)
|
||||
assert.equal(report.budgetedTotal, 49)
|
||||
assert.equal(report.operatingMaximum, 46)
|
||||
assert.equal(report.budgetedTotal, 47)
|
||||
})
|
||||
|
||||
// Why: production.tfvars overrides push_max_instances down to 2 while variables.tf still defaults
|
||||
// to 4, so reading the default instead of the override would overstate the live draw by half.
|
||||
test('a tfvars push_max_instances override wins over the variable default', () => {
|
||||
test('dedicated push scaling does not consume shared capacity', () => {
|
||||
const report = readRelayCloudSqlConnectionBudget({
|
||||
proposedAsiaCellCount: 1,
|
||||
appConsumers: { authInstances: 1, authPoolMax: 10, apiInstances: 1, apiPoolMax: 5, maxConnections: 100 },
|
||||
@@ -175,8 +106,9 @@ test('a tfvars push_max_instances override wins over the variable default', () =
|
||||
explicitReserve: 1
|
||||
})
|
||||
|
||||
assert.equal(report.consumers.push, 6)
|
||||
assert.equal(report.rolloutOverlap.pushCandidate, 15)
|
||||
assert.equal(report.consumers.push, undefined)
|
||||
assert.equal(report.rolloutOverlap.pushCandidate, undefined)
|
||||
assert.equal(report.operatingMaximum, 46)
|
||||
})
|
||||
|
||||
test('requires strict headroom below the physical ceiling', () => {
|
||||
@@ -190,14 +122,12 @@ test('requires strict headroom below the physical ceiling', () => {
|
||||
authPoolMax: 10,
|
||||
apiInstances: 1,
|
||||
apiPoolMax: 5,
|
||||
pushInstances: 1,
|
||||
pushPoolMax: 2,
|
||||
maxConnections: 50,
|
||||
maintenanceAdminAllowance: 9,
|
||||
explicitReserve: 3
|
||||
})
|
||||
|
||||
assert.equal(report.budgetedTotal, 65)
|
||||
assert.equal(report.budgetedTotal, 63)
|
||||
assert.equal(report.withinBudget, false)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user