mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Phase 6 of the relay split: the relay's deploy/operate surface moves under cloud/ with 24 cloud-* workflows gated on ORCA_CLOUD_OPERATIONS_ENABLED, the Cloud SQL rollout lease action, the relay Terraform root (dual-accept identities for both repositories), scripts, docs, CODEOWNERS, and a terraform validate job in Cloud Verify.
4472 lines
163 KiB
TypeScript
4472 lines
163 KiB
TypeScript
import { ASSIGNMENT_LIMITS } from '@orca-cloud/relay-contract'
|
|
import { createHash } from 'node:crypto'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import type { RelayCellConfig } from './config.js'
|
|
import { RelayAssignmentStore, STRANDED_MIGRATION_ABANDON_MS } from './assignment-store.js'
|
|
import {
|
|
encodeMembership,
|
|
type CellAdmissionMembership
|
|
} from './cell-admission-selector.js'
|
|
import {
|
|
openInMemoryRelayDatabase,
|
|
type RelayDatabase,
|
|
type RelayLockOptions,
|
|
type RelayTransactionOptions,
|
|
type SqlRow
|
|
} from './database.js'
|
|
|
|
const CELLS: RelayCellConfig[] = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 2 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 2 }
|
|
]
|
|
const NO_EXPIRED_EVACUATION_DIAGNOSTICS = {
|
|
expiredUnregistered: 0,
|
|
repairableExpiredUnregistered: 0,
|
|
abortableExpiredUnregistered: 0,
|
|
blockedExpiredUnregistered: 0,
|
|
blockedExpiredOnNewerTargetAssignment: 0
|
|
}
|
|
const NO_REGISTERED_EVACUATION_DIAGNOSTICS = {
|
|
registeredSourceActive: 0,
|
|
registeredCompletable: 0,
|
|
registeredTargetInactive: 0
|
|
}
|
|
const NO_ACTIVE_MIGRATION_LEASE = {
|
|
oldestExpiresAt: null,
|
|
oldestRemainingMs: null
|
|
}
|
|
const FENCE_PLAN_BINDING = {
|
|
planObjectName:
|
|
'terraform/state/relay-fence-plans/production/22222222-2222-4222-8222-222222222222.tfplan',
|
|
planObjectGeneration: '123456789',
|
|
varFileSha256: 'c'.repeat(64),
|
|
terraformStateLineage: '33333333-3333-4333-8333-333333333333',
|
|
terraformStateSerial: 7,
|
|
terraformStateObjectGeneration: '987654321',
|
|
terraformStateObjectSha256: 'd'.repeat(64),
|
|
requestReason:
|
|
'orca-relay-fence/22222222-2222-4222-8222-222222222222'
|
|
}
|
|
const FENCE_INVOCATION_ID = '55555555-5555-4555-8555-555555555555'
|
|
const FENCE_INVOCATION_REASON =
|
|
`${FENCE_PLAN_BINDING.requestReason}/${FENCE_INVOCATION_ID}`
|
|
|
|
async function applyGenerationZeroSelector(
|
|
store: RelayAssignmentStore,
|
|
input: {
|
|
attemptId: string
|
|
membership: CellAdmissionMembership
|
|
}
|
|
) {
|
|
const current = (await store.inspectCellAdmissionSelector()).selector.membership
|
|
return await store.applyCellAdmissionSelector({
|
|
...input,
|
|
expectedGeneration: 0,
|
|
expectedMembershipSha256: createHash('sha256')
|
|
.update(encodeMembership(current))
|
|
.digest('hex')
|
|
})
|
|
}
|
|
|
|
function cellFenceEvidence(attemptId = '22222222-2222-4222-8222-222222222222') {
|
|
return {
|
|
attemptId,
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
}
|
|
|
|
class OneShotInventoryFailureDatabase implements RelayDatabase {
|
|
readonly retryLocks: string[] = []
|
|
private armed = false
|
|
private failed = false
|
|
|
|
constructor(private readonly delegate: RelayDatabase) {}
|
|
|
|
arm(): void {
|
|
this.armed = true
|
|
}
|
|
|
|
async query(sql: string, params: unknown[] = []): Promise<SqlRow[]> {
|
|
return await this.delegate.query(sql, params)
|
|
}
|
|
|
|
async queryLocked(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
options: RelayLockOptions = {}
|
|
): Promise<SqlRow[]> {
|
|
return await this.lockedQuery(this.delegate, sql, params, options)
|
|
}
|
|
|
|
async transaction<T>(operation: (transaction: RelayDatabase) => Promise<T>): Promise<T> {
|
|
return await this.delegate.transaction(
|
|
async (transaction) =>
|
|
await operation({
|
|
query: async (sql, params = []) => await transaction.query(sql, params),
|
|
queryLocked: async (sql, params = [], options = {}) =>
|
|
await this.lockedQuery(transaction, sql, params, options),
|
|
transaction: async (nested) => await transaction.transaction(nested),
|
|
close: async () => {}
|
|
})
|
|
)
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.delegate.close()
|
|
}
|
|
|
|
private async lockedQuery(
|
|
database: RelayDatabase,
|
|
sql: string,
|
|
params: unknown[],
|
|
options: RelayLockOptions
|
|
): Promise<SqlRow[]> {
|
|
const inventory = sql.trim() === 'SELECT * FROM relay_cells ORDER BY cell_id ASC'
|
|
if (this.armed && inventory && options.failIfUnavailable) {
|
|
this.armed = false
|
|
this.failed = true
|
|
this.retryLocks.push('inventory-nowait-failed')
|
|
throw new Error('database_lock_unavailable')
|
|
}
|
|
if (this.failed && inventory) this.retryLocks.push('inventory-first')
|
|
if (this.failed && sql.includes('FROM relay_assignments')) {
|
|
this.retryLocks.push(options.failIfUnavailable ? 'assignment-nowait' : 'assignment')
|
|
}
|
|
return await database.queryLocked(sql, params, options)
|
|
}
|
|
}
|
|
|
|
class RepeatedDrainAccountingFailureDatabase implements RelayDatabase {
|
|
refreshAttempts = 0
|
|
private armed = false
|
|
private failuresRemaining = 0
|
|
|
|
constructor(private readonly delegate: RelayDatabase) {}
|
|
|
|
arm(failures: number): void {
|
|
this.armed = true
|
|
this.failuresRemaining = failures
|
|
this.refreshAttempts = 0
|
|
}
|
|
|
|
async query(sql: string, params: unknown[] = []): Promise<SqlRow[]> {
|
|
return await this.delegate.query(sql, params)
|
|
}
|
|
|
|
async queryLocked(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
options: RelayLockOptions = {}
|
|
): Promise<SqlRow[]> {
|
|
return await this.lockedQuery(this.delegate, sql, params, options)
|
|
}
|
|
|
|
async transaction<T>(operation: (transaction: RelayDatabase) => Promise<T>): Promise<T> {
|
|
return await this.delegate.transaction(
|
|
async (transaction) =>
|
|
await operation({
|
|
query: async (sql, params = []) => await transaction.query(sql, params),
|
|
queryLocked: async (sql, params = [], options = {}) =>
|
|
await this.lockedQuery(transaction, sql, params, options),
|
|
transaction: async (nested) => await transaction.transaction(nested),
|
|
close: async () => {}
|
|
})
|
|
)
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.delegate.close()
|
|
}
|
|
|
|
private async lockedQuery(
|
|
database: RelayDatabase,
|
|
sql: string,
|
|
params: unknown[],
|
|
options: RelayLockOptions
|
|
): Promise<SqlRow[]> {
|
|
const drainRefresh =
|
|
sql.includes('SELECT migration.*') &&
|
|
sql.includes('WHERE migration.source_cell_id = ?') &&
|
|
!sql.includes('source_cell_incarnation')
|
|
if (this.armed && drainRefresh) {
|
|
this.refreshAttempts++
|
|
if (this.failuresRemaining > 0) {
|
|
this.failuresRemaining--
|
|
throw new Error('migration_activity_accounting_mismatch')
|
|
}
|
|
}
|
|
return await database.queryLocked(sql, params, options)
|
|
}
|
|
}
|
|
|
|
// Runs a side effect between the sticky lane and the placement lane, the window
|
|
// in which a host can acquire control after the sticky read called it dormant.
|
|
class SeedBetweenAssignmentLanesDatabase implements RelayDatabase {
|
|
private transactions = 0
|
|
private armed = false
|
|
|
|
constructor(
|
|
private readonly delegate: RelayDatabase,
|
|
private readonly seed: () => Promise<void>
|
|
) {}
|
|
|
|
arm(): void {
|
|
this.armed = true
|
|
this.transactions = 0
|
|
}
|
|
|
|
async query(sql: string, params: unknown[] = []): Promise<SqlRow[]> {
|
|
return await this.delegate.query(sql, params)
|
|
}
|
|
|
|
async queryLocked(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
options: RelayLockOptions = {}
|
|
): Promise<SqlRow[]> {
|
|
return await this.delegate.queryLocked(sql, params, options)
|
|
}
|
|
|
|
async transaction<T>(
|
|
operation: (transaction: RelayDatabase) => Promise<T>,
|
|
options?: RelayTransactionOptions
|
|
): Promise<T> {
|
|
if (this.armed && ++this.transactions === 2) await this.seed()
|
|
return await this.delegate.transaction(operation, options)
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.delegate.close()
|
|
}
|
|
}
|
|
|
|
describe('RelayAssignmentStore', () => {
|
|
let database: RelayDatabase | undefined
|
|
|
|
afterEach(async () => await database?.close())
|
|
|
|
async function setup(now: () => number, cells = CELLS): Promise<RelayAssignmentStore> {
|
|
database = await openInMemoryRelayDatabase()
|
|
const store = new RelayAssignmentStore(database, now)
|
|
await store.reconcileCells(cells)
|
|
return store
|
|
}
|
|
|
|
async function setupWithHeartbeats(
|
|
now: () => number,
|
|
cells = CELLS
|
|
): Promise<RelayAssignmentStore> {
|
|
database = await openInMemoryRelayDatabase()
|
|
const store = new RelayAssignmentStore(database, now, {
|
|
requireLiveCells: true,
|
|
heartbeatTtlMs: 45_000
|
|
})
|
|
await store.reconcileCells(cells)
|
|
return store
|
|
}
|
|
|
|
async function heartbeat(
|
|
store: RelayAssignmentStore,
|
|
cell = CELLS[0]!,
|
|
input: {
|
|
incarnation?: string
|
|
startedAt?: number
|
|
ready?: boolean
|
|
observedRequests?: number
|
|
} = {}
|
|
): Promise<void> {
|
|
await store.recordCellHeartbeat({
|
|
cellId: cell.id,
|
|
cellUrl: cell.url,
|
|
cellIncarnation: input.incarnation ?? '11111111-1111-4111-8111-111111111111',
|
|
startedAt: input.startedAt ?? 50,
|
|
ready: input.ready ?? true,
|
|
observedRequests: input.observedRequests ?? 0,
|
|
...(cell.connectionHardCap === undefined
|
|
? {}
|
|
: {
|
|
totalConnections: 0,
|
|
inFlightConnections: 0,
|
|
reservedConnectionUnits: 0,
|
|
enforcedConnectionUnits: 0,
|
|
connectionHardCap: cell.connectionHardCap,
|
|
connectionUnobservedBound: cell.connectionUnobservedBound
|
|
})
|
|
})
|
|
}
|
|
|
|
it('admits only cells with a fresh ready heartbeat', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
|
|
await expect(store.assign(identity)).rejects.toThrow('relay_capacity_exhausted')
|
|
await heartbeat(store, CELLS[0]!, { ready: false })
|
|
await expect(store.assign(identity)).rejects.toThrow('relay_capacity_exhausted')
|
|
await heartbeat(store)
|
|
expect((await store.assign(identity)).cellId).toBe('cell-a')
|
|
now += 45_001
|
|
expect(await store.resolve(identity)).toBeNull()
|
|
})
|
|
|
|
it('fences stale incarnations and origin mismatches', async () => {
|
|
const store = await setupWithHeartbeats(() => 100, [CELLS[0]!])
|
|
await heartbeat(store, CELLS[0]!, { startedAt: 50 })
|
|
await expect(
|
|
heartbeat(store, { ...CELLS[0]!, url: 'https://other.example.com' })
|
|
).rejects.toThrow('cell_origin_mismatch')
|
|
await expect(
|
|
heartbeat(store, CELLS[0]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222',
|
|
startedAt: 50
|
|
})
|
|
).rejects.toThrow('stale_cell_incarnation')
|
|
await heartbeat(store, CELLS[0]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222',
|
|
startedAt: 51
|
|
})
|
|
await expect(heartbeat(store, CELLS[0]!, { startedAt: 50 })).rejects.toThrow(
|
|
'stale_cell_incarnation'
|
|
)
|
|
})
|
|
|
|
it('records receipt-relative legacy drain states and pins post-send migrations', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET migration_leases = migration_leases + 1
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
const incarnation = '11111111-1111-4111-8111-111111111111'
|
|
const attemptId = '33333333-3333-4333-8333-333333333333'
|
|
const traceValue = '44444444-4444-4444-8444-444444444444'
|
|
|
|
await expect(
|
|
store.prepareCellDrainAttempt({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation,
|
|
traceValue,
|
|
plannedGraceMs: 120_000
|
|
})
|
|
).resolves.toMatchObject({ state: 'prepared', shouldSend: false })
|
|
expect(
|
|
await database!.query(`SELECT * FROM relay_post_drain_migration_pins`)
|
|
).toEqual([])
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation
|
|
})
|
|
).resolves.toMatchObject({
|
|
shouldSend: false,
|
|
preparedAttempt: {
|
|
attemptId,
|
|
state: 'prepared',
|
|
traceValue,
|
|
plannedGraceMs: 120_000
|
|
}
|
|
})
|
|
|
|
await expect(
|
|
store.beginCellDrainSend({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation
|
|
})
|
|
).resolves.toMatchObject({
|
|
state: 'send-may-have-started',
|
|
shouldSend: true,
|
|
sendPermitExpiresAt: 30_100
|
|
})
|
|
await expect(
|
|
database!.query(
|
|
`SELECT migration_leases FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).resolves.toEqual([{ migration_leases: 1 }])
|
|
await expect(
|
|
store.beginCellDrainSend({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation
|
|
})
|
|
).resolves.toMatchObject({
|
|
state: 'send-may-have-started',
|
|
shouldSend: false
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation
|
|
})
|
|
).rejects.toThrow('drain_application_receipt_missing')
|
|
expect(
|
|
await database!.query(
|
|
`SELECT drain_attempt_id, source_cell_id, source_cell_incarnation,
|
|
target_cell_id, assignment_epoch
|
|
FROM relay_post_drain_migration_pins`
|
|
)
|
|
).toEqual([
|
|
{
|
|
drain_attempt_id: attemptId,
|
|
source_cell_id: 'cell-a',
|
|
source_cell_incarnation: incarnation,
|
|
target_cell_id: 'cell-b',
|
|
assignment_epoch: migration.assignmentEpoch
|
|
}
|
|
])
|
|
|
|
now = migration.expiresAt + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
expect(await store.releaseExpiredActivityLeases()).toBe(1)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_kind, cell_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? ORDER BY activity_kind`,
|
|
[identity.userId]
|
|
)
|
|
).toEqual([
|
|
{ activity_kind: 'control', cell_id: 'cell-b' },
|
|
{ activity_kind: 'migration', cell_id: 'cell-b' }
|
|
])
|
|
|
|
now = 200_000
|
|
await expect(
|
|
store.recordCellDrainApplicationReceipt({
|
|
attemptId,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: incarnation,
|
|
traceValue,
|
|
backendStatus: 200
|
|
})
|
|
).resolves.toMatchObject({
|
|
state: 'application-receipt',
|
|
applicationReceiptAt: 200_000,
|
|
retryAfter: 350_000
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainRecovery({ attemptId, cellId: 'cell-a', cellIncarnation: incarnation })
|
|
).rejects.toThrow('drain_recovery_too_early')
|
|
now = 350_000
|
|
await expect(
|
|
store.prepareCellDrainRecovery({ attemptId, cellId: 'cell-a', cellIncarnation: incarnation })
|
|
).resolves.toEqual({
|
|
shouldSend: true,
|
|
retryAfter: 350_000
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainRecovery({ attemptId, cellId: 'cell-a', cellIncarnation: incarnation })
|
|
).resolves.toEqual({
|
|
shouldSend: false,
|
|
retryAfter: 350_000
|
|
})
|
|
})
|
|
|
|
it('recovers a proven drain once after the source cell is replaced', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
const oldIncarnation = '11111111-1111-4111-8111-111111111111'
|
|
const newIncarnation = '22222222-2222-4222-8222-222222222222'
|
|
const newerIncarnation = '55555555-5555-4555-8555-555555555555'
|
|
const attempt = {
|
|
attemptId: '33333333-3333-4333-8333-333333333333',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: oldIncarnation,
|
|
traceValue: '44444444-4444-4444-8444-444444444444',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
await store.beginCellDrainSend(attempt)
|
|
now = 200
|
|
await store.recordCellDrainApplicationReceipt({
|
|
...attempt,
|
|
backendStatus: 200
|
|
})
|
|
|
|
now = 150_200
|
|
await heartbeat(store, CELLS[0]!, {
|
|
incarnation: newIncarnation,
|
|
startedAt: 51
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newIncarnation
|
|
})
|
|
).resolves.toEqual({ shouldSend: true, retryAfter: 150_200 })
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newIncarnation
|
|
})
|
|
).resolves.toEqual({ shouldSend: false, retryAfter: 150_200 })
|
|
|
|
now = 150_300
|
|
await heartbeat(store, CELLS[0]!, {
|
|
incarnation: newerIncarnation,
|
|
startedAt: 52
|
|
})
|
|
const concurrentRecoveries = await Promise.all([
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newerIncarnation
|
|
}),
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newerIncarnation
|
|
})
|
|
])
|
|
expect(concurrentRecoveries.map((recovery) => recovery.shouldSend).sort()).toEqual([
|
|
false,
|
|
true
|
|
])
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newerIncarnation
|
|
})
|
|
).resolves.toEqual({ shouldSend: false, retryAfter: 150_200 })
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_incarnation FROM relay_cell_drain_recovery_attempts
|
|
WHERE drain_attempt_id = ? ORDER BY cell_incarnation`,
|
|
[attempt.attemptId]
|
|
)
|
|
).resolves.toEqual([
|
|
{ cell_incarnation: newIncarnation },
|
|
{ cell_incarnation: newerIncarnation }
|
|
])
|
|
})
|
|
|
|
it('rejects an invalid receipt and a prepared drain from an old incarnation', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
const oldIncarnation = '11111111-1111-4111-8111-111111111111'
|
|
const newIncarnation = '22222222-2222-4222-8222-222222222222'
|
|
const attempt = {
|
|
attemptId: '33333333-3333-4333-8333-333333333333',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: oldIncarnation,
|
|
traceValue: '44444444-4444-4444-8444-444444444444',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
now = 150_200
|
|
await heartbeat(store, CELLS[0]!, {
|
|
incarnation: newIncarnation,
|
|
startedAt: 51
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newIncarnation
|
|
})
|
|
).rejects.toThrow('drain_application_receipt_missing')
|
|
|
|
await database!.query(
|
|
`UPDATE relay_cell_drain_attempt_states
|
|
SET state = 'application-receipt', application_receipt_at = ?,
|
|
receipt_cell_incarnation = ?, retry_after = ?
|
|
WHERE attempt_id = ?`,
|
|
[200, oldIncarnation, 150_200, attempt.attemptId]
|
|
)
|
|
await expect(
|
|
store.prepareCellDrainRecovery({
|
|
cellId: 'cell-a',
|
|
cellIncarnation: newIncarnation
|
|
})
|
|
).rejects.toThrow('drain_application_receipt_missing')
|
|
})
|
|
|
|
it('restores an expired registered migration lease before a prepared drain send', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
const attempt = {
|
|
attemptId: '55555555-5555-4555-8555-555555555555',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: '66666666-6666-4666-8666-666666666666',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
|
|
now = migration.expiresAt + 1
|
|
expect(await store.releaseExpiredActivityLeases()).toBeGreaterThan(0)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
await expect(
|
|
store.beginCellDrainSend({
|
|
attemptId: attempt.attemptId,
|
|
cellId: attempt.cellId,
|
|
cellIncarnation: attempt.cellIncarnation
|
|
})
|
|
).resolves.toMatchObject({
|
|
state: 'send-may-have-started',
|
|
shouldSend: true
|
|
})
|
|
await expect(
|
|
database!.query(
|
|
`SELECT activity_id, activity_kind, cell_id, request_units, expires_at
|
|
FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).resolves.toContainEqual({
|
|
activity_id: `migration:${migration.assignmentEpoch}`,
|
|
activity_kind: 'migration',
|
|
cell_id: 'cell-b',
|
|
request_units: 1,
|
|
expires_at: now + ASSIGNMENT_LIMITS.migrationLeaseMs
|
|
})
|
|
})
|
|
|
|
it('refreshes registered migration leases before prepared drain recovery', async () => {
|
|
let now = 100
|
|
const cappedCells = CELLS.map((cell) => ({
|
|
...cell,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
}))
|
|
const store = await setupWithHeartbeats(() => now, cappedCells)
|
|
await heartbeat(store, cappedCells[0]!)
|
|
await heartbeat(store, cappedCells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
const attempt = {
|
|
attemptId: '99999999-9999-4999-8999-999999999999',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
|
|
now = migration.expiresAt - 500_000
|
|
await heartbeat(store, cappedCells[0]!)
|
|
await heartbeat(store, cappedCells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
await expect(store.prepareCellDrainRecovery(attempt)).resolves.toMatchObject({
|
|
shouldSend: false,
|
|
preparedAttempt: { attemptId: attempt.attemptId }
|
|
})
|
|
const refreshedExpiresAt = now + ASSIGNMENT_LIMITS.migrationLeaseMs
|
|
await expect(
|
|
database!.query(
|
|
`SELECT expires_at FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ? AND assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, migration.assignmentEpoch]
|
|
)
|
|
).resolves.toEqual([{ expires_at: refreshedExpiresAt }])
|
|
await expect(
|
|
database!.query(
|
|
`SELECT state, timeout_at FROM relay_control_connection_reservations
|
|
WHERE user_id = ? AND relay_host_id = ? AND assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, migration.assignmentEpoch]
|
|
)
|
|
).resolves.toEqual([{ state: 'reserved', timeout_at: refreshedExpiresAt }])
|
|
})
|
|
|
|
it('bounds repeated accounting repair before prepared drain recovery', async () => {
|
|
const delegate = await openInMemoryRelayDatabase()
|
|
const retryDatabase = new RepeatedDrainAccountingFailureDatabase(delegate)
|
|
database = retryDatabase
|
|
const store = new RelayAssignmentStore(retryDatabase, () => 100, {
|
|
requireLiveCells: true,
|
|
heartbeatTtlMs: 45_000
|
|
})
|
|
await store.reconcileCells(CELLS)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
const attempt = {
|
|
attemptId: '99999999-9999-4999-8999-999999999999',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
retryDatabase.arm(2)
|
|
|
|
await expect(store.prepareCellDrainRecovery(attempt)).resolves.toMatchObject({
|
|
shouldSend: false,
|
|
preparedAttempt: { attemptId: attempt.attemptId }
|
|
})
|
|
expect(retryDatabase.refreshAttempts).toBe(3)
|
|
|
|
retryDatabase.arm(4)
|
|
await expect(store.prepareCellDrainRecovery(attempt)).rejects.toThrow(
|
|
'migration_activity_accounting_mismatch'
|
|
)
|
|
expect(retryDatabase.refreshAttempts).toBe(4)
|
|
})
|
|
|
|
it('retires a pinned migration superseded by a newer authoritative assignment', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
...CELLS,
|
|
{
|
|
id: 'cell-c',
|
|
url: 'https://relay-c.example.com',
|
|
capacityRequests: 2
|
|
}
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
await heartbeat(store, cells[2]!, {
|
|
incarnation: '33333333-3333-4333-8333-333333333333'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
const attempt = {
|
|
attemptId: '99999999-9999-4999-8999-999999999999',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
await store.beginCellDrainSend(attempt)
|
|
const receipt = await store.recordCellDrainApplicationReceipt({
|
|
...attempt,
|
|
backendStatus: 200
|
|
})
|
|
|
|
await database!.query(
|
|
`DELETE FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET reserved_controls = 0, reserved_splices = 0,
|
|
reserved_invites = 0, pending_installs = 0, pending_confirmations = 0,
|
|
migration_leases = 0, lease_expires_at = 0, last_activity_at = 0
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 0 WHERE cell_id IN (?, ?)`,
|
|
['cell-a', 'cell-b']
|
|
)
|
|
now = ASSIGNMENT_LIMITS.dormantTtlMs + 1
|
|
await heartbeat(store, cells[2]!, {
|
|
incarnation: '33333333-3333-4333-8333-333333333333'
|
|
})
|
|
const current = await store.rebalanceDormant(identity, 'cell-c')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-c',
|
|
assignmentEpoch: current.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
|
|
expect(receipt.retryAfter).toBeLessThanOrEqual(now)
|
|
await heartbeat(store, cells[0]!)
|
|
await expect(store.prepareCellDrainRecovery(attempt)).resolves.toMatchObject({
|
|
shouldSend: true
|
|
})
|
|
await expect(
|
|
database!.query(
|
|
`SELECT aborted_at FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ? AND assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, migration.assignmentEpoch]
|
|
)
|
|
).resolves.toEqual([{ aborted_at: now }])
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_id, assignment_epoch FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).resolves.toEqual([
|
|
{ cell_id: 'cell-c', assignment_epoch: current.assignmentEpoch }
|
|
])
|
|
await expect(
|
|
database!.query(
|
|
`SELECT activity_id, cell_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).resolves.toEqual([{ activity_id: 'control:cell-c:1', cell_id: 'cell-c' }])
|
|
})
|
|
|
|
it('refuses to restore a missing migration lease without durable target registration', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
const attempt = {
|
|
attemptId: '77777777-7777-4777-8777-777777777777',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: '88888888-8888-4888-8888-888888888888',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(attempt)
|
|
|
|
now = migration.expiresAt + 1
|
|
expect(await store.releaseExpiredActivityLeases()).toBeGreaterThan(0)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
await expect(
|
|
store.beginCellDrainSend({
|
|
attemptId: attempt.attemptId,
|
|
cellId: attempt.cellId,
|
|
cellIncarnation: attempt.cellIncarnation
|
|
})
|
|
).rejects.toThrow('migration_activity_lease_shape_mismatch')
|
|
})
|
|
|
|
it('moves a dead assignment only after a completed exact-incarnation fence attempt', async () => {
|
|
let now = 100
|
|
const cappedCells = CELLS.map((cell) => ({
|
|
...cell,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
}))
|
|
const store = await setupWithHeartbeats(() => now, cappedCells)
|
|
await heartbeat(store, cappedCells[0]!)
|
|
await heartbeat(store, cappedCells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.setCellEnabled('cell-a', false)
|
|
now += 45_001
|
|
await heartbeat(store, cappedCells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
|
|
await store.attestCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id FROM relay_cell_legacy_fence_adoptions WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).toEqual([])
|
|
expect(await store.evacuateDeadCells()).toBe(0)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id FROM relay_assignments WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ cell_id: 'cell-a' }])
|
|
|
|
now += 300_001
|
|
await heartbeat(store, cappedCells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const evidence = {
|
|
attemptId: '22222222-2222-4222-8222-222222222222',
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
await store.prepareCellFenceAttempt(evidence)
|
|
await store.bindCellFencePlanGeneration(evidence, evidence.planObjectGeneration)
|
|
await store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
await store.recordCellFenceOperation(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON,
|
|
'operation-1'
|
|
)
|
|
await store.attestCellFenceAttempt(evidence, 'operation-1')
|
|
|
|
expect(await store.evacuateDeadCells()).toBe(1)
|
|
expect((await store.resolve(identity))?.cellId).toBe('cell-b')
|
|
})
|
|
|
|
it('preserves proven non-delivery and permits one fresh full-grace attempt', async () => {
|
|
const store = await setupWithHeartbeats(() => 100, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const first = {
|
|
attemptId: '33333333-3333-4333-8333-333333333333',
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
traceValue: '44444444-4444-4444-8444-444444444444',
|
|
plannedGraceMs: 120_000
|
|
}
|
|
await store.prepareCellDrainAttempt(first)
|
|
await store.beginCellDrainSend(first)
|
|
await expect(store.proveCellDrainNotDelivered(first)).resolves.toMatchObject({
|
|
state: 'proven-not-delivered',
|
|
provenNotDeliveredAt: 100
|
|
})
|
|
await expect(
|
|
store.prepareCellDrainAttempt({
|
|
...first,
|
|
attemptId: '55555555-5555-4555-8555-555555555555',
|
|
traceValue: '66666666-6666-4666-8666-666666666666'
|
|
})
|
|
).resolves.toMatchObject({ state: 'prepared' })
|
|
})
|
|
|
|
it('binds fence attestation to one durable Terraform attempt', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const evidence = {
|
|
attemptId: '22222222-2222-4222-8222-222222222222',
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
const prepared = await store.prepareCellFenceAttempt(evidence)
|
|
expect(prepared).toMatchObject({ createdAt: 100, expiresAt: 3_600_100 })
|
|
expect(prepared.planObjectGeneration).toBeUndefined()
|
|
await expect(
|
|
store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
evidence.planObjectGeneration
|
|
)
|
|
).resolves.toMatchObject({
|
|
planObjectGeneration: evidence.planObjectGeneration
|
|
})
|
|
await expect(
|
|
store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
evidence.planObjectGeneration
|
|
)
|
|
).resolves.toMatchObject({
|
|
planObjectGeneration: evidence.planObjectGeneration
|
|
})
|
|
await expect(
|
|
store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
'987654321'
|
|
)
|
|
).rejects.toThrow('cell_fence_plan_generation_mismatch')
|
|
for (const changed of [
|
|
{ attemptId: '33333333-3333-4333-8333-333333333333' },
|
|
{ environment: 'staging' as const },
|
|
{ cellId: 'cell-b' },
|
|
{ cellIncarnation: '33333333-3333-4333-8333-333333333333' },
|
|
{ migName: 'orca-relay-other' },
|
|
{ instanceGroup: `${evidence.instanceGroup}-other` },
|
|
{ generationIdentity: `${evidence.generationIdentity}-other` },
|
|
{ fenceCommit: 'c'.repeat(40) },
|
|
{ planSha256: 'c'.repeat(64) }
|
|
]) {
|
|
await expect(
|
|
store.startCellFenceApply(
|
|
{ ...evidence, ...changed },
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
).rejects.toThrow()
|
|
}
|
|
await store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
await store.recordCellFenceOperation(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON,
|
|
'operation-1'
|
|
)
|
|
now += 45_001
|
|
await expect(
|
|
store.attestCellFenceAttempt(evidence, 'operation-other')
|
|
).rejects.toThrow('cell_fence_operation_not_attested')
|
|
const attested = await store.attestCellFenceAttempt(evidence, 'operation-1')
|
|
expect(attested).toMatchObject({
|
|
expiresAt: now + 300_000,
|
|
attempt: { ...evidence, gceOperation: 'operation-1', completedAt: now }
|
|
})
|
|
await expect(store.attestCellFenceAttempt(evidence, 'operation-1')).resolves.toEqual(
|
|
attested
|
|
)
|
|
now += 300_001
|
|
await expect(
|
|
store.attestCellFenceAttempt(evidence, 'operation-1')
|
|
).resolves.toMatchObject({
|
|
expiresAt: now + 300_000,
|
|
attempt: { completedAt: attested.attempt.completedAt }
|
|
})
|
|
})
|
|
|
|
it('aborts only a Terraform fence whose apply never started', async () => {
|
|
const store = await setupWithHeartbeats(() => 100, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const evidence = {
|
|
attemptId: '22222222-2222-4222-8222-222222222222',
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
await store.prepareCellFenceAttempt(evidence)
|
|
await store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
evidence.planObjectGeneration
|
|
)
|
|
await expect(store.abortCellFenceAttempt(evidence)).resolves.toMatchObject({
|
|
abortedAt: 100
|
|
})
|
|
await expect(
|
|
store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
).rejects.toThrow(
|
|
'cell_fence_attempt_aborted'
|
|
)
|
|
})
|
|
|
|
it('adopts a stale disabled legacy fence only when no durable attempt exists', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
now += 45_001
|
|
|
|
await expect(
|
|
store.adoptLegacyCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
).resolves.toBe(now + 300_000)
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_id, cell_incarnation FROM relay_cell_fences WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).resolves.toEqual([
|
|
{
|
|
cell_id: 'cell-a',
|
|
cell_incarnation: '11111111-1111-4111-8111-111111111111'
|
|
}
|
|
])
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_id, cell_incarnation
|
|
FROM relay_cell_legacy_fence_adoptions WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).resolves.toEqual([])
|
|
await store.commitLegacyCellFenceAdoption(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_id, cell_incarnation
|
|
FROM relay_cell_legacy_fence_adoptions WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).resolves.toEqual([
|
|
{
|
|
cell_id: 'cell-a',
|
|
cell_incarnation: '11111111-1111-4111-8111-111111111111'
|
|
}
|
|
])
|
|
|
|
now += 1
|
|
await expect(
|
|
store.adoptLegacyCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
).resolves.toBe(now + 300_000)
|
|
await store.commitLegacyCellFenceAdoption(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
await expect(
|
|
database!.query(
|
|
`SELECT attested_at, expires_at
|
|
FROM relay_cell_legacy_fence_adoptions WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).resolves.toEqual([{ attested_at: now, expires_at: now + 300_000 }])
|
|
|
|
await heartbeat(store)
|
|
await expect(
|
|
database!.query(
|
|
`SELECT cell_id FROM relay_cell_legacy_fence_adoptions WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
).resolves.toEqual([])
|
|
})
|
|
|
|
it.each(['active', 'completed', 'aborted'] as const)(
|
|
'rejects legacy adoption after a %s durable attempt',
|
|
async (status) => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const evidence = cellFenceEvidence()
|
|
await store.prepareCellFenceAttempt(evidence)
|
|
if (status === 'aborted') await store.abortCellFenceAttempt(evidence)
|
|
if (status === 'completed') {
|
|
await store.bindCellFencePlanGeneration(evidence, evidence.planObjectGeneration)
|
|
await store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
await store.recordCellFenceOperation(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON,
|
|
'operation-1'
|
|
)
|
|
}
|
|
now += 45_001
|
|
if (status === 'completed') {
|
|
await store.attestCellFenceAttempt(evidence, 'operation-1')
|
|
}
|
|
|
|
await expect(
|
|
store.adoptLegacyCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
).rejects.toThrow('legacy_cell_fence_attempt_exists')
|
|
}
|
|
)
|
|
|
|
it('serializes legacy adoption against durable attempt preparation', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
now += 45_001
|
|
|
|
const results = await Promise.allSettled([
|
|
store.adoptLegacyCellFence('cell-a', '11111111-1111-4111-8111-111111111111'),
|
|
store.prepareCellFenceAttempt(cellFenceEvidence())
|
|
])
|
|
expect(results.filter(({ status }) => status === 'fulfilled')).toHaveLength(1)
|
|
const attempts = await database!.query(
|
|
`SELECT COUNT(*) AS count FROM relay_cell_fence_attempts WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
const fences = await database!.query(
|
|
`SELECT COUNT(*) AS count FROM relay_cell_fences WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
const adoptions = await database!.query(
|
|
`SELECT COUNT(*) AS count FROM relay_cell_legacy_fence_adoptions
|
|
WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
expect(Number(attempts[0]!.count) + Number(fences[0]!.count)).toBe(1)
|
|
expect(Number(adoptions[0]!.count)).toBe(0)
|
|
})
|
|
|
|
it('rejects an expired durable Terraform fence attempt', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const evidence = {
|
|
attemptId: '22222222-2222-4222-8222-222222222222',
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
await store.prepareCellFenceAttempt(evidence)
|
|
await store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
evidence.planObjectGeneration
|
|
)
|
|
now += 3_600_001
|
|
await expect(
|
|
store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
).rejects.toThrow(
|
|
'cell_fence_attempt_expired'
|
|
)
|
|
})
|
|
|
|
it('keeps a started Terraform fence attempt recoverable after its preparation TTL', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
await store.setCellEnabled('cell-a', false)
|
|
const evidence = {
|
|
attemptId: '22222222-2222-4222-8222-222222222222',
|
|
environment: 'production' as const,
|
|
cellId: 'cell-a',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
migName: 'orca-relay-c1',
|
|
instanceGroup: 'https://compute.example/instanceGroups/orca-relay-c1',
|
|
generationIdentity: 'https://compute.example/instanceTemplates/orca-relay-c1-abc',
|
|
fenceCommit: 'a'.repeat(40),
|
|
planSha256: 'b'.repeat(64),
|
|
...FENCE_PLAN_BINDING
|
|
}
|
|
await store.prepareCellFenceAttempt(evidence)
|
|
await store.bindCellFencePlanGeneration(
|
|
evidence,
|
|
evidence.planObjectGeneration
|
|
)
|
|
await store.startCellFenceApply(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON
|
|
)
|
|
now += 3_600_001
|
|
await expect(
|
|
store.recordCellFenceOperation(
|
|
evidence,
|
|
FENCE_INVOCATION_ID,
|
|
FENCE_INVOCATION_REASON,
|
|
'operation-1'
|
|
)
|
|
).resolves.toMatchObject({
|
|
attempt: { gceOperation: 'operation-1' },
|
|
invocation: { gceOperation: 'operation-1' }
|
|
})
|
|
await expect(
|
|
store.prepareCellFenceAttempt({
|
|
...evidence,
|
|
attemptId: '44444444-4444-4444-8444-444444444444',
|
|
planObjectName:
|
|
'terraform/state/relay-fence-plans/production/44444444-4444-4444-8444-444444444444.tfplan',
|
|
requestReason:
|
|
'orca-relay-fence/44444444-4444-4444-8444-444444444444'
|
|
})
|
|
).rejects.toThrow('cell_fence_attempt_evidence_mismatch')
|
|
})
|
|
|
|
it('reports aggregate deployment state and heartbeat freshness without identities', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [CELLS[0]!])
|
|
await heartbeat(store)
|
|
const identity = { userId: 'private-user', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
activityLeases: 1,
|
|
activityRequestUnits: 1,
|
|
restartBlockingActivityLeases: 0,
|
|
restartBlockingActivityRequestUnits: 0,
|
|
restartBlockingReservedRequests: 0
|
|
})
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
|
|
expect(await store.cellDeploymentStatus('cell-a')).toEqual({
|
|
cellId: 'cell-a',
|
|
cellUrl: 'https://relay-a.example.com',
|
|
region: 'us-central1',
|
|
enabled: true,
|
|
admissionState: 'general',
|
|
capacityRequests: 2,
|
|
reservedRequests: 1,
|
|
assignments: 1,
|
|
activityLeases: 1,
|
|
activityRequestUnits: 1,
|
|
restartBlockingActivityLeases: 1,
|
|
restartBlockingActivityRequestUnits: 1,
|
|
restartBlockingReservedRequests: 1,
|
|
outgoingMigrations: 0,
|
|
incomingMigrations: 0,
|
|
connectionCapacity: null,
|
|
runtime: {
|
|
cellUrl: 'https://relay-a.example.com',
|
|
cellIncarnation: '11111111-1111-4111-8111-111111111111',
|
|
startedAt: 50,
|
|
ready: true,
|
|
observedRequests: 0,
|
|
lastHeartbeatAt: 100,
|
|
heartbeatFresh: true,
|
|
regionalRehomeProtocol: 0
|
|
}
|
|
})
|
|
now += 45_001
|
|
expect((await store.cellDeploymentStatus('cell-a')).runtime?.heartbeatFresh).toBe(false)
|
|
await expect(store.cellDeploymentStatus('missing')).rejects.toThrow('cell_not_found')
|
|
})
|
|
|
|
it('keeps concurrent sticky assignment grants restart-safe', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'private-user', relayHostId: 'host000000000001' }
|
|
|
|
await Promise.all(Array.from({ length: 20 }, async () => await store.assign(identity)))
|
|
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
activityLeases: 1,
|
|
activityRequestUnits: 1,
|
|
reservedRequests: 1,
|
|
restartBlockingActivityLeases: 0,
|
|
restartBlockingActivityRequestUnits: 0,
|
|
restartBlockingReservedRequests: 0
|
|
})
|
|
})
|
|
|
|
it('classifies activated and non-control activity as restart-blocking', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'private-user', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
for (const kind of ['splice', 'invite', 'install', 'confirmation', 'migration'] as const) {
|
|
await store.acquireActivity(identity, {
|
|
activityId: `${kind}:test`,
|
|
kind,
|
|
cellId: assignment.cellId
|
|
})
|
|
}
|
|
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
activityLeases: 6,
|
|
activityRequestUnits: 7,
|
|
reservedRequests: 7,
|
|
restartBlockingActivityLeases: 6,
|
|
restartBlockingActivityRequestUnits: 7,
|
|
restartBlockingReservedRequests: 7
|
|
})
|
|
})
|
|
|
|
it('fails closed for malformed pending controls and unexplained reservations', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 }
|
|
])
|
|
await store.assign({ userId: 'private-user', relayHostId: 'host000000000001' })
|
|
await database!.query(
|
|
`UPDATE relay_assignment_activity_leases SET request_units = 2
|
|
WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
restartBlockingActivityLeases: 1,
|
|
restartBlockingActivityRequestUnits: 2,
|
|
restartBlockingReservedRequests: 1
|
|
})
|
|
|
|
await database!.query(
|
|
`UPDATE relay_assignment_activity_leases SET request_units = 1 WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 0 WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
restartBlockingActivityLeases: 0,
|
|
restartBlockingActivityRequestUnits: 0,
|
|
restartBlockingReservedRequests: -1
|
|
})
|
|
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 2 WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
expect(await store.cellDeploymentStatus('cell-a')).toMatchObject({
|
|
restartBlockingActivityLeases: 0,
|
|
restartBlockingActivityRequestUnits: 0,
|
|
restartBlockingReservedRequests: 1
|
|
})
|
|
})
|
|
|
|
it('keeps a migration blocking when its target also has a pending control', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'private-user', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
|
|
expect(await store.cellDeploymentStatus('cell-b')).toMatchObject({
|
|
activityLeases: 2,
|
|
restartBlockingActivityLeases: 1,
|
|
restartBlockingActivityRequestUnits: 1,
|
|
restartBlockingReservedRequests: 1,
|
|
incomingMigrations: 1
|
|
})
|
|
})
|
|
|
|
it('starts declared candidates disabled without overwriting later operator state', async () => {
|
|
const candidate: RelayCellConfig = {
|
|
id: 'candidate',
|
|
url: 'https://candidate.example.com',
|
|
capacityRequests: 20,
|
|
initiallyEnabled: false
|
|
}
|
|
const store = await setup(() => 100, [candidate])
|
|
expect((await store.cellDeploymentStatus('candidate')).enabled).toBe(false)
|
|
await store.setCellEnabled('candidate', true)
|
|
await store.reconcileCells([candidate])
|
|
expect((await store.cellDeploymentStatus('candidate')).enabled).toBe(true)
|
|
})
|
|
|
|
it('moves a stranded host off an existing-only cell that stopped serving it', async () => {
|
|
// Why: C3's decommission created an existing-only cell that rejects
|
|
// attaches; the #194 pin then loops returning hosts forever while each
|
|
// grant refreshes their own activity (issue #225). C3 has no
|
|
// connection-limits row and expired leases are cleaned within a
|
|
// maintenance cycle, so the only durable evidence is the assignment row:
|
|
// a recent grant with no live real activity behind it.
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.setCellEnabled(first.cellId, false)
|
|
|
|
// The pin holds while the last grant is young enough to still attach.
|
|
now += 10_000
|
|
const pinned = await store.assign(identity)
|
|
expect(pinned.cellId).toBe(first.cellId)
|
|
|
|
// The grant had ample time to attach and produced nothing live.
|
|
now += 61_000
|
|
const moved = await store.assign(identity)
|
|
expect(moved.cellId).not.toBe(first.cellId)
|
|
expect(moved.assignmentEpoch).toBe(first.assignmentEpoch + 1)
|
|
|
|
// The move is durable: no bounce back to the closed cell.
|
|
now += 1_000
|
|
const settled = await store.assign(identity)
|
|
expect(settled.cellId).toBe(moved.cellId)
|
|
expect(settled.assignmentEpoch).toBe(moved.assignmentEpoch)
|
|
})
|
|
|
|
it('keeps a serving existing-only cell pinned for hosts with live activity', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.setCellEnabled(first.cellId, false)
|
|
// A live claimed control proves the cell still serves this host.
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, 'control', ?, 1, ?, ?)`,
|
|
[identity.userId, identity.relayHostId, 'control:live-1', first.cellId, now + 600_000, now]
|
|
)
|
|
now += 61_000
|
|
const pinned = await store.assign(identity)
|
|
expect(pinned.cellId).toBe(first.cellId)
|
|
})
|
|
|
|
it('keeps migration-only cells pinned inside the stranded window', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.setCellAdmissionState(first.cellId, 'migration-only')
|
|
now += 61_000
|
|
const pinned = await store.assign(identity)
|
|
expect(pinned.cellId).toBe(first.cellId)
|
|
})
|
|
|
|
it('leaves quiet existing-only assignments to the normal dormancy rule', async () => {
|
|
// Why: outside the 15-minute stranded window there is no active retry
|
|
// loop to break; ordinary returns stay pinned until 24h dormancy.
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.setCellEnabled(first.cellId, false)
|
|
now += 20 * 60_000
|
|
const pinned = await store.assign(identity)
|
|
expect(pinned.cellId).toBe(first.cellId)
|
|
})
|
|
|
|
it('reassigns an active assignment from an uncapped stale cell without a fence', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now)
|
|
await heartbeat(store, CELLS[0]!)
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.changeActivity(identity, 'invite', 1)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_migrations
|
|
(user_id, relay_host_id, source_cell_id, target_cell_id,
|
|
previous_epoch, assignment_epoch, source_request_units,
|
|
target_reserved_units, expires_at, target_registered_at,
|
|
completed_at, aborted_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
'cell-b',
|
|
'cell-a',
|
|
-1,
|
|
0,
|
|
0,
|
|
1,
|
|
90_100,
|
|
now,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`INSERT INTO relay_post_drain_migration_pins
|
|
(user_id, relay_host_id, assignment_epoch, drain_attempt_id,
|
|
source_cell_id, source_cell_incarnation, target_cell_id,
|
|
target_cell_incarnation, source_request_units, target_reserved_units,
|
|
pinned_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
0,
|
|
'33333333-3333-4333-8333-333333333333',
|
|
'cell-b',
|
|
'22222222-2222-4222-8222-222222222222',
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111',
|
|
0,
|
|
1,
|
|
now
|
|
]
|
|
)
|
|
|
|
now += 45_001
|
|
await heartbeat(store, CELLS[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222'
|
|
})
|
|
expect(await store.evacuateDeadCells()).toBe(1)
|
|
const replacement = await store.resolve(identity)
|
|
expect(replacement).toMatchObject({
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: first.assignmentEpoch + 1
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_kind, cell_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ activity_kind: 'control', cell_id: 'cell-b' }])
|
|
})
|
|
|
|
it('does not let an unfenced capped cell starve eligible legacy recovery', async () => {
|
|
let now = 100
|
|
const cells: RelayCellConfig[] = [
|
|
{
|
|
id: 'cell-a',
|
|
url: 'https://relay-a.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600,
|
|
connectionUnobservedBound: 50
|
|
},
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
await store.setCellEnabled('cell-b', false)
|
|
await store.setCellEnabled('cell-c', false)
|
|
const cappedIdentity = { userId: 'a-capped', relayHostId: 'host000000000001' }
|
|
expect(await store.assign(cappedIdentity)).toMatchObject({ cellId: 'cell-a' })
|
|
|
|
await store.setCellEnabled('cell-a', false)
|
|
await store.setCellEnabled('cell-b', true)
|
|
const legacyIdentity = { userId: 'z-legacy', relayHostId: 'host000000000002' }
|
|
expect(await store.assign(legacyIdentity)).toMatchObject({ cellId: 'cell-b' })
|
|
await store.setCellEnabled('cell-c', true)
|
|
|
|
now += 45_001
|
|
await heartbeat(store, cells[2]!)
|
|
expect(await store.evacuateDeadCells(1)).toBe(1)
|
|
expect(await store.resolve(legacyIdentity)).toMatchObject({ cellId: 'cell-c' })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id FROM relay_assignments WHERE user_id = ?`,
|
|
[cappedIdentity.userId]
|
|
)
|
|
).toEqual([{ cell_id: 'cell-a' }])
|
|
})
|
|
|
|
it('refuses evacuation into a cell without a fresh ready heartbeat', async () => {
|
|
const store = await setupWithHeartbeats(() => 100)
|
|
await heartbeat(store, CELLS[0]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
|
|
await expect(store.startEvacuation(identity, 'cell-b')).rejects.toThrow(
|
|
'target_cell_unavailable'
|
|
)
|
|
})
|
|
|
|
it('keeps an active assignment sticky without increasing its epoch or reservation', async () => {
|
|
const store = await setup(() => 100)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const second = await store.assign(identity)
|
|
|
|
expect(second).toEqual(first)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 0 })
|
|
})
|
|
|
|
it('selects the least-loaded cell with a stable cell-id tie break', async () => {
|
|
const store = await setup(() => 100)
|
|
const first = await store.assign({ userId: 'user-a', relayHostId: 'host000000000001' })
|
|
const second = await store.assign({ userId: 'user-b', relayHostId: 'host000000000002' })
|
|
|
|
expect([first.cellId, second.cellId]).toEqual(['cell-a', 'cell-b'])
|
|
})
|
|
|
|
it('admits exactly through the configured capacity boundary', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 2 }
|
|
])
|
|
await store.assign({ userId: 'user-a', relayHostId: 'host000000000001' })
|
|
await store.assign({ userId: 'user-b', relayHostId: 'host000000000002' })
|
|
|
|
await expect(
|
|
store.assign({ userId: 'user-c', relayHostId: 'host000000000003' })
|
|
).rejects.toThrow('relay_capacity_exhausted')
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 2 })
|
|
})
|
|
|
|
it('does not oversubscribe when assignments race', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 2 }
|
|
])
|
|
const results = await Promise.allSettled(
|
|
Array.from({ length: 8 }, (_, index) =>
|
|
store.assign({ userId: `user-${index}`, relayHostId: `host${String(index).padStart(12, '0')}` })
|
|
)
|
|
)
|
|
|
|
expect(results.filter(({ status }) => status === 'fulfilled')).toHaveLength(2)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 2 })
|
|
})
|
|
|
|
it('reassigns only after all activity expires and the dormant TTL elapses', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.changeActivity(identity, 'invite', 1)
|
|
await store.changeActivity(identity, 'control', -1)
|
|
|
|
now += ASSIGNMENT_LIMITS.activityLeaseMs + 1
|
|
await store.releaseExpiredActivityLeases()
|
|
await store.releaseExpiredActivity()
|
|
await database!.query(`UPDATE relay_cells SET observed_requests = 2 WHERE cell_id = ?`, [
|
|
first.cellId
|
|
])
|
|
now += ASSIGNMENT_LIMITS.dormantTtlMs
|
|
const reassigned = await store.assign(identity)
|
|
|
|
expect(reassigned.cellId).not.toBe(first.cellId)
|
|
expect(reassigned.assignmentEpoch).toBe(first.assignmentEpoch + 1)
|
|
})
|
|
|
|
it('will not normally move an assignment while any durable activity remains', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.changeActivity(identity, 'migration', 1)
|
|
await store.changeActivity(identity, 'control', -1)
|
|
await database!.query(`UPDATE relay_cells SET observed_requests = 2 WHERE cell_id = ?`, [
|
|
first.cellId
|
|
])
|
|
now += ASSIGNMENT_LIMITS.dormantTtlMs + 1
|
|
|
|
expect(await store.assign(identity)).toMatchObject({
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch
|
|
})
|
|
})
|
|
|
|
it('releases every expired activity reservation without going negative', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
for (const kind of ['splice', 'invite', 'install', 'confirmation', 'migration'] as const) {
|
|
await store.changeActivity(identity, kind, 1)
|
|
}
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 7 })
|
|
|
|
now += ASSIGNMENT_LIMITS.activityLeaseMs + 1
|
|
expect(await store.releaseExpiredActivityLeases()).toBe(1)
|
|
expect(await store.releaseExpiredActivity()).toBe(1)
|
|
expect(await store.releaseExpiredActivity()).toBe(0)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0 })
|
|
})
|
|
|
|
it('isolates assignments by host and verifies both cell and epoch', async () => {
|
|
const store = await setup(() => 100)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
|
|
await expect(
|
|
store.verifyCellAssignment({ ...identity, cellId: assignment.cellId, assignmentEpoch: 1 })
|
|
).resolves.toBe(true)
|
|
await expect(
|
|
store.verifyCellAssignment({ ...identity, cellId: 'cell-b', assignmentEpoch: 1 })
|
|
).resolves.toBe(false)
|
|
await expect(
|
|
store.verifyCellAssignment({ ...identity, cellId: assignment.cellId, assignmentEpoch: 2 })
|
|
).resolves.toBe(false)
|
|
await expect(
|
|
store.resolve({ userId: 'other', relayHostId: identity.relayHostId })
|
|
).resolves.toBeNull()
|
|
})
|
|
|
|
it('converts the director reservation into an idempotent active-control lease', async () => {
|
|
const store = await setup(() => 100)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
|
|
const activityId = await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
|
|
expect(activityId).toBe(`control:${assignment.cellId}:1`)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 0 })
|
|
const leases = await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases`
|
|
)
|
|
expect(leases).toEqual([{ activity_id: activityId }])
|
|
})
|
|
|
|
it('transactionally supersedes older controls on only the same cell', async () => {
|
|
const cells = [
|
|
{
|
|
id: 'cell-a',
|
|
url: 'https://relay-a.example.com',
|
|
capacityRequests: 10
|
|
},
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setup(() => 100, cells)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.setCellEnabled('cell-b', false)
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-b', true)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, 'control', 'cell-b', 1, 90100, 100),
|
|
(?, ?, ?, 'control', 'cell-b', 1, 90100, 100)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
'control:cell-b:2',
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
'control:cell-b:3'
|
|
]
|
|
)
|
|
const latest = await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 4
|
|
})
|
|
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id, cell_id FROM relay_assignment_activity_leases
|
|
WHERE activity_kind = 'control' ORDER BY cell_id`
|
|
)
|
|
).toEqual([
|
|
{ activity_id: 'control:cell-a:1', cell_id: 'cell-a' },
|
|
{ activity_id: latest, cell_id: 'cell-b' }
|
|
])
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 2 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 2 }])
|
|
})
|
|
|
|
it('re-reserves a sticky grant for a host holding no control lease', async () => {
|
|
const store = await setup(() => 100)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
const control = await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.releaseActivity(identity, control)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases`
|
|
)
|
|
).toEqual([{ activity_id: 'control-pending:1' }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 1 }])
|
|
})
|
|
|
|
it('re-pins a host that took control after the sticky lane read it dormant', async () => {
|
|
const now = 100
|
|
const delegate = await openInMemoryRelayDatabase()
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const seeded = new SeedBetweenAssignmentLanesDatabase(delegate, async () => {
|
|
await delegate.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, 'control:cell-a:1', 'control', 'cell-a', 1, ?, ?),
|
|
(?, ?, 'control-pending:1', 'control', 'cell-a', 1, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now,
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await delegate.query(
|
|
`UPDATE relay_assignments SET reserved_controls = 2
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await delegate.query(
|
|
`UPDATE relay_cells SET reserved_requests = 2 WHERE cell_id = 'cell-a'`
|
|
)
|
|
})
|
|
database = seeded
|
|
const store = new RelayAssignmentStore(seeded, () => now)
|
|
await store.reconcileCells(CELLS)
|
|
await delegate.query(
|
|
`INSERT INTO relay_assignments
|
|
(user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at,
|
|
last_activity_at, reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases)
|
|
VALUES (?, ?, 'cell-a', 1, ?, ?, 0, 0, 0, 0, 0, 0)`,
|
|
[identity.userId, identity.relayHostId, now, now - ASSIGNMENT_LIMITS.dormantTtlMs]
|
|
)
|
|
seeded.arm()
|
|
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: 1
|
|
})
|
|
expect(
|
|
await delegate.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 2 }])
|
|
expect(await cellReservations(delegate)).toEqual({ 'cell-a': 2, 'cell-b': 0 })
|
|
expect(
|
|
await delegate.query(
|
|
`SELECT lease_expires_at, last_activity_at FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ lease_expires_at: now + ASSIGNMENT_LIMITS.activityLeaseMs, last_activity_at: now }
|
|
])
|
|
})
|
|
|
|
it('reserves control on the pinned cell when the only lease sits on another', async () => {
|
|
const now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await database!.query(
|
|
`INSERT INTO relay_assignments
|
|
(user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at,
|
|
last_activity_at, reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases)
|
|
VALUES (?, ?, 'cell-b', 2, ?, ?, 1, 0, 0, 0, 0, 0)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, 'control:cell-a:1', 'control', 'cell-a', 1, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 1 WHERE cell_id = 'cell-a'`
|
|
)
|
|
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id, cell_id FROM relay_assignment_activity_leases
|
|
ORDER BY activity_id ASC`
|
|
)
|
|
).toEqual([
|
|
{ activity_id: 'control-pending:2', cell_id: 'cell-b' },
|
|
{ activity_id: 'control:cell-a:1', cell_id: 'cell-a' }
|
|
])
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 1 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 2 }])
|
|
})
|
|
|
|
it('mints a pending control when the only lease is an older epoch pending', async () => {
|
|
const now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await database!.query(
|
|
`INSERT INTO relay_assignments
|
|
(user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at,
|
|
last_activity_at, reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases)
|
|
VALUES (?, ?, 'cell-a', 3, ?, ?, 1, 0, 0, 0, 0, 0)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, 'control-pending:1', 'control', 'cell-a', 1, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 1 WHERE cell_id = 'cell-a'`
|
|
)
|
|
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: 3
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
ORDER BY activity_id ASC`
|
|
)
|
|
).toEqual([{ activity_id: 'control-pending:1' }, { activity_id: 'control-pending:3' }])
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 2, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 2 }])
|
|
})
|
|
|
|
it('re-reserves a re-pinned grant for a host holding no control lease', async () => {
|
|
const now = 100
|
|
const delegate = await openInMemoryRelayDatabase()
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const seeded = new SeedBetweenAssignmentLanesDatabase(delegate, async () => {
|
|
await delegate.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, 'splice:connection-1', 'splice', 'cell-a', 1, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await delegate.query(
|
|
`UPDATE relay_assignments SET reserved_splices = 1
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await delegate.query(
|
|
`UPDATE relay_cells SET reserved_requests = 1 WHERE cell_id = 'cell-a'`
|
|
)
|
|
})
|
|
database = seeded
|
|
const store = new RelayAssignmentStore(seeded, () => now)
|
|
await store.reconcileCells(CELLS)
|
|
await delegate.query(
|
|
`INSERT INTO relay_assignments
|
|
(user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at,
|
|
last_activity_at, reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases)
|
|
VALUES (?, ?, 'cell-a', 1, ?, ?, 0, 0, 0, 0, 0, 0)`,
|
|
[identity.userId, identity.relayHostId, now, now - ASSIGNMENT_LIMITS.dormantTtlMs]
|
|
)
|
|
seeded.arm()
|
|
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: 1
|
|
})
|
|
expect(
|
|
await delegate.query(
|
|
`SELECT reserved_controls, reserved_splices FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 1, reserved_splices: 1 }])
|
|
expect(
|
|
await delegate.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
ORDER BY activity_id ASC`
|
|
)
|
|
).toEqual([{ activity_id: 'control-pending:1' }, { activity_id: 'splice:connection-1' }])
|
|
expect(await cellReservations(delegate)).toEqual({ 'cell-a': 2, 'cell-b': 0 })
|
|
})
|
|
|
|
it('extends the lease of a control-holding host on a sticky grant', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
|
|
now = 40_000
|
|
await expect(store.assign(identity)).resolves.toMatchObject({
|
|
cellId: assignment.cellId
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT lease_expires_at, last_activity_at FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ lease_expires_at: now + ASSIGNMENT_LIMITS.activityLeaseMs, last_activity_at: now }
|
|
])
|
|
})
|
|
|
|
// The old absolute write shortened a 15-minute migration lease to the 90s
|
|
// grant lease; only the touch's monotonic CASE keeps the longer one now.
|
|
it('never shortens a migration lease to the grant lease', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: assignment.cellId,
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const before = await database!.query(
|
|
`SELECT lease_expires_at FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
expect(before).toEqual([{ lease_expires_at: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs }])
|
|
|
|
now = 1000
|
|
await expect(store.assign(identity)).resolves.toMatchObject({ cellId: 'cell-b' })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT lease_expires_at, last_activity_at FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ lease_expires_at: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs, last_activity_at: now }
|
|
])
|
|
})
|
|
|
|
it('moves an origin-scoped activity reservation without double counting', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.acquireActivity(identity, {
|
|
activityId: 'splice:connection-1',
|
|
kind: 'splice',
|
|
cellId: 'cell-a'
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
await store.acquireActivity(identity, {
|
|
activityId: 'splice:connection-1',
|
|
kind: 'splice',
|
|
cellId: 'cell-b'
|
|
})
|
|
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 6 })
|
|
await expect(store.releaseActivity(identity, 'splice:connection-1')).resolves.toBe(true)
|
|
await expect(store.releaseActivity(identity, 'splice:connection-1')).resolves.toBe(false)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 4 })
|
|
})
|
|
|
|
it('rejects a durable activity before it can exceed cell capacity', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 2 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
|
|
await expect(
|
|
store.acquireActivity(identity, {
|
|
activityId: 'splice:connection-1',
|
|
kind: 'splice',
|
|
cellId: 'cell-a'
|
|
})
|
|
).rejects.toThrow('relay_capacity_exhausted')
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1 })
|
|
})
|
|
|
|
it('moves active assignments target-first and completes only after source drain', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
expect(await store.startEvacuation(identity, 'cell-b')).toEqual(migration)
|
|
expect(migration).toMatchObject({
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-b',
|
|
previousEpoch: 1,
|
|
assignmentEpoch: 2
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 2 })
|
|
|
|
const targetControl = await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, { cellId: 'cell-b', assignmentEpoch: 2 })
|
|
await expect(store.completeEvacuation(identity, 2)).rejects.toThrow(
|
|
'migration_source_still_active'
|
|
)
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await store.completeEvacuation(identity, 2)
|
|
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 1 })
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-b', assignmentEpoch: 2 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases ORDER BY activity_id`
|
|
)
|
|
).toEqual([{ activity_id: targetControl }])
|
|
await expect(
|
|
store.acquireActivity(identity, {
|
|
activityId: 'splice:late-source',
|
|
kind: 'splice',
|
|
cellId: 'cell-a'
|
|
})
|
|
).rejects.toThrow('activity_cell_not_authoritative')
|
|
})
|
|
|
|
it('automatically completes only after the source runtime is freshly quiescent', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await store.setCellEnabled('cell-a', false)
|
|
await heartbeat(store, cells[0]!, { observedRequests: 1 })
|
|
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
await heartbeat(store, cells[0]!, { observedRequests: 0 })
|
|
now = 150
|
|
await heartbeat(store, cells[1]!, {
|
|
incarnation: '22222222-2222-4222-8222-222222222222',
|
|
startedAt: 125
|
|
})
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 2
|
|
})
|
|
expect(await store.completeReadyEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('completes a fenced migration only after the source heartbeat is stale', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!, { observedRequests: 1 })
|
|
await heartbeat(store, cells[1]!)
|
|
await store.setCellEnabled('cell-b', false)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-b', true)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await store.setCellEnabled('cell-a', false)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toMatchObject({
|
|
inProgress: 1,
|
|
blocked: 1
|
|
})
|
|
now += 45_001
|
|
await heartbeat(store, cells[1]!)
|
|
await store.attestCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toMatchObject({
|
|
inProgress: 0,
|
|
completed: 1
|
|
})
|
|
})
|
|
|
|
it('blocks aggregate fenced completion on assignment accounting mismatch', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
await store.setCellEnabled('cell-b', false)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-b', true)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await store.setCellEnabled('cell-a', false)
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET reserved_splices = reserved_splices + 1
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
now += 45_001
|
|
await heartbeat(store, cells[1]!)
|
|
await store.attestCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toMatchObject({
|
|
inProgress: 1,
|
|
blocked: 1
|
|
})
|
|
})
|
|
|
|
it('blocks aggregate fenced completion on an unexpected third-cell lease', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
await store.setCellEnabled('cell-b', false)
|
|
await store.setCellEnabled('cell-c', false)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-b', true)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await store.setCellEnabled('cell-a', false)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
'splice:unexpected-cell',
|
|
'splice',
|
|
'cell-c',
|
|
2,
|
|
now + ASSIGNMENT_LIMITS.activityLeaseMs,
|
|
now
|
|
]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET reserved_splices = reserved_splices + 1
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = reserved_requests + 2 WHERE cell_id = ?`,
|
|
['cell-c']
|
|
)
|
|
now += 45_001
|
|
await heartbeat(store, cells[1]!)
|
|
await store.attestCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toMatchObject({
|
|
inProgress: 1,
|
|
blocked: 1
|
|
})
|
|
})
|
|
|
|
it('rolls back an unregistered expired evacuation with a strictly newer epoch', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-a', assignmentEpoch: 3 })
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 0 })
|
|
})
|
|
|
|
// Why: completion needs an enabled target and expiry rollback needs an unregistered one, so a
|
|
// target disabled after it registered satisfies neither and the migration never leaves the table.
|
|
async function wedgeRegisteredMigration(
|
|
now: () => number,
|
|
cells: RelayCellConfig[],
|
|
disableTarget = true,
|
|
disableSource = true,
|
|
releaseSourceControl = true
|
|
): Promise<{
|
|
store: RelayAssignmentStore
|
|
identity: { userId: string; relayHostId: string }
|
|
}> {
|
|
const store = await setupWithHeartbeats(now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
const targetControl = await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
if (releaseSourceControl) await store.releaseActivity(identity, sourceControl)
|
|
if (disableSource) await store.setCellEnabled('cell-a', false)
|
|
// The desktop leaves the half-migrated target, then an operator disables that cell.
|
|
await store.releaseActivity(identity, targetControl)
|
|
if (disableTarget) await store.setCellEnabled('cell-b', false)
|
|
return { store, identity }
|
|
}
|
|
|
|
async function insertReservedControlConnection(
|
|
identity: { userId: string; relayHostId: string },
|
|
cellId: string,
|
|
assignmentEpoch: number,
|
|
now: number
|
|
): Promise<void> {
|
|
await database!.query(
|
|
`INSERT INTO relay_control_connection_reservations
|
|
(reservation_id, idempotency_key, user_id, relay_host_id, assignment_epoch,
|
|
cell_id, state, created_at, timeout_at, updated_at)
|
|
VALUES ('abandoned-reservation', 'abandoned-key', ?, ?, ?, ?, 'reserved', ?, ?, ?)`,
|
|
[identity.userId, identity.relayHostId, assignmentEpoch, cellId, now, now + 60_000, now]
|
|
)
|
|
}
|
|
|
|
it('reaps an expired migration whose registered target cell was disabled', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(() => now, cells)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
// A disabled target can never satisfy completion, so waiting cannot help.
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
// Rollback lands on the disabled source, so the host resolves to nothing and is placed afresh
|
|
// by evacuateDeadCells; the point is that the row is retired rather than reaped every tick.
|
|
expect(await store.resolve(identity)).toBeNull()
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
})
|
|
|
|
it('leaves a freshly disabled target for supersede-target to recover', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells)
|
|
|
|
// Expired, but a disabled target is what supersede-target itself creates, so the operator
|
|
// window must stay open; aborting here would fail their run with migration_already_superseded.
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
|
|
now += STRANDED_MIGRATION_ABANDON_MS
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('rolls an abandoned disabled target back while its source remains active', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(
|
|
() => now,
|
|
cells,
|
|
true,
|
|
false,
|
|
false
|
|
)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id, assignment_epoch FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ cell_id: 'cell-a', assignment_epoch: 3 }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT completed_at, aborted_at FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ completed_at: null, aborted_at: now }])
|
|
})
|
|
|
|
it('starts the abandon window when an old migration target is disabled', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells, false, false)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
|
|
await store.setCellEnabled('cell-b', false)
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
|
|
now += STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('reaps an expired migration from a retired source when its target control is gone', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(() => now, cells, false)
|
|
await applyGenerationZeroSelector(store, {
|
|
attemptId: 'retired_source_migration_target',
|
|
membership: {
|
|
existingOnly: ['cell-a'],
|
|
migrationOnly: ['cell-b'],
|
|
general: []
|
|
}
|
|
})
|
|
await insertReservedControlConnection(identity, 'cell-b', 2, now)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT assignment.cell_id, assignment.assignment_epoch,
|
|
migration.completed_at, migration.aborted_at
|
|
FROM relay_assignments assignment
|
|
JOIN relay_assignment_migrations migration
|
|
ON migration.user_id = assignment.user_id
|
|
AND migration.relay_host_id = assignment.relay_host_id
|
|
WHERE assignment.user_id = ? AND assignment.relay_host_id = ?
|
|
AND migration.assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, 2]
|
|
)
|
|
).toEqual([
|
|
{
|
|
cell_id: 'cell-b',
|
|
assignment_epoch: 2,
|
|
completed_at: now,
|
|
aborted_at: null
|
|
}
|
|
])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls, migration_leases FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 0, migration_leases: 0 }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([])
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT state FROM relay_control_connection_reservations
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ state: 'released' }])
|
|
})
|
|
|
|
it('reaps a retired-side migration with only a pending target control', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await applyGenerationZeroSelector(store, {
|
|
attemptId: 'retired_source_pending_target',
|
|
membership: {
|
|
existingOnly: ['cell-a'],
|
|
migrationOnly: ['cell-b'],
|
|
general: []
|
|
}
|
|
})
|
|
await insertReservedControlConnection(identity, 'cell-b', migration.assignmentEpoch, now)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
await database!.query(
|
|
`UPDATE relay_assignment_activity_leases SET expires_at = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND activity_id = ?`,
|
|
[now, identity.userId, identity.relayHostId, `control-pending:${migration.assignmentEpoch}`]
|
|
)
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id, assignment_epoch FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ cell_id: 'cell-b', assignment_epoch: migration.assignmentEpoch }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls, migration_leases FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ reserved_controls: 0, migration_leases: 0 }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([])
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT state FROM relay_control_connection_reservations
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ state: 'released' }])
|
|
})
|
|
|
|
it('preserves a fresh target grant created after abandoned migration selection', async () => {
|
|
let now = 100
|
|
const cells: RelayCellConfig[] = [
|
|
{
|
|
id: 'cell-a',
|
|
url: 'https://relay-a.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600,
|
|
connectionUnobservedBound: 50
|
|
},
|
|
{
|
|
id: 'cell-b',
|
|
url: 'https://relay-b.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600,
|
|
connectionUnobservedBound: 50
|
|
}
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(() => now, cells, false)
|
|
await applyGenerationZeroSelector(store, {
|
|
attemptId: 'retired_source_fresh_target_grant',
|
|
membership: {
|
|
existingOnly: ['cell-a'],
|
|
migrationOnly: ['cell-b'],
|
|
general: []
|
|
}
|
|
})
|
|
|
|
now += 45_001
|
|
await heartbeat(store, cells[1]!)
|
|
await store.adoptLegacyCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
await store.commitLegacyCellFenceAdoption(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
await heartbeat(store, cells[1]!)
|
|
const query = database!.query.bind(database)
|
|
let grant: Awaited<ReturnType<typeof store.assign>> | undefined
|
|
vi.spyOn(database!, 'query').mockImplementationOnce(async (sql, params) => {
|
|
const rows = await query(sql, params)
|
|
grant = await store.assign(identity)
|
|
return rows
|
|
})
|
|
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
expect(grant).toMatchObject({ cellId: 'cell-b', assignmentEpoch: 2 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id, expires_at FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ? AND activity_id = ?`,
|
|
[identity.userId, identity.relayHostId, 'control-pending:2']
|
|
)
|
|
).toEqual([{ activity_id: 'control-pending:2', expires_at: grant!.leaseExpiresAt }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT state FROM relay_control_connection_reservations
|
|
WHERE user_id = ? AND relay_host_id = ? AND assignment_epoch = ?
|
|
AND cell_id = ? AND state = 'reserved'`,
|
|
[identity.userId, identity.relayHostId, 2, 'cell-b']
|
|
)
|
|
).toEqual([{ state: 'reserved' }])
|
|
|
|
await store.activateControl(identity, {
|
|
cellId: grant!.cellId,
|
|
assignmentEpoch: grant!.assignmentEpoch,
|
|
generation: 2
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ? AND activity_kind = 'control'`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ activity_id: 'control:cell-b:2' }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT completed_at FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ? AND assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, 2]
|
|
)
|
|
).toEqual([{ completed_at: null }])
|
|
})
|
|
|
|
it('keeps an abandoned target migration open while its source is still active', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(
|
|
() => now,
|
|
cells,
|
|
false,
|
|
true,
|
|
false
|
|
)
|
|
await applyGenerationZeroSelector(store, {
|
|
attemptId: 'retired_source_active',
|
|
membership: {
|
|
existingOnly: ['cell-a'],
|
|
migrationOnly: ['cell-b'],
|
|
general: []
|
|
}
|
|
})
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
expect(
|
|
await database!.query(
|
|
`SELECT completed_at, aborted_at FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ completed_at: null, aborted_at: null }])
|
|
})
|
|
|
|
it('starts the abandon window when an old migration source is retired', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells, false, false)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
|
|
await store.setCellEnabled('cell-a', false)
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
|
|
now += STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('reaps after an old source migration lease is refreshed and expires', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells, false)
|
|
await applyGenerationZeroSelector(store, {
|
|
attemptId: 'retired_source_refreshed_lease',
|
|
membership: {
|
|
existingOnly: ['cell-a'],
|
|
migrationOnly: ['cell-b'],
|
|
general: []
|
|
}
|
|
})
|
|
now += STRANDED_MIGRATION_ABANDON_MS + ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
await database!.query(
|
|
`UPDATE relay_assignment_migrations SET expires_at = ?`,
|
|
[now - 1]
|
|
)
|
|
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('keeps a freshly retired target open despite an old retired source', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells, false)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
|
|
await store.setCellEnabled('cell-b', false)
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
|
|
now += STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
})
|
|
|
|
it('fails supersession closed when reaping wins after selection', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
const query = database!.query.bind(database)
|
|
vi.spyOn(database!, 'query').mockImplementationOnce(async (sql, params) => {
|
|
const rows = await query(sql, params)
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
return rows
|
|
})
|
|
|
|
await expect(
|
|
store.supersedeRegisteredCellEvacuations('cell-a', 'cell-b', 'cell-c', 100)
|
|
).rejects.toThrow('migration_already_superseded')
|
|
})
|
|
|
|
it('fails supersession closed when reaping wins after preparation', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
await store.attestCellFence('cell-b', '11111111-1111-4111-8111-111111111111')
|
|
const supersede = store.supersedeRegisteredEvacuation.bind(store)
|
|
vi.spyOn(store, 'supersedeRegisteredEvacuation').mockImplementationOnce(async (...args) => {
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
return await supersede(...args)
|
|
})
|
|
|
|
await expect(
|
|
store.supersedeRegisteredCellEvacuations('cell-a', 'cell-b', 'cell-c', 100)
|
|
).rejects.toThrow('migration_already_superseded')
|
|
})
|
|
|
|
it('fails supersession closed when reaping wins before an accounting retry', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store } = await wedgeRegisteredMigration(() => now, cells)
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[2]!)
|
|
await store.attestCellFence('cell-b', '11111111-1111-4111-8111-111111111111')
|
|
await database!.query(`UPDATE relay_cells SET reserved_requests = 1 WHERE cell_id = ?`, [
|
|
'cell-c'
|
|
])
|
|
const supersede = store.supersedeRegisteredEvacuation.bind(store)
|
|
let attempts = 0
|
|
vi.spyOn(store, 'supersedeRegisteredEvacuation').mockImplementation(async (...args) => {
|
|
attempts++
|
|
if (attempts === 2) expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
return await supersede(...args)
|
|
})
|
|
|
|
await expect(
|
|
store.supersedeRegisteredCellEvacuations('cell-a', 'cell-b', 'cell-c', 100)
|
|
).rejects.toThrow('migration_already_superseded')
|
|
expect(attempts).toBe(2)
|
|
})
|
|
|
|
it('reaps a pinned expired migration once its target cell is disabled', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
]
|
|
const { store, identity } = await wedgeRegisteredMigration(() => now, cells)
|
|
// Drains pin their migrations and nothing ever deletes a pin, so the pin outlives the drain.
|
|
await database!.query(
|
|
`INSERT INTO relay_post_drain_migration_pins
|
|
(user_id, relay_host_id, assignment_epoch, drain_attempt_id, source_cell_id,
|
|
source_cell_incarnation, target_cell_id, target_cell_incarnation,
|
|
source_request_units, target_reserved_units, pinned_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
2,
|
|
'attempt-0000',
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111',
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111',
|
|
1,
|
|
1,
|
|
now
|
|
]
|
|
)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + STRANDED_MIGRATION_ABANDON_MS + 1
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
})
|
|
|
|
it('repairs an expired migration marker from its exact active target control', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
expect(await store.abortExpiredEvacuations()).toBe(0)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toEqual({
|
|
inProgress: 1,
|
|
oldestExpiresAt: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
oldestRemainingMs: -1,
|
|
targetRegistered: 1,
|
|
registeredSourceActive: 1,
|
|
registeredCompletable: 0,
|
|
registeredTargetInactive: 0,
|
|
completed: 0,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toEqual({
|
|
inProgress: 0,
|
|
...NO_ACTIVE_MIGRATION_LEASE,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 1,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-b', assignmentEpoch: 2 })
|
|
})
|
|
|
|
it('keeps a registered migration pending while its proven target control is offline', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
const targetControl = await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 2
|
|
})
|
|
expect(
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
).toBe(true)
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
await store.releaseActivity(identity, sourceControl)
|
|
now += ASSIGNMENT_LIMITS.activityLeaseMs + 1
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
await expect(
|
|
store.completeEvacuation(identity, migration.assignmentEpoch)
|
|
).rejects.toThrow('migration_target_not_active')
|
|
await store.releaseActivity(identity, targetControl)
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toEqual({
|
|
inProgress: 1,
|
|
oldestExpiresAt: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
oldestRemainingMs:
|
|
ASSIGNMENT_LIMITS.migrationLeaseMs - ASSIGNMENT_LIMITS.activityLeaseMs - 1,
|
|
targetRegistered: 1,
|
|
registeredSourceActive: 0,
|
|
registeredCompletable: 0,
|
|
registeredTargetInactive: 1,
|
|
completed: 0,
|
|
blocked: 1,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 3
|
|
})
|
|
expect(await store.completeReadyEvacuations()).toBe(1)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toEqual({
|
|
inProgress: 0,
|
|
...NO_ACTIVE_MIGRATION_LEASE,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 0,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 1 })
|
|
})
|
|
|
|
it('completes a registered migration after its disabled source is proven dead', async () => {
|
|
let now = 100
|
|
const store = await setupWithHeartbeats(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
await heartbeat(store, { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 })
|
|
await heartbeat(store, { id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 })
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
now += 45_001
|
|
await heartbeat(
|
|
store,
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ startedAt: 50 }
|
|
)
|
|
await store.attestCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
|
|
const input = {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-b'
|
|
}
|
|
await expect(store.completeEvacuationFromDeadSource(identity, input)).resolves.toEqual({
|
|
changed: true,
|
|
assignmentEpoch: 2,
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-b'
|
|
})
|
|
await expect(store.completeEvacuationFromDeadSource(identity, input)).resolves.toEqual({
|
|
changed: false,
|
|
assignmentEpoch: 2,
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-b'
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 1 })
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 0
|
|
})
|
|
})
|
|
|
|
it('retires an inactive registered migration after its source is proven dead', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{
|
|
id: 'cell-a',
|
|
url: 'https://relay-a.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
},
|
|
{
|
|
id: 'cell-b',
|
|
url: 'https://relay-b.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
}
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
now += 45_001
|
|
await heartbeat(store, cells[1]!, { startedAt: 50 })
|
|
await store.attestCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
|
|
await expect(
|
|
store.cellEvacuationStatus('cell-a', 'cell-b', true)
|
|
).resolves.toMatchObject({ inProgress: 0, completed: 1, blocked: 0 })
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT state FROM relay_control_connection_reservations
|
|
WHERE user_id = ? AND relay_host_id = ? AND cell_id = ? AND assignment_epoch = ?`,
|
|
[identity.userId, identity.relayHostId, 'cell-b', migration.assignmentEpoch]
|
|
)
|
|
).toEqual([{ state: 'released' }])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id, assignment_epoch, reserved_controls, migration_leases
|
|
FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{
|
|
cell_id: 'cell-b',
|
|
assignment_epoch: 2,
|
|
reserved_controls: 0,
|
|
migration_leases: 0
|
|
}
|
|
])
|
|
})
|
|
|
|
it.each(['expired', 'previous-incarnation'] as const)(
|
|
'retires a registered migration and its %s target control after the source dies',
|
|
async (targetControlState) => {
|
|
let now = 100
|
|
const cells = [
|
|
{
|
|
id: 'cell-a',
|
|
url: 'https://relay-a.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
},
|
|
{
|
|
id: 'cell-b',
|
|
url: 'https://relay-b.example.com',
|
|
capacityRequests: 10,
|
|
connectionHardCap: 600 as const,
|
|
connectionUnobservedBound: 50
|
|
}
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
await heartbeat(store, cells[0]!)
|
|
await heartbeat(store, cells[1]!)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
now +=
|
|
targetControlState === 'expired'
|
|
? ASSIGNMENT_LIMITS.activityLeaseMs + 1
|
|
: 45_001
|
|
await heartbeat(store, cells[1]!, {
|
|
startedAt: targetControlState === 'previous-incarnation' ? now - 1 : 50,
|
|
incarnation:
|
|
targetControlState === 'previous-incarnation'
|
|
? '22222222-2222-4222-8222-222222222222'
|
|
: '11111111-1111-4111-8111-111111111111'
|
|
})
|
|
await store.attestCellFence(
|
|
'cell-a',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
|
|
await expect(
|
|
store.cellEvacuationStatus('cell-a', 'cell-b', false)
|
|
).resolves.toMatchObject({
|
|
inProgress: 1,
|
|
registeredCompletable: 0,
|
|
registeredTargetInactive: 1
|
|
})
|
|
await expect(
|
|
store.cellEvacuationStatus('cell-a', 'cell-b', true)
|
|
).resolves.toMatchObject({ inProgress: 0, completed: 1, blocked: 0 })
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id, assignment_epoch, reserved_controls, migration_leases
|
|
FROM relay_assignments WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{
|
|
cell_id: 'cell-b',
|
|
assignment_epoch: migration.assignmentEpoch,
|
|
reserved_controls: 0,
|
|
migration_leases: 0
|
|
}
|
|
])
|
|
}
|
|
)
|
|
|
|
it('refuses dead-source completion while the source heartbeat is fresh', async () => {
|
|
const store = await setupWithHeartbeats(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
await heartbeat(store, { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 })
|
|
await heartbeat(store, { id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 })
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
|
|
await expect(
|
|
store.attestCellFence('cell-a', '11111111-1111-4111-8111-111111111111')
|
|
).rejects.toThrow('cell_fence_runtime_not_stale')
|
|
await expect(
|
|
store.completeEvacuationFromDeadSource(identity, {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-b'
|
|
})
|
|
).rejects.toThrow('cell_fence_attestation_missing')
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 2 })
|
|
})
|
|
|
|
it('supersedes a registered migration after its disabled target is proven unavailable', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await database!.query(
|
|
`INSERT INTO relay_control_connection_reservations
|
|
(reservation_id, idempotency_key, user_id, relay_host_id,
|
|
assignment_epoch, cell_id, state, inclusion_watermark,
|
|
claim_activity_id, created_at, timeout_at, claimed_at, released_at,
|
|
updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'late-arrival-debt', NULL, NULL, ?, ?, NULL, NULL, ?)`,
|
|
[
|
|
'superseded-reservation',
|
|
'superseded-reservation',
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
migration.assignmentEpoch,
|
|
'cell-b',
|
|
100,
|
|
100,
|
|
100
|
|
]
|
|
)
|
|
await store.setCellEnabled('cell-b', false)
|
|
now += 45_001
|
|
await heartbeat(store, cells[0]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[2]!, { startedAt: 50 })
|
|
await store.attestCellFence(
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
const input = {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
currentTargetCellId: 'cell-b',
|
|
replacementTargetCellId: 'cell-c'
|
|
}
|
|
|
|
const replacement = await store.supersedeRegisteredEvacuation(identity, input)
|
|
expect(replacement).toMatchObject({
|
|
sourceCellId: 'cell-a',
|
|
targetCellId: 'cell-c',
|
|
previousEpoch: 2,
|
|
assignmentEpoch: 3
|
|
})
|
|
await expect(store.supersedeRegisteredEvacuation(identity, input)).resolves.toEqual(replacement)
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-c', assignmentEpoch: 3 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT state FROM relay_control_connection_reservations
|
|
WHERE reservation_id = ?`,
|
|
['superseded-reservation']
|
|
)
|
|
).toEqual([{ state: 'released' }])
|
|
expect(await cellReservations(database!)).toEqual({
|
|
'cell-a': 1,
|
|
'cell-b': 0,
|
|
'cell-c': 2
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_kind, cell_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ? ORDER BY cell_id, activity_id`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ activity_kind: 'control', cell_id: 'cell-a' },
|
|
{ activity_kind: 'control', cell_id: 'cell-c' },
|
|
{ activity_kind: 'migration', cell_id: 'cell-c' }
|
|
])
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 0
|
|
})
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-c', false)).toMatchObject({
|
|
inProgress: 1
|
|
})
|
|
})
|
|
|
|
it('retries registered migration supersession with inventory locked first', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const delegate = await openInMemoryRelayDatabase()
|
|
const retryDatabase = new OneShotInventoryFailureDatabase(delegate)
|
|
database = retryDatabase
|
|
const store = new RelayAssignmentStore(retryDatabase, () => now, {
|
|
requireLiveCells: true,
|
|
heartbeatTtlMs: 45_000
|
|
})
|
|
await store.reconcileCells(cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.setCellEnabled('cell-b', false)
|
|
now += 45_001
|
|
await heartbeat(store, cells[0]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[2]!, { startedAt: 50 })
|
|
await store.attestCellFence(
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
retryDatabase.arm()
|
|
|
|
await expect(
|
|
store.supersedeRegisteredEvacuation(identity, {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
currentTargetCellId: 'cell-b',
|
|
replacementTargetCellId: 'cell-c'
|
|
})
|
|
).resolves.toMatchObject({ targetCellId: 'cell-c', assignmentEpoch: 3 })
|
|
expect(retryDatabase.retryLocks).toEqual([
|
|
'inventory-nowait-failed',
|
|
'inventory-first',
|
|
'assignment-nowait'
|
|
])
|
|
})
|
|
|
|
it('reconciles durable cell accounting once before aggregate supersession', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.setCellEnabled('cell-b', false)
|
|
now += 45_001
|
|
await heartbeat(store, cells[0]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[2]!, { startedAt: 50 })
|
|
await store.attestCellFence(
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells
|
|
SET reserved_requests = CASE WHEN cell_id = ? THEN 1 ELSE 0 END
|
|
WHERE cell_id IN (?, ?)`,
|
|
['cell-c', 'cell-a', 'cell-c']
|
|
)
|
|
|
|
await expect(
|
|
store.supersedeRegisteredCellEvacuations('cell-a', 'cell-b', 'cell-c', 100)
|
|
).resolves.toBe(1)
|
|
expect(await cellReservations(database!)).toEqual({
|
|
'cell-a': 1,
|
|
'cell-b': 0,
|
|
'cell-c': 2
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT assignment_epoch, target_cell_id, aborted_at
|
|
FROM relay_assignment_migrations
|
|
WHERE user_id = ? ORDER BY assignment_epoch`,
|
|
[identity.userId]
|
|
)
|
|
).toEqual([
|
|
{ assignment_epoch: 2, target_cell_id: 'cell-b', aborted_at: 45_101 },
|
|
{ assignment_epoch: 3, target_cell_id: 'cell-c', aborted_at: null }
|
|
])
|
|
})
|
|
|
|
it('preserves healthy third-cell activity during aggregate supersession', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-d', url: 'https://relay-d.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await database!.query(
|
|
`UPDATE relay_assignment_activity_leases SET cell_id = ?
|
|
WHERE user_id = ? AND relay_host_id = ? AND activity_id = ?`,
|
|
['cell-d', identity.userId, identity.relayHostId, sourceControl]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = ? WHERE cell_id = ?`,
|
|
[5, 'cell-c']
|
|
)
|
|
await store.setCellEnabled('cell-b', false)
|
|
now += 45_001
|
|
await heartbeat(store, cells[0]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[2]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[3]!, { startedAt: 50 })
|
|
await store.attestCellFence(
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
|
|
await expect(
|
|
store.supersedeRegisteredCellEvacuations('cell-a', 'cell-b', 'cell-c', 100)
|
|
).resolves.toBe(1)
|
|
expect(await cellReservations(database!)).toEqual({
|
|
'cell-a': 0,
|
|
'cell-b': 0,
|
|
'cell-c': 1,
|
|
'cell-d': 1
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_kind, cell_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ? ORDER BY cell_id, activity_id`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ activity_kind: 'control', cell_id: 'cell-c' },
|
|
{ activity_kind: 'migration', cell_id: 'cell-c' },
|
|
{ activity_kind: 'control', cell_id: 'cell-d' }
|
|
])
|
|
})
|
|
|
|
it('refuses supersession while the registered target remains available', async () => {
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => 100, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.setCellEnabled('cell-b', false)
|
|
|
|
await expect(
|
|
store.attestCellFence('cell-b', '11111111-1111-4111-8111-111111111111')
|
|
).rejects.toThrow('cell_fence_runtime_not_stale')
|
|
await expect(
|
|
store.supersedeRegisteredEvacuation(identity, {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
currentTargetCellId: 'cell-b',
|
|
replacementTargetCellId: 'cell-c'
|
|
})
|
|
).rejects.toThrow('cell_fence_attestation_missing')
|
|
expect(
|
|
await database!.query(
|
|
`SELECT cell_id, assignment_epoch FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ cell_id: 'cell-b', assignment_epoch: 2 }])
|
|
})
|
|
|
|
it('serializes concurrent retries of registered migration supersession', async () => {
|
|
let now = 100
|
|
const cells = [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 10 }
|
|
]
|
|
const store = await setupWithHeartbeats(() => now, cells)
|
|
for (const cell of cells) await heartbeat(store, cell)
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.setCellEnabled('cell-a', false)
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.setCellEnabled('cell-b', false)
|
|
now += 45_001
|
|
await heartbeat(store, cells[0]!, { startedAt: 50 })
|
|
await heartbeat(store, cells[2]!, { startedAt: 50 })
|
|
await store.attestCellFence(
|
|
'cell-b',
|
|
'11111111-1111-4111-8111-111111111111'
|
|
)
|
|
const input = {
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
sourceCellId: 'cell-a',
|
|
currentTargetCellId: 'cell-b',
|
|
replacementTargetCellId: 'cell-c'
|
|
}
|
|
|
|
const results = await Promise.all([
|
|
store.supersedeRegisteredEvacuation(identity, input),
|
|
store.supersedeRegisteredEvacuation(identity, input)
|
|
])
|
|
expect(results[0]).toEqual(results[1])
|
|
expect(
|
|
await database!.query(
|
|
`SELECT assignment_epoch FROM relay_assignment_migrations
|
|
WHERE user_id = ? AND relay_host_id = ? ORDER BY assignment_epoch`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([{ assignment_epoch: 2 }, { assignment_epoch: 3 }])
|
|
expect(await cellReservations(database!)).toEqual({
|
|
'cell-a': 1,
|
|
'cell-b': 0,
|
|
'cell-c': 2
|
|
})
|
|
})
|
|
|
|
it('classifies an expired migration blocked by a newer target assignment', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET assignment_epoch = ?
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[3, identity.userId, identity.relayHostId]
|
|
)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toEqual({
|
|
inProgress: 1,
|
|
oldestExpiresAt: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
oldestRemainingMs: -1,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 0,
|
|
blocked: 0,
|
|
expiredUnregistered: 1,
|
|
repairableExpiredUnregistered: 0,
|
|
abortableExpiredUnregistered: 0,
|
|
blockedExpiredUnregistered: 1,
|
|
blockedExpiredOnNewerTargetAssignment: 1
|
|
})
|
|
})
|
|
|
|
it('does not complete a registered migration after the assignment epoch advances', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const migration = await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: migration.assignmentEpoch
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET assignment_epoch = ?
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[migration.assignmentEpoch + 1, identity.userId, identity.relayHostId]
|
|
)
|
|
|
|
expect(await store.completeReadyEvacuations()).toBe(0)
|
|
await expect(
|
|
store.completeEvacuation(identity, migration.assignmentEpoch)
|
|
).rejects.toThrow('migration_assignment_mismatch')
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 1,
|
|
targetRegistered: 1
|
|
})
|
|
})
|
|
|
|
it('retires an expired migration without rewriting its newer target assignment', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET assignment_epoch = ?
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[3, identity.userId, identity.relayHostId]
|
|
)
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
expect(await store.abortExpiredEvacuations()).toBe(1)
|
|
expect(await store.resolve(identity)).toMatchObject({
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 3
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT activity_id FROM relay_assignment_activity_leases
|
|
WHERE user_id = ? AND relay_host_id = ? ORDER BY activity_id`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{ activity_id: 'control:cell-a:1' },
|
|
{ activity_id: 'control:cell-b:1' }
|
|
])
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 0
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
{ assignmentEpoch: 1, removeAssignment: false, expected: 'migration_assignment_mismatch' },
|
|
{ assignmentEpoch: 2, removeAssignment: true, expected: 'migration_assignment_missing' }
|
|
])(
|
|
'fails closed when expired migration assignment state is not superseding: $expected',
|
|
async ({ assignmentEpoch, removeAssignment, expected }) => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
if (removeAssignment) {
|
|
await database!.query(
|
|
`DELETE FROM relay_assignments WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
} else {
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET assignment_epoch = ?
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
[assignmentEpoch, identity.userId, identity.relayHostId]
|
|
)
|
|
}
|
|
|
|
now += ASSIGNMENT_LIMITS.migrationLeaseMs + 1
|
|
await expect(store.abortExpiredEvacuations()).rejects.toThrow(expected)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 1,
|
|
targetRegistered: 0
|
|
})
|
|
}
|
|
)
|
|
|
|
it('reconciles duplicated assignment counters and cell reservations after evacuation', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const sourceControl = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.startEvacuation(identity, 'cell-b')
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2,
|
|
generation: 1
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2
|
|
})
|
|
await store.releaseActivity(identity, sourceControl)
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET reserved_controls = 9, reserved_splices = 4,
|
|
reserved_invites = 3, pending_installs = 2, pending_confirmations = 2,
|
|
migration_leases = 7 WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests =
|
|
CASE WHEN cell_id = ? THEN 11 ELSE 3 END`,
|
|
['cell-a']
|
|
)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toEqual({
|
|
inProgress: 0,
|
|
...NO_ACTIVE_MIGRATION_LEASE,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 1,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 1 })
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases
|
|
FROM relay_assignments WHERE user_id = ? AND relay_host_id = ?`,
|
|
[identity.userId, identity.relayHostId]
|
|
)
|
|
).toEqual([
|
|
{
|
|
reserved_controls: 1,
|
|
reserved_splices: 0,
|
|
reserved_invites: 0,
|
|
pending_installs: 0,
|
|
pending_confirmations: 0,
|
|
migration_leases: 0
|
|
}
|
|
])
|
|
})
|
|
|
|
it('refuses reservation reconciliation when an activity lease has no assignment', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
['orphan-user', 'orphanhost000001', 'control:orphan', 'control', 'cell-a', 1, 200, 100]
|
|
)
|
|
|
|
await expect(store.cellEvacuationStatus('cell-a', 'cell-b', true)).rejects.toThrow(
|
|
'activity_lease_assignment_missing'
|
|
)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 0 })
|
|
})
|
|
|
|
it('refuses reservation reconciliation when an activity lease names no cell', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
await store.assign(identity)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
identity.userId,
|
|
identity.relayHostId,
|
|
'splice:missing-cell',
|
|
'splice',
|
|
'missing-cell',
|
|
2,
|
|
200,
|
|
100
|
|
]
|
|
)
|
|
|
|
await expect(store.cellEvacuationStatus('cell-a', 'cell-b', true)).rejects.toThrow(
|
|
'activity_lease_cell_missing'
|
|
)
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 1, 'cell-b': 0 })
|
|
})
|
|
|
|
it('leaves accounting for cells outside the selected evacuation pair unchanged', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 20 }
|
|
])
|
|
await database!.query(
|
|
`INSERT INTO relay_assignments
|
|
(user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at,
|
|
last_activity_at, reserved_controls, reserved_splices, reserved_invites,
|
|
pending_installs, pending_confirmations, migration_leases)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
['unrelated-user', 'unrelatedhost001', 'cell-c', 1, 200, 100, 9, 0, 0, 0, 0, 0]
|
|
)
|
|
await database!.query(
|
|
`INSERT INTO relay_assignment_activity_leases
|
|
(user_id, relay_host_id, activity_id, activity_kind, cell_id,
|
|
request_units, expires_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
'unrelated-user',
|
|
'unrelatedhost001',
|
|
'control:unrelated',
|
|
'control',
|
|
'cell-c',
|
|
1,
|
|
200,
|
|
100
|
|
]
|
|
)
|
|
await database!.query(
|
|
`UPDATE relay_cells SET reserved_requests = 9 WHERE cell_id = ?`,
|
|
['cell-c']
|
|
)
|
|
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toMatchObject({
|
|
inProgress: 0
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({
|
|
'cell-a': 0,
|
|
'cell-b': 0,
|
|
'cell-c': 9
|
|
})
|
|
expect(
|
|
await database!.query(
|
|
`SELECT reserved_controls FROM relay_assignments
|
|
WHERE user_id = ? AND relay_host_id = ?`,
|
|
['unrelated-user', 'unrelatedhost001']
|
|
)
|
|
).toEqual([{ reserved_controls: 9 }])
|
|
})
|
|
|
|
it('rebalances only a fully inactive assignment after the dormant TTL', async () => {
|
|
let now = 100
|
|
const store = await setup(() => now, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const first = await store.assign(identity)
|
|
const control = await store.activateControl(identity, {
|
|
cellId: first.cellId,
|
|
assignmentEpoch: first.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await expect(store.rebalanceDormant(identity, 'cell-b')).rejects.toThrow('assignment_active')
|
|
await store.releaseActivity(identity, control)
|
|
now += ASSIGNMENT_LIMITS.dormantTtlMs + 1
|
|
|
|
await expect(store.rebalanceDormant(identity, 'cell-b')).resolves.toMatchObject({
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: 2
|
|
})
|
|
expect(await cellReservations(database!)).toEqual({ 'cell-a': 0, 'cell-b': 1 })
|
|
})
|
|
|
|
it('durably removes an unhealthy cell from new assignment without moving existing hosts', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
const existing = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
expect(await store.assign(existing)).toMatchObject({ cellId: 'cell-a' })
|
|
await store.setCellEnabled('cell-a', false)
|
|
await store.reconcileCells([
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
|
|
])
|
|
expect(await store.resolve(existing)).toMatchObject({ cellId: 'cell-a' })
|
|
await expect(
|
|
store.assign({ userId: 'user-b', relayHostId: 'host000000000002' })
|
|
).resolves.toMatchObject({ cellId: 'cell-b' })
|
|
await store.setCellEnabled('cell-a', true)
|
|
await expect(store.setCellEnabled('missing', false)).rejects.toThrow('cell_not_found')
|
|
})
|
|
|
|
it('bulk-migrates active controls without exposing assignment identities', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
await store.setCellEnabled('cell-b', false)
|
|
const identities = [1, 2, 3].map((index) => ({
|
|
userId: `user-${index}`,
|
|
relayHostId: `host${String(index).padStart(12, '0')}`
|
|
}))
|
|
const sourceControls: string[] = []
|
|
for (const identity of identities) {
|
|
const assignment = await store.assign(identity)
|
|
sourceControls.push(
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
)
|
|
}
|
|
await store.setCellEnabled('cell-b', true)
|
|
expect(await store.cellEvacuationCapacity('cell-a', 'cell-b')).toEqual({
|
|
sourceAssignments: 3,
|
|
requiredTargetUnits: 6,
|
|
availableTargetUnits: 20
|
|
})
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 2)).toBe(2)
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 2)).toBe(1)
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 2)).toBe(0)
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toEqual({
|
|
inProgress: 3,
|
|
oldestExpiresAt: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
oldestRemainingMs: ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 0,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
for (const identity of identities) {
|
|
const assignment = await store.resolve(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: assignment!.assignmentEpoch,
|
|
generation: 2
|
|
})
|
|
await store.markMigrationTargetRegistered(identity, {
|
|
cellId: 'cell-b',
|
|
assignmentEpoch: assignment!.assignmentEpoch
|
|
})
|
|
}
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toEqual({
|
|
inProgress: 3,
|
|
oldestExpiresAt: 100 + ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
oldestRemainingMs: ASSIGNMENT_LIMITS.migrationLeaseMs,
|
|
targetRegistered: 3,
|
|
registeredSourceActive: 3,
|
|
registeredCompletable: 0,
|
|
registeredTargetInactive: 0,
|
|
completed: 0,
|
|
blocked: 3,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
for (let index = 0; index < identities.length; index++) {
|
|
await store.releaseActivity(identities[index]!, sourceControls[index]!)
|
|
}
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', true)).toEqual({
|
|
inProgress: 0,
|
|
...NO_ACTIVE_MIGRATION_LEASE,
|
|
targetRegistered: 0,
|
|
...NO_REGISTERED_EVACUATION_DIAGNOSTICS,
|
|
completed: 3,
|
|
blocked: 0,
|
|
...NO_EXPIRED_EVACUATION_DIAGNOSTICS
|
|
})
|
|
})
|
|
|
|
it.each(['cell-b', 'cell-c'])(
|
|
'skips a batch row that concurrently moved from the selected source to %s',
|
|
async (movedCellId) => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-c', url: 'https://relay-c.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const startEvacuation = store.startEvacuation.bind(store)
|
|
vi.spyOn(store, 'startEvacuation').mockImplementationOnce(async (...args) => {
|
|
await database!.query(
|
|
`UPDATE relay_assignments SET cell_id = ? WHERE user_id = ? AND relay_host_id = ?`,
|
|
[movedCellId, identity.userId, identity.relayHostId]
|
|
)
|
|
return await startEvacuation(...args)
|
|
})
|
|
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 10)).toBe(0)
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: movedCellId })
|
|
expect(await database!.query(`SELECT * FROM relay_assignment_migrations`)).toEqual([])
|
|
}
|
|
)
|
|
|
|
it('does not count an existing migration after a stale batch row moves to its target', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
await store.activateControl(identity, {
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
const startEvacuation = store.startEvacuation.bind(store)
|
|
vi.spyOn(store, 'startEvacuation').mockImplementationOnce(async (...args) => {
|
|
await startEvacuation(identity, 'cell-b')
|
|
return await startEvacuation(...args)
|
|
})
|
|
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 10)).toBe(0)
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-b' })
|
|
expect(await store.cellEvacuationStatus('cell-a', 'cell-b', false)).toMatchObject({
|
|
inProgress: 1
|
|
})
|
|
})
|
|
|
|
it('preserves operator-owned tagged URLs and admission state across reconciliation', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 }
|
|
])
|
|
await store.configureCell(
|
|
{ id: 'cell-a', url: 'https://old---relay-a.example.com', capacityRequests: 20 },
|
|
false
|
|
)
|
|
await store.reconcileCells([
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 }
|
|
])
|
|
const rows = await database!.query(
|
|
`SELECT cell_url, enabled, capacity_requests FROM relay_cells WHERE cell_id = ?`,
|
|
['cell-a']
|
|
)
|
|
expect(rows).toEqual([
|
|
{
|
|
cell_url: 'https://old---relay-a.example.com',
|
|
enabled: 0,
|
|
capacity_requests: 20
|
|
}
|
|
])
|
|
})
|
|
|
|
it('bulk-migrates activity even when its source control is temporarily absent', async () => {
|
|
const store = await setup(() => 100, [
|
|
{ id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 20 },
|
|
{ id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 20 }
|
|
])
|
|
const identity = { userId: 'user-1', relayHostId: 'host000000000001' }
|
|
const assignment = await store.assign(identity)
|
|
const control = await store.activateControl(identity, {
|
|
cellId: 'cell-a',
|
|
assignmentEpoch: assignment.assignmentEpoch,
|
|
generation: 1
|
|
})
|
|
await store.acquireActivity(identity, {
|
|
activityId: 'splice-without-control',
|
|
kind: 'splice',
|
|
cellId: 'cell-a'
|
|
})
|
|
await store.releaseActivity(identity, control)
|
|
expect(await store.cellEvacuationCapacity('cell-a', 'cell-b')).toEqual({
|
|
sourceAssignments: 1,
|
|
requiredTargetUnits: 3,
|
|
availableTargetUnits: 20
|
|
})
|
|
expect(await store.startActiveCellEvacuations('cell-a', 'cell-b', 10)).toBe(1)
|
|
expect(await store.resolve(identity)).toMatchObject({ cellId: 'cell-b', assignmentEpoch: 2 })
|
|
})
|
|
})
|
|
|
|
async function cellReservations(database: RelayDatabase): Promise<Record<string, number>> {
|
|
const rows = await database.query(
|
|
`SELECT cell_id, reserved_requests FROM relay_cells ORDER BY cell_id ASC`
|
|
)
|
|
return Object.fromEntries(rows.map((row) => [String(row.cell_id), Number(row.reserved_requests)]))
|
|
}
|