Skip to content

Commit 14ae18d

Browse files
committed
perf: stream control plane backups to disk
1 parent 9f7a8ad commit 14ae18d

1 file changed

Lines changed: 15 additions & 5 deletions

File tree

  • packages/ts-cloud/src/control-plane

packages/ts-cloud/src/control-plane/store.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Changes, SQLQueryBindings } from 'bun:sqlite'
22
import type { AppendEventInput, AuthorizationGrant, AuthorizationScope, AuthorizationScopeType, AuthorizationTarget, CompactResult, ControlPlaneActor, ControlPlaneEnvironment, ControlPlaneEvent, ControlPlaneHealth, ControlPlaneOperation, ControlPlaneOrganization, ControlPlaneProject, ControlPlaneResource, ControlPlaneSnapshot, ControlPlaneStoreOptions, ControlPlaneTag, CreateActorInput, CreateEnvironmentInput, CreateGrantInput, CreateInvitationInput, CreateMembershipInput, CreateOperationInput, CreateOrganizationInput, CreateProjectInput, CreateResourceInput, EventListOptions, ImportSnapshotOptions, JsonValue, NavigationPreference, OperationListOptions, OperationState, OrganizationInvitation, OrganizationMembership, ReconcileResult, SavedFilter, TransitionOperationInput, UpdateProjectInput, UpdateResourceInput } from './types'
33
import { createHash, randomBytes } from 'node:crypto'
4-
import { chmodSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
4+
import { chmodSync, existsSync, mkdirSync, statSync } from 'node:fs'
55
import { dirname, join } from 'node:path'
66
import { resolveStatePath, statePath } from '@ts-cloud/core'
77
import { Database } from 'bun:sqlite'
@@ -349,6 +349,16 @@ export class ControlPlaneStore {
349349
}
350350
}
351351

352+
private backupTo(path: string): void {
353+
// SQLite writes VACUUM INTO incrementally. database.serialize() duplicates
354+
// the complete database in the Bun heap, which makes a routine schema
355+
// migration consume hundreds of megabytes once telemetry history grows.
356+
this.database.run('VACUUM INTO ?', [path])
357+
try {
358+
chmodSync(path, 0o600)
359+
} catch {}
360+
}
361+
352362
private migrate(): void {
353363
const row = this.database.query<Row, []>('PRAGMA user_version').get()
354364
const current = Number(row?.user_version ?? 0)
@@ -359,7 +369,7 @@ export class ControlPlaneStore {
359369
let backupPath: string | undefined
360370
if (current > 0 && this.path !== ':memory:') {
361371
backupPath = `${this.path}.v${current}.${Date.now()}.bak`
362-
writeFileSync(backupPath, this.database.serialize(), { mode: 0o600 })
372+
this.backupTo(backupPath)
363373
}
364374

365375
try {
@@ -723,7 +733,7 @@ export class ControlPlaneStore {
723733
}
724734

725735
listMemberships(organizationId: string, options: { includeRevoked?: boolean } = {}): OrganizationMembership[] {
726-
const revoked = options.includeRevoked ? '' : "AND status = 'active'"
736+
const revoked = options.includeRevoked ? '' : `AND status = 'active'`
727737
return this.database
728738
.query<Row, [string]>(
729739
`SELECT * FROM organization_memberships WHERE organization_id = ? ${revoked} ORDER BY created_at, id`,
@@ -802,7 +812,7 @@ export class ControlPlaneStore {
802812
const owners = Number(
803813
this.database
804814
.query<Row, [string, string]>(
805-
"SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'",
815+
`SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'`,
806816
)
807817
.get(current.organizationId, 'active')?.count ?? 0,
808818
)
@@ -1535,7 +1545,7 @@ export class ControlPlaneStore {
15351545
createBackup(reason: string = 'manual'): string {
15361546
if (this.path === ':memory:') throw new Error('Cannot create a filesystem backup for an in-memory control plane')
15371547
const backupPath = `${this.path}.${Date.now()}.bak`
1538-
writeFileSync(backupPath, this.database.serialize(), { mode: 0o600 })
1548+
this.backupTo(backupPath)
15391549
this.setSetting('storage.last_backup', { path: backupPath, createdAt: this.now(), reason })
15401550
return backupPath
15411551
}

0 commit comments

Comments
 (0)