chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
import { getPostgresErrorCode } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import { backoffWithJitter } from '@sim/utils/retry'
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
import { runScriptMigrations } from '../script-migrations/index'
/**
* Concurrent-index convention: plain `CREATE INDEX` write-blocks large/hot
* tables, and CONCURRENTLY cannot run inside drizzle's migration transaction.
* For indexes on big tables, edit the generated SQL to:
*
* COMMIT;--> statement-breakpoint
* SET lock_timeout = 0;--> statement-breakpoint
* CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_name" ON "table" (...);--> statement-breakpoint
* SET lock_timeout = '5s';
*
* The embedded COMMIT ends the batch transaction, so everything after it (in
* this and later pending files) runs in autocommit and must be idempotent
* (`IF NOT EXISTS` etc.) — a failed run replays unjournaled files from the top.
* A failed CONCURRENTLY build leaves an INVALID index that `IF NOT EXISTS`
* skips; `warnOnInvalidIndexes` below surfaces those.
*/
/**
* Prefer a direct (non-pooled) DSN: session advisory locks and session `SET`s
* are unsupported through PgBouncer transaction pooling. Falls back to
* DATABASE_URL for setups that connect directly anyway.
*/
const url = process.env.MIGRATION_DATABASE_URL || process.env.DATABASE_URL
if (!url) {
console.error('ERROR: Missing DATABASE_URL environment variable.')
console.error('Ensure packages/db/.env is configured.')
process.exit(1)
}
/**
* The pid guard is only sound on a direct connection — through transaction
* pooling, consecutive statements legitimately land on different backends.
*/
const hasDirectMigrationUrl = Boolean(process.env.MIGRATION_DATABASE_URL)
/**
* `max_lifetime: null` pins the session for the whole run: the postgres-js
* default recycles the connection after 3060 min, silently dropping the
* session advisory lock and `SET`s.
*/
const client = postgres(url, {
max: 1,
connect_timeout: 10,
max_lifetime: null,
connection: { application_name: 'sim-migrate' },
})
/**
* Cross-process migration lock. drizzle's `migrate()` has no built-in lock, so
* concurrent runners (one per app replica at deploy time) must be serialized.
* Acquisition is a bounded try-lock loop: a plain `pg_advisory_lock` wait let
* one wedged runner silently hang every other runner and the whole deploy.
*/
const MIGRATION_LOCK_KEY = 4_961_002_270n
const LOCK_ACQUIRE_DEADLINE_MS = 30 * 60_000
const LOCK_RETRY_INTERVAL_MS = 5_000
/**
* Max time a migration statement may queue for a table lock (SQLSTATE 55P03 on
* expiry). Without it, DDL waiting on an AccessExclusiveLock queues every other
* query on the table behind it — a table-wide stall for the whole wait.
*/
const DDL_LOCK_TIMEOUT = '5s'
const MAX_MIGRATE_ATTEMPTS = 8
const MIGRATE_RETRY_BACKOFF = { baseMs: 2_000, maxMs: 30_000 } as const
const CONNECT_MAX_ATTEMPTS = 10
const CONNECT_RETRY_BACKOFF = { baseMs: 1_000, maxMs: 15_000 } as const
/**
* Error codes that mean the database was momentarily unreachable rather than
* the migration being wrong: chiefly `53300` (too_many_connections — every
* non-superuser slot was taken, surfaced as "remaining connection slots are
* reserved for roles with the SUPERUSER attribute"), the `08xxx`
* connection_exception class, and the postgres-js driver's own transport
* codes. These are retried while opening the session; anything else is fatal.
*/
const TRANSIENT_CONNECT_CODES = new Set([
'53300',
'53400',
'CONNECT_TIMEOUT',
'CONNECTION_CLOSED',
'CONNECTION_DESTROYED',
'CONNECTION_ENDED',
'ECONNREFUSED',
'ECONNRESET',
'ETIMEDOUT',
'EHOSTUNREACH',
'ENOTFOUND',
])
function isTransientConnectError(error: unknown): boolean {
const code = getPostgresErrorCode(error)
if (!code) return false
return TRANSIENT_CONNECT_CODES.has(code) || code.startsWith('08')
}
/** Backend pid of the lock-holding session; a change means the lock was lost. */
let lockSessionPid = 0
try {
await connectWithRetry()
await acquireMigrationLock()
try {
await runMigrationsWithRetry()
console.log('Migrations applied successfully.')
await assertLockSessionHeld()
await runScriptMigrations(client)
await warnOnInvalidIndexes()
} finally {
await releaseMigrationLock()
}
} catch (error) {
console.error('ERROR: Migration failed.')
printMigrationError(error)
process.exit(1)
} finally {
await client.end()
}
/**
* Open the migration session before taking the advisory lock, retrying
* transient connection failures with bounded backoff. The deploy database can
* briefly exhaust every non-superuser connection slot at peak (`53300`); the
* migration is a single short-lived session, so waiting out a spike that frees
* within seconds is far safer than failing the whole deploy. Non-transient
* errors (auth, unknown host config, etc.) still fail fast.
*/
async function connectWithRetry(): Promise<void> {
for (let attempt = 1; ; attempt++) {
try {
await client`SELECT 1`
return
} catch (error) {
if (!isTransientConnectError(error) || attempt >= CONNECT_MAX_ATTEMPTS) throw error
const delayMs = backoffWithJitter(attempt, null, CONNECT_RETRY_BACKOFF)
console.warn(
`WARN: database unavailable (${getPostgresErrorCode(error)}); ` +
`attempt ${attempt}/${CONNECT_MAX_ATTEMPTS}, retrying in ${Math.round(delayMs)}ms.`
)
await sleep(delayMs)
}
}
}
/**
* Acquire the cross-process migration lock, failing loudly after the deadline
* instead of blocking forever behind a wedged runner.
*/
async function acquireMigrationLock(): Promise<void> {
const deadline = Date.now() + LOCK_ACQUIRE_DEADLINE_MS
for (;;) {
const [{ locked, pid }] =
await client`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_KEY}) AS locked, pg_backend_pid() AS pid`
if (locked) {
lockSessionPid = pid
return
}
if (Date.now() >= deadline) {
throw new Error(
`Timed out after ${LOCK_ACQUIRE_DEADLINE_MS}ms waiting for the migration advisory lock; ` +
'another runner is likely stuck mid-migration. Investigate before retrying.'
)
}
await sleep(LOCK_RETRY_INTERVAL_MS)
}
}
/**
* Run pending migrations, retrying on lock timeout (55P03, found anywhere in
* the wrapped `cause` chain). Each attempt re-verifies the lock session (pid)
* and re-asserts the session timeouts — a migration file may have changed them,
* and `SET` cannot be parameterized, hence `client.unsafe` with constants.
* Replays are safe: drizzle rolls the batch back on failure, and post-COMMIT
* CONCURRENTLY statements are idempotent by convention.
*/
/**
* Verify the session still holds the migration advisory lock: a changed
* backend pid means the connection was recycled and the lock silently dropped.
* Only sound on a direct connection — see `hasDirectMigrationUrl`.
*/
async function assertLockSessionHeld(): Promise<void> {
if (!hasDirectMigrationUrl) return
const [{ pid }] = await client`SELECT pg_backend_pid() AS pid`
if (pid !== lockSessionPid) {
throw new Error(
`Database session changed mid-run (backend pid ${lockSessionPid} -> ${pid}); ` +
'the migration advisory lock was lost. Aborting so a fresh runner can retry safely.'
)
}
}
async function runMigrationsWithRetry(): Promise<void> {
for (let attempt = 1; ; attempt++) {
await assertLockSessionHeld()
await client.unsafe('SET statement_timeout = 0')
await client.unsafe(`SET lock_timeout = '${DDL_LOCK_TIMEOUT}'`)
try {
await migrate(drizzle(client), { migrationsFolder: './migrations' })
return
} catch (error) {
const isLockTimeout = getPostgresErrorCode(error) === '55P03'
if (!isLockTimeout || attempt >= MAX_MIGRATE_ATTEMPTS) throw error
const delayMs = backoffWithJitter(attempt, null, MIGRATE_RETRY_BACKOFF)
console.warn(
`WARN: migration DDL hit lock_timeout (attempt ${attempt}/${MAX_MIGRATE_ATTEMPTS}); ` +
`retrying in ${Math.round(delayMs)}ms.`
)
await sleep(delayMs)
}
}
}
/**
* A failed CONCURRENTLY build leaves an INVALID index that `IF NOT EXISTS`
* silently skips forever — surface it (warn only; the migration committed).
*/
async function warnOnInvalidIndexes(): Promise<void> {
try {
const rows = await client`
SELECT n.nspname AS schema, c.relname AS index
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE NOT i.indisvalid
`
for (const row of rows) {
console.warn(
`WARN: invalid index ${row.schema}.${row.index} — a CONCURRENTLY build failed partway. ` +
'Drop and rebuild it; IF NOT EXISTS will keep skipping it.'
)
}
} catch (checkError) {
console.warn('WARN: could not check for invalid indexes.', checkError)
}
}
/**
* Unlock errors are swallowed: the session lock auto-releases on disconnect,
* and a thrown unlock would falsely report a committed migration as failed.
*/
async function releaseMigrationLock(): Promise<void> {
try {
await client`SELECT pg_advisory_unlock(${MIGRATION_LOCK_KEY})`
} catch (unlockError) {
console.error(
'WARN: pg_advisory_unlock failed; the session lock will auto-release on disconnect.',
unlockError
)
}
}
/**
* Print every diagnostic field a Postgres driver puts on a thrown error. The default
* `error.message` loses the constraint name, affected table/column, PG code, and hint —
* which are usually what you need to diagnose a failed migration.
*/
function printMigrationError(error: unknown): void {
if (!(error instanceof Error)) {
console.error(error)
return
}
console.error(`message: ${error.message}`)
const pgFields = [
'code',
'severity',
'severity_local',
'detail',
'hint',
'schema',
'schema_name',
'table',
'table_name',
'column',
'column_name',
'constraint',
'constraint_name',
'data_type',
'where',
'internal_query',
'internal_position',
'position',
'routine',
'file',
'line',
] as const
const err = error as Record<string, unknown>
for (const field of pgFields) {
const value = err[field]
if (value !== undefined && value !== null && value !== '') {
console.error(`${field}: ${String(value)}`)
}
}
if (err.query && typeof err.query === 'string') {
console.error('\nfailing query:')
console.error(err.query)
}
if (err.parameters !== undefined) {
console.error('\nparameters:')
console.error(err.parameters)
}
if (error.cause) {
console.error('\ncause:')
printMigrationError(error.cause)
}
if (error.stack) {
console.error('\nstack:')
console.error(error.stack)
}
}