@rocicorp/zero 1.8.0 → 1.9.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/out/shared/src/cache.d.ts +8 -1
  2. package/out/shared/src/cache.d.ts.map +1 -1
  3. package/out/shared/src/cache.js +25 -9
  4. package/out/shared/src/cache.js.map +1 -1
  5. package/out/shared/src/objects.d.ts +6 -1
  6. package/out/shared/src/objects.d.ts.map +1 -1
  7. package/out/shared/src/objects.js +19 -4
  8. package/out/shared/src/objects.js.map +1 -1
  9. package/out/zero/package.js +2 -19
  10. package/out/zero/package.js.map +1 -1
  11. package/out/zero-cache/src/config/zero-config.d.ts +4 -0
  12. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  13. package/out/zero-cache/src/config/zero-config.js +21 -6
  14. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  15. package/out/zero-cache/src/custom-queries/transform-query.d.ts +6 -0
  16. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  17. package/out/zero-cache/src/custom-queries/transform-query.js +8 -0
  18. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  19. package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
  20. package/out/zero-cache/src/db/migration-lite.js +5 -2
  21. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  22. package/out/zero-cache/src/db/migration.d.ts.map +1 -1
  23. package/out/zero-cache/src/db/migration.js +3 -1
  24. package/out/zero-cache/src/db/migration.js.map +1 -1
  25. package/out/zero-cache/src/db/sqlite-corruption.d.ts +11 -0
  26. package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -0
  27. package/out/zero-cache/src/db/sqlite-corruption.js +191 -0
  28. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -0
  29. package/out/zero-cache/src/server/anonymous-otel-start.d.ts +1 -1
  30. package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
  31. package/out/zero-cache/src/server/backup-watermark-reader.js +1 -1
  32. package/out/zero-cache/src/server/backup-watermark-reader.js.map +1 -1
  33. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  34. package/out/zero-cache/src/server/change-streamer.js +7 -2
  35. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  36. package/out/zero-cache/src/server/logging.d.ts +8 -0
  37. package/out/zero-cache/src/server/logging.d.ts.map +1 -1
  38. package/out/zero-cache/src/server/logging.js +27 -5
  39. package/out/zero-cache/src/server/logging.js.map +1 -1
  40. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  41. package/out/zero-cache/src/server/main.js +6 -1
  42. package/out/zero-cache/src/server/main.js.map +1 -1
  43. package/out/zero-cache/src/server/mutator.js +1 -1
  44. package/out/zero-cache/src/server/mutator.js.map +1 -1
  45. package/out/zero-cache/src/server/otel-log-sink.d.ts +7 -1
  46. package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
  47. package/out/zero-cache/src/server/otel-log-sink.js +7 -2
  48. package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
  49. package/out/zero-cache/src/server/reaper.js +1 -1
  50. package/out/zero-cache/src/server/reaper.js.map +1 -1
  51. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  52. package/out/zero-cache/src/server/replicator.js +11 -1
  53. package/out/zero-cache/src/server/replicator.js.map +1 -1
  54. package/out/zero-cache/src/server/shadow-syncer.js +1 -1
  55. package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
  56. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  57. package/out/zero-cache/src/server/syncer.js +6 -1
  58. package/out/zero-cache/src/server/syncer.js.map +1 -1
  59. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  60. package/out/zero-cache/src/services/change-source/common/replica-schema.js +8 -5
  61. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  62. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +36 -2
  63. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/change-source/pg/change-source.js +76 -33
  65. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  66. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +15 -0
  67. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +41 -10
  69. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  70. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +21 -0
  71. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  72. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +34 -5
  73. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  74. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +2 -2
  75. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  76. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  77. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +61 -87
  78. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  79. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +17 -12
  80. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  81. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  82. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/life-cycle.js +15 -3
  84. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  85. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  86. package/out/zero-cache/src/services/litestream/commands.js +20 -3
  87. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  88. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +2 -2
  89. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  90. package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
  91. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  92. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/replicator/change-processor.js +28 -15
  94. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  95. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +1 -1
  96. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/replicator/reporter/recorder.js +5 -12
  98. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  99. package/out/zero-cache/src/services/replicator/write-worker.js +38 -7
  100. package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
  101. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  102. package/out/zero-cache/src/services/view-syncer/cvr.js +8 -1
  103. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  104. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  105. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +66 -26
  106. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  107. package/out/zero-cache/src/services/view-syncer/query-covering.d.ts +28 -0
  108. package/out/zero-cache/src/services/view-syncer/query-covering.d.ts.map +1 -0
  109. package/out/zero-cache/src/services/view-syncer/query-covering.js +159 -0
  110. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -0
  111. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +2 -0
  112. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  113. package/out/zero-cache/src/services/view-syncer/view-syncer.js +71 -1
  114. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  115. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  116. package/out/zero-cache/src/types/lite.js +4 -1
  117. package/out/zero-cache/src/types/lite.js.map +1 -1
  118. package/out/zero-cache/src/types/pg.d.ts +18 -0
  119. package/out/zero-cache/src/types/pg.d.ts.map +1 -1
  120. package/out/zero-cache/src/types/pg.js +50 -0
  121. package/out/zero-cache/src/types/pg.js.map +1 -1
  122. package/out/zero-cache/src/workers/syncer.d.ts +1 -1
  123. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  124. package/out/zero-cache/src/workers/syncer.js +1 -0
  125. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  126. package/out/zero-client/src/client/crud.d.ts.map +1 -1
  127. package/out/zero-client/src/client/crud.js +3 -4
  128. package/out/zero-client/src/client/crud.js.map +1 -1
  129. package/out/zero-client/src/client/version.js +1 -1
  130. package/out/zero-schema/src/builder/relationship-builder.d.ts.map +1 -1
  131. package/out/zero-schema/src/builder/relationship-builder.js +3 -2
  132. package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
  133. package/out/zero-schema/src/builder/schema-builder.d.ts.map +1 -1
  134. package/out/zero-schema/src/builder/schema-builder.js +7 -7
  135. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  136. package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
  137. package/out/zero-schema/src/builder/table-builder.js +2 -1
  138. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  139. package/out/zero-server/src/custom.js +2 -1
  140. package/out/zero-server/src/custom.js.map +1 -1
  141. package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
  142. package/out/zql/src/ivm/view-apply-change.js +2 -1
  143. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  144. package/out/zql/src/mutate/crud.d.ts +10 -8
  145. package/out/zql/src/mutate/crud.d.ts.map +1 -1
  146. package/out/zql/src/mutate/crud.js +3 -4
  147. package/out/zql/src/mutate/crud.js.map +1 -1
  148. package/out/zqlite/src/db.js +2 -2
  149. package/out/zqlite/src/internal/statement-cache.d.ts +25 -1
  150. package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
  151. package/out/zqlite/src/internal/statement-cache.js +48 -18
  152. package/out/zqlite/src/internal/statement-cache.js.map +1 -1
  153. package/out/zqlite/src/query-builder.js +3 -3
  154. package/out/zqlite/src/query-builder.js.map +1 -1
  155. package/out/zqlite/src/sqlite-cost-model.js +5 -5
  156. package/out/zqlite/src/table-source.js +3 -3
  157. package/package.json +2 -33
@@ -1 +1 @@
1
- {"version":3,"file":"migration.js","names":[],"sources":["../../../../../zero-cache/src/db/migration.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../types/pg.ts';\nimport {runTx} from './run-transaction.ts';\n\ntype Operations = (log: LogContext, tx: PostgresTransaction) => Promise<void>;\n\n/**\n * Encapsulates the logic for setting up or upgrading to a new schema. After the\n * Migration code successfully completes, {@link runSchemaMigrations}\n * will update the schema version and commit the transaction.\n */\nexport type Migration = {\n /**\n * Perform database operations that create or alter table structure. This is\n * called at most once during lifetime of the application. If a `migrateData()`\n * operation is defined, that will be performed after `migrateSchema()` succeeds.\n */\n migrateSchema?: Operations;\n\n /**\n * Perform database operations to migrate data to the new schema. This is\n * called after `migrateSchema()` (if defined), and may be called again\n * to re-migrate data after the server was rolled back to an earlier version,\n * and rolled forward again.\n *\n * Consequently, the logic in `migrateData()` must be idempotent.\n */\n migrateData?: Operations;\n\n /**\n * Sets the `minSafeVersion` to the specified value, prohibiting running\n * any earlier code versions.\n */\n minSafeVersion?: number;\n};\n\n/**\n * Mapping of incremental migrations to move from the previous old code\n * version to next one. Versions must be non-zero.\n *\n * The schema resulting from performing incremental migrations should be\n * equivalent to that of the `setupMigration` on a blank database.\n *\n * The highest destinationVersion of this map denotes the current\n * \"code version\", and is also used as the destination version when\n * running the initial setup migration on a blank database.\n */\nexport type IncrementalMigrationMap = {\n [destinationVersion: number]: Migration;\n};\n\n/**\n * Ensures that the schema is compatible with the current code, updating and\n * migrating the schema if necessary.\n */\nexport async function runSchemaMigrations(\n log: LogContext,\n debugName: string,\n schemaName: string,\n db: PostgresDB,\n setupMigration: Migration,\n incrementalMigrationMap: IncrementalMigrationMap,\n): Promise<void> {\n log = log.withContext('initSchema', schemaName);\n\n const versionMigrations = sorted(incrementalMigrationMap);\n assert(\n versionMigrations.length,\n `Must specify at least one version migration`,\n );\n assert(\n versionMigrations[0][0] > 0,\n `Versions must be non-zero positive numbers`,\n );\n // oxlint-disable-next-line typescript/no-non-null-assertion\n const codeVersion = versionMigrations.at(-1)![0];\n\n log.info?.(\n `Checking schema for compatibility with ${debugName} at schema v${codeVersion}`,\n );\n\n try {\n await runTx(db, async tx => {\n // Acquire advisory lock to prevent concurrent migrations from racing.\n // This can happen during rolling deployments when multiple pods start\n // up simultaneously. The lock auto-releases when the transaction ends.\n const lockName = `migrate-schema:${schemaName}`;\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n let versions = await ensureVersionHistory(tx, schemaName);\n\n if (codeVersion < versions.minSafeVersion) {\n throw new Error(\n `Cannot run ${debugName} at schema v${codeVersion} because rollback limit is v${versions.minSafeVersion}`,\n );\n }\n\n if (versions.dataVersion > codeVersion) {\n log.info?.(\n `Data is at v${versions.dataVersion}. Resetting to v${codeVersion}`,\n );\n await updateVersionHistory(log, tx, schemaName, versions, codeVersion);\n return;\n }\n\n if (versions.dataVersion === codeVersion) {\n return;\n }\n\n const migrations =\n versions.dataVersion === 0\n ? // For an empty database (v0), only run the setup migration.\n ([[codeVersion, setupMigration]] as const)\n : versionMigrations;\n\n for (const [dest, migration] of migrations) {\n if (versions.dataVersion < dest) {\n log.info?.(\n `Migrating schema from v${versions.dataVersion} to v${dest}`,\n );\n void log.flush();\n versions = await runMigration(\n log,\n schemaName,\n tx,\n versions,\n dest,\n migration,\n );\n }\n }\n });\n\n log.info?.(`Running ${debugName} at schema v${codeVersion}`);\n } catch (e) {\n log.error?.('Error in ensureSchemaMigrated', e);\n throw e;\n } finally {\n void log.flush();\n }\n}\n\nfunction sorted(\n incrementalMigrationMap: IncrementalMigrationMap,\n): [number, Migration][] {\n const versionMigrations: [number, Migration][] = [];\n for (const [v, m] of Object.entries(incrementalMigrationMap)) {\n versionMigrations.push([Number(v), m]);\n }\n return versionMigrations.sort(([a], [b]) => a - b);\n}\n\n// Exposed for tests.\nexport const versionHistory = v.object({\n /**\n * The `schemaVersion` is highest code version that has ever been run\n * on the database, and is used to delineate the structure of the tables\n * in the database. A schemaVersion only moves forward; rolling back to\n * an earlier (safe) code version does not revert schema changes that\n * have already been applied.\n */\n schemaVersion: v.number(),\n\n /**\n * The data version is the code version of the latest server that ran.\n * Note that this may be less than the schemaVersion in the case that\n * a server is rolled back to an earlier version after a schema change.\n * In such a case, data (but not schema), may need to be re-migrated\n * when rolling forward again.\n */\n dataVersion: v.number(),\n\n /**\n * The minimum code version that is safe to run. This is used when\n * a schema migration is not backwards compatible with an older version\n * of the code.\n */\n minSafeVersion: v.number(),\n});\n\n// Exposed for tests.\nexport type VersionHistory = v.Infer<typeof versionHistory>;\n\n// Exposed for tests.\nexport async function createVersionHistoryTable(\n sql: postgres.Sql,\n schemaName: string,\n) {\n // Note: The `lock` column transparently ensures that at most one row exists.\n await sql`\n CREATE SCHEMA IF NOT EXISTS ${sql(schemaName)};\n CREATE TABLE IF NOT EXISTS ${sql(schemaName)}.\"versionHistory\" (\n \"dataVersion\" int NOT NULL,\n \"schemaVersion\" int NOT NULL,\n \"minSafeVersion\" int NOT NULL,\n\n lock char(1) NOT NULL CONSTRAINT DF_schema_meta_lock DEFAULT 'v',\n CONSTRAINT PK_schema_meta_lock PRIMARY KEY (lock),\n CONSTRAINT CK_schema_meta_lock CHECK (lock='v')\n );`.simple();\n}\n\nasync function ensureVersionHistory(\n sql: postgres.Sql,\n schemaName: string,\n): Promise<VersionHistory> {\n return must(await getVersionHistory(sql, schemaName, true));\n}\n\nexport async function getVersionHistory(\n sql: postgres.Sql,\n schemaName: string,\n create = false,\n): Promise<VersionHistory | null> {\n const exists = await sql`\n SELECT nspname, relname FROM pg_class\n JOIN pg_namespace ON relnamespace = pg_namespace.oid\n WHERE nspname = ${schemaName} AND relname = ${'versionHistory'}`;\n\n if (exists.length === 0) {\n if (create) {\n await createVersionHistoryTable(sql, schemaName);\n } else {\n return null;\n }\n }\n const rows = await sql`\n SELECT \"dataVersion\", \"schemaVersion\", \"minSafeVersion\"\n FROM ${sql(schemaName)}.\"versionHistory\"`;\n\n if (rows.length === 0) {\n return create\n ? {schemaVersion: 0, dataVersion: 0, minSafeVersion: 0}\n : null;\n }\n return v.parse(rows[0], versionHistory);\n}\n\nasync function updateVersionHistory(\n log: LogContext,\n sql: postgres.Sql,\n schemaName: string,\n prev: VersionHistory,\n newVersion: number,\n minSafeVersion?: number,\n): Promise<VersionHistory> {\n assert(newVersion > 0, 'newVersion must be positive');\n const versions = {\n dataVersion: newVersion,\n // The schemaVersion never moves backwards.\n schemaVersion: Math.max(newVersion, prev.schemaVersion),\n minSafeVersion: getMinSafeVersion(log, prev, minSafeVersion),\n } satisfies VersionHistory;\n\n await sql`\n INSERT INTO ${sql(schemaName)}.\"versionHistory\" ${sql(versions)}\n ON CONFLICT (lock) DO UPDATE SET ${sql(versions)}\n `;\n return versions;\n}\n\nasync function runMigration(\n log: LogContext,\n schemaName: string,\n tx: PostgresTransaction,\n versions: VersionHistory,\n destinationVersion: number,\n migration: Migration,\n): Promise<VersionHistory> {\n if (versions.schemaVersion < destinationVersion) {\n await migration.migrateSchema?.(log, tx);\n }\n if (versions.dataVersion < destinationVersion) {\n await migration.migrateData?.(log, tx);\n }\n return updateVersionHistory(\n log,\n tx,\n schemaName,\n versions,\n destinationVersion,\n migration.minSafeVersion,\n );\n}\n\n/**\n * Bumps the rollback limit [[toAtLeast]] the specified version.\n * Leaves the rollback limit unchanged if it is equal or greater.\n */\nfunction getMinSafeVersion(\n log: LogContext,\n current: VersionHistory,\n proposedSafeVersion?: number,\n): number {\n if (proposedSafeVersion === undefined) {\n return current.minSafeVersion;\n }\n if (current.minSafeVersion >= proposedSafeVersion) {\n // The rollback limit must never move backwards.\n log.debug?.(\n `rollback limit is already at ${current.minSafeVersion}, ` +\n `don't need to bump to ${proposedSafeVersion}`,\n );\n return current.minSafeVersion;\n }\n log.info?.(\n `bumping rollback limit from ${current.minSafeVersion} to ${proposedSafeVersion}`,\n );\n return proposedSafeVersion;\n}\n"],"mappings":";;;;;;;;;;AA2DA,eAAsB,oBACpB,KACA,WACA,YACA,IACA,gBACA,yBACe;CACf,MAAM,IAAI,YAAY,cAAc,UAAU;CAE9C,MAAM,oBAAoB,OAAO,uBAAuB;CACxD,OACE,kBAAkB,QAClB,6CACF;CACA,OACE,kBAAkB,GAAG,KAAK,GAC1B,4CACF;CAEA,MAAM,cAAc,kBAAkB,GAAG,EAAE,EAAG;CAE9C,IAAI,OACF,0CAA0C,UAAU,cAAc,aACpE;CAEA,IAAI;EACF,MAAM,MAAM,IAAI,OAAM,OAAM;GAK1B,MAAM,EAAE,yCAAyC,kBADd,aACuB;GAE1D,IAAI,WAAW,MAAM,qBAAqB,IAAI,UAAU;GAExD,IAAI,cAAc,SAAS,gBACzB,MAAM,IAAI,MACR,cAAc,UAAU,cAAc,YAAY,8BAA8B,SAAS,gBAC3F;GAGF,IAAI,SAAS,cAAc,aAAa;IACtC,IAAI,OACF,eAAe,SAAS,YAAY,kBAAkB,aACxD;IACA,MAAM,qBAAqB,KAAK,IAAI,YAAY,UAAU,WAAW;IACrE;GACF;GAEA,IAAI,SAAS,gBAAgB,aAC3B;GAGF,MAAM,aACJ,SAAS,gBAAgB,IAEpB,CAAC,CAAC,aAAa,cAAc,CAAC,IAC/B;GAEN,KAAK,MAAM,CAAC,MAAM,cAAc,YAC9B,IAAI,SAAS,cAAc,MAAM;IAC/B,IAAI,OACF,0BAA0B,SAAS,YAAY,OAAO,MACxD;IACA,IAAS,MAAM;IACf,WAAW,MAAM,aACf,KACA,YACA,IACA,UACA,MACA,SACF;GACF;EAEJ,CAAC;EAED,IAAI,OAAO,WAAW,UAAU,cAAc,aAAa;CAC7D,SAAS,GAAG;EACV,IAAI,QAAQ,iCAAiC,CAAC;EAC9C,MAAM;CACR,UAAU;EACR,IAAS,MAAM;CACjB;AACF;AAEA,SAAS,OACP,yBACuB;CACvB,MAAM,oBAA2C,CAAC;CAClD,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,uBAAuB,GACzD,kBAAkB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;CAEvC,OAAO,kBAAkB,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC;AACnD;AAGA,IAAa,iBAAiB,eAAE,OAAO;;;;;;;;CAQrC,eAAe,eAAE,OAAO;;;;;;;;CASxB,aAAa,eAAE,OAAO;;;;;;CAOtB,gBAAgB,eAAE,OAAO;AAC3B,CAAC;AAMD,eAAsB,0BACpB,KACA,YACA;CAEA,MAAM,GAAG;kCACuB,IAAI,UAAU,EAAE;iCACjB,IAAI,UAAU,EAAE;;;;;;;;QAQzC,OAAO;AACf;AAEA,eAAe,qBACb,KACA,YACyB;CACzB,OAAO,KAAK,MAAM,kBAAkB,KAAK,YAAY,IAAI,CAAC;AAC5D;AAEA,eAAsB,kBACpB,KACA,YACA,SAAS,OACuB;CAMhC,KAAI,MALiB,GAAG;;;sBAGJ,WAAW,iBAAiB,oBAErC,WAAW,GACpB,IAAI,QACF,MAAM,0BAA0B,KAAK,UAAU;MAE/C,OAAO;CAGX,MAAM,OAAO,MAAM,GAAG;;cAEV,IAAI,UAAU,EAAE;CAE5B,IAAI,KAAK,WAAW,GAClB,OAAO,SACH;EAAC,eAAe;EAAG,aAAa;EAAG,gBAAgB;CAAC,IACpD;CAEN,OAAO,MAAQ,KAAK,IAAI,cAAc;AACxC;AAEA,eAAe,qBACb,KACA,KACA,YACA,MACA,YACA,gBACyB;CACzB,OAAO,aAAa,GAAG,6BAA6B;CACpD,MAAM,WAAW;EACf,aAAa;EAEb,eAAe,KAAK,IAAI,YAAY,KAAK,aAAa;EACtD,gBAAgB,kBAAkB,KAAK,MAAM,cAAc;CAC7D;CAEA,MAAM,GAAG;kBACO,IAAI,UAAU,EAAE,oBAAoB,IAAI,QAAQ,EAAE;yCAC3B,IAAI,QAAQ,EAAE;;CAErD,OAAO;AACT;AAEA,eAAe,aACb,KACA,YACA,IACA,UACA,oBACA,WACyB;CACzB,IAAI,SAAS,gBAAgB,oBAC3B,MAAM,UAAU,gBAAgB,KAAK,EAAE;CAEzC,IAAI,SAAS,cAAc,oBACzB,MAAM,UAAU,cAAc,KAAK,EAAE;CAEvC,OAAO,qBACL,KACA,IACA,YACA,UACA,oBACA,UAAU,cACZ;AACF;;;;;AAMA,SAAS,kBACP,KACA,SACA,qBACQ;CACR,IAAI,wBAAwB,KAAA,GAC1B,OAAO,QAAQ;CAEjB,IAAI,QAAQ,kBAAkB,qBAAqB;EAEjD,IAAI,QACF,gCAAgC,QAAQ,eAAe,0BAC5B,qBAC7B;EACA,OAAO,QAAQ;CACjB;CACA,IAAI,OACF,+BAA+B,QAAQ,eAAe,MAAM,qBAC9D;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"migration.js","names":[],"sources":["../../../../../zero-cache/src/db/migration.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../types/pg.ts';\nimport {runTx} from './run-transaction.ts';\n\ntype Operations = (log: LogContext, tx: PostgresTransaction) => Promise<void>;\n\n/**\n * Encapsulates the logic for setting up or upgrading to a new schema. After the\n * Migration code successfully completes, {@link runSchemaMigrations}\n * will update the schema version and commit the transaction.\n */\nexport type Migration = {\n /**\n * Perform database operations that create or alter table structure. This is\n * called at most once during lifetime of the application. If a `migrateData()`\n * operation is defined, that will be performed after `migrateSchema()` succeeds.\n */\n migrateSchema?: Operations;\n\n /**\n * Perform database operations to migrate data to the new schema. This is\n * called after `migrateSchema()` (if defined), and may be called again\n * to re-migrate data after the server was rolled back to an earlier version,\n * and rolled forward again.\n *\n * Consequently, the logic in `migrateData()` must be idempotent.\n */\n migrateData?: Operations;\n\n /**\n * Sets the `minSafeVersion` to the specified value, prohibiting running\n * any earlier code versions.\n */\n minSafeVersion?: number;\n};\n\n/**\n * Mapping of incremental migrations to move from the previous old code\n * version to next one. Versions must be non-zero.\n *\n * The schema resulting from performing incremental migrations should be\n * equivalent to that of the `setupMigration` on a blank database.\n *\n * The highest destinationVersion of this map denotes the current\n * \"code version\", and is also used as the destination version when\n * running the initial setup migration on a blank database.\n */\nexport type IncrementalMigrationMap = {\n [destinationVersion: number]: Migration;\n};\n\n/**\n * Ensures that the schema is compatible with the current code, updating and\n * migrating the schema if necessary.\n */\nexport async function runSchemaMigrations(\n log: LogContext,\n debugName: string,\n schemaName: string,\n db: PostgresDB,\n setupMigration: Migration,\n incrementalMigrationMap: IncrementalMigrationMap,\n): Promise<void> {\n log = log.withContext('initSchema', schemaName);\n\n const versionMigrations = sorted(incrementalMigrationMap);\n assert(\n versionMigrations.length,\n `Must specify at least one version migration`,\n );\n assert(\n versionMigrations[0][0] > 0,\n `Versions must be non-zero positive numbers`,\n );\n // oxlint-disable-next-line typescript/no-non-null-assertion\n const codeVersion = versionMigrations.at(-1)![0];\n\n log.info?.(\n `Checking schema for compatibility with ${debugName} at schema v${codeVersion}`,\n );\n\n try {\n await runTx(db, async tx => {\n // Acquire advisory lock to prevent concurrent migrations from racing.\n // This can happen during rolling deployments when multiple pods start\n // up simultaneously. The lock auto-releases when the transaction ends.\n const lockName = `migrate-schema:${schemaName}`;\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n let versions = await ensureVersionHistory(tx, schemaName);\n\n if (codeVersion < versions.minSafeVersion) {\n throw new Error(\n `Cannot run ${debugName} at schema v${codeVersion} because rollback limit is v${versions.minSafeVersion}`,\n );\n }\n\n if (versions.dataVersion > codeVersion) {\n log.info?.(\n `Data is at v${versions.dataVersion}. Resetting to v${codeVersion}`,\n );\n await updateVersionHistory(log, tx, schemaName, versions, codeVersion);\n return;\n }\n\n if (versions.dataVersion === codeVersion) {\n return;\n }\n\n const migrations =\n versions.dataVersion === 0\n ? // For an empty database (v0), only run the setup migration.\n ([[codeVersion, setupMigration]] as const)\n : versionMigrations;\n\n for (const [dest, migration] of migrations) {\n if (versions.dataVersion < dest) {\n log.info?.(\n `Migrating schema from v${versions.dataVersion} to v${dest}`,\n );\n void log.flush();\n versions = await runMigration(\n log,\n schemaName,\n tx,\n versions,\n dest,\n migration,\n );\n }\n }\n });\n\n log.info?.(`Running ${debugName} at schema v${codeVersion}`);\n } catch (e) {\n if (e instanceof AbortError) {\n // AbortErrors (e.g. AutoResetSignal) are not failures. They are the\n // expected mechanism for signaling that the schema is not initialized\n // (or otherwise needs a reset) so that the caller can backtrack and\n // resync. Logging them at `error` would trip error-based alerting for a\n // normal, self-healing path.\n log.warn?.('Schema requires reset; resync will follow', e);\n } else {\n log.error?.('Error in ensureSchemaMigrated', e);\n }\n throw e;\n } finally {\n void log.flush();\n }\n}\n\nfunction sorted(\n incrementalMigrationMap: IncrementalMigrationMap,\n): [number, Migration][] {\n const versionMigrations: [number, Migration][] = [];\n for (const [v, m] of Object.entries(incrementalMigrationMap)) {\n versionMigrations.push([Number(v), m]);\n }\n return versionMigrations.sort(([a], [b]) => a - b);\n}\n\n// Exposed for tests.\nexport const versionHistory = v.object({\n /**\n * The `schemaVersion` is highest code version that has ever been run\n * on the database, and is used to delineate the structure of the tables\n * in the database. A schemaVersion only moves forward; rolling back to\n * an earlier (safe) code version does not revert schema changes that\n * have already been applied.\n */\n schemaVersion: v.number(),\n\n /**\n * The data version is the code version of the latest server that ran.\n * Note that this may be less than the schemaVersion in the case that\n * a server is rolled back to an earlier version after a schema change.\n * In such a case, data (but not schema), may need to be re-migrated\n * when rolling forward again.\n */\n dataVersion: v.number(),\n\n /**\n * The minimum code version that is safe to run. This is used when\n * a schema migration is not backwards compatible with an older version\n * of the code.\n */\n minSafeVersion: v.number(),\n});\n\n// Exposed for tests.\nexport type VersionHistory = v.Infer<typeof versionHistory>;\n\n// Exposed for tests.\nexport async function createVersionHistoryTable(\n sql: postgres.Sql,\n schemaName: string,\n) {\n // Note: The `lock` column transparently ensures that at most one row exists.\n await sql`\n CREATE SCHEMA IF NOT EXISTS ${sql(schemaName)};\n CREATE TABLE IF NOT EXISTS ${sql(schemaName)}.\"versionHistory\" (\n \"dataVersion\" int NOT NULL,\n \"schemaVersion\" int NOT NULL,\n \"minSafeVersion\" int NOT NULL,\n\n lock char(1) NOT NULL CONSTRAINT DF_schema_meta_lock DEFAULT 'v',\n CONSTRAINT PK_schema_meta_lock PRIMARY KEY (lock),\n CONSTRAINT CK_schema_meta_lock CHECK (lock='v')\n );`.simple();\n}\n\nasync function ensureVersionHistory(\n sql: postgres.Sql,\n schemaName: string,\n): Promise<VersionHistory> {\n return must(await getVersionHistory(sql, schemaName, true));\n}\n\nexport async function getVersionHistory(\n sql: postgres.Sql,\n schemaName: string,\n create = false,\n): Promise<VersionHistory | null> {\n const exists = await sql`\n SELECT nspname, relname FROM pg_class\n JOIN pg_namespace ON relnamespace = pg_namespace.oid\n WHERE nspname = ${schemaName} AND relname = ${'versionHistory'}`;\n\n if (exists.length === 0) {\n if (create) {\n await createVersionHistoryTable(sql, schemaName);\n } else {\n return null;\n }\n }\n const rows = await sql`\n SELECT \"dataVersion\", \"schemaVersion\", \"minSafeVersion\"\n FROM ${sql(schemaName)}.\"versionHistory\"`;\n\n if (rows.length === 0) {\n return create\n ? {schemaVersion: 0, dataVersion: 0, minSafeVersion: 0}\n : null;\n }\n return v.parse(rows[0], versionHistory);\n}\n\nasync function updateVersionHistory(\n log: LogContext,\n sql: postgres.Sql,\n schemaName: string,\n prev: VersionHistory,\n newVersion: number,\n minSafeVersion?: number,\n): Promise<VersionHistory> {\n assert(newVersion > 0, 'newVersion must be positive');\n const versions = {\n dataVersion: newVersion,\n // The schemaVersion never moves backwards.\n schemaVersion: Math.max(newVersion, prev.schemaVersion),\n minSafeVersion: getMinSafeVersion(log, prev, minSafeVersion),\n } satisfies VersionHistory;\n\n await sql`\n INSERT INTO ${sql(schemaName)}.\"versionHistory\" ${sql(versions)}\n ON CONFLICT (lock) DO UPDATE SET ${sql(versions)}\n `;\n return versions;\n}\n\nasync function runMigration(\n log: LogContext,\n schemaName: string,\n tx: PostgresTransaction,\n versions: VersionHistory,\n destinationVersion: number,\n migration: Migration,\n): Promise<VersionHistory> {\n if (versions.schemaVersion < destinationVersion) {\n await migration.migrateSchema?.(log, tx);\n }\n if (versions.dataVersion < destinationVersion) {\n await migration.migrateData?.(log, tx);\n }\n return updateVersionHistory(\n log,\n tx,\n schemaName,\n versions,\n destinationVersion,\n migration.minSafeVersion,\n );\n}\n\n/**\n * Bumps the rollback limit [[toAtLeast]] the specified version.\n * Leaves the rollback limit unchanged if it is equal or greater.\n */\nfunction getMinSafeVersion(\n log: LogContext,\n current: VersionHistory,\n proposedSafeVersion?: number,\n): number {\n if (proposedSafeVersion === undefined) {\n return current.minSafeVersion;\n }\n if (current.minSafeVersion >= proposedSafeVersion) {\n // The rollback limit must never move backwards.\n log.debug?.(\n `rollback limit is already at ${current.minSafeVersion}, ` +\n `don't need to bump to ${proposedSafeVersion}`,\n );\n return current.minSafeVersion;\n }\n log.info?.(\n `bumping rollback limit from ${current.minSafeVersion} to ${proposedSafeVersion}`,\n );\n return proposedSafeVersion;\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { LogContext } from '@rocicorp/logger';
2
+ type DiagnosticTarget = {
3
+ readonly debugName: string;
4
+ readonly dbPath: string;
5
+ };
6
+ export declare function isSQLiteCorruption(e: unknown): boolean;
7
+ export declare function registerSQLiteCorruptionDiagnosticTarget(target: DiagnosticTarget): () => void;
8
+ export declare function logLastChanceSQLiteCorruptionDiagnostics(lc: LogContext, cause: unknown): boolean;
9
+ export declare function logSQLiteCorruptionDiagnostics(lc: LogContext, debugName: string, dbPath: string, cause: unknown): void;
10
+ export {};
11
+ //# sourceMappingURL=sqlite-corruption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-corruption.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/db/sqlite-corruption.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAiCjD,KAAK,gBAAgB,GAAG;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAKF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED,wBAAgB,wCAAwC,CACtD,MAAM,EAAE,gBAAgB,GACvB,MAAM,IAAI,CAIZ;AAED,wBAAgB,wCAAwC,CACtD,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,GACb,OAAO,CAQT;AAED,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,UAAU,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,IAAI,CA+CN"}
@@ -0,0 +1,191 @@
1
+ import { Database } from "../../../zqlite/src/db.js";
2
+ import { SqliteError } from "@rocicorp/zero-sqlite3";
3
+ import { existsSync, statSync } from "node:fs";
4
+ //#region ../zero-cache/src/db/sqlite-corruption.ts
5
+ var SQLITE_CORRUPTION_CODES = new Set(["SQLITE_CORRUPT", "SQLITE_NOTADB"]);
6
+ var SQLITE_CORRUPTION_MESSAGE = /database disk image is malformed|file is not a database|database is malformed|malformed database/i;
7
+ var SQLITE_FILE_SUFFIXES = [
8
+ "",
9
+ "-wal",
10
+ "-wal2",
11
+ "-shm",
12
+ "-journal"
13
+ ];
14
+ var MAX_LOGGED_CHECK_ISSUES = 20;
15
+ var diagnosticTargets = /* @__PURE__ */ new Map();
16
+ var nextDiagnosticTargetID = 0;
17
+ function isSQLiteCorruption(e) {
18
+ return getSQLiteCorruptionCode(e) !== void 0;
19
+ }
20
+ function registerSQLiteCorruptionDiagnosticTarget(target) {
21
+ const id = nextDiagnosticTargetID++;
22
+ diagnosticTargets.set(id, target);
23
+ return () => diagnosticTargets.delete(id);
24
+ }
25
+ function logLastChanceSQLiteCorruptionDiagnostics(lc, cause) {
26
+ if (!isSQLiteCorruption(cause) || diagnosticTargets.size === 0) return false;
27
+ for (const { debugName, dbPath } of diagnosticTargets.values()) logSQLiteCorruptionDiagnostics(lc, debugName, dbPath, cause);
28
+ return true;
29
+ }
30
+ function logSQLiteCorruptionDiagnostics(lc, debugName, dbPath, cause) {
31
+ const start = Date.now();
32
+ lc.error?.("SQLite replica corruption detected", {
33
+ debugName,
34
+ dbPath,
35
+ cause: summarizeError(cause)
36
+ });
37
+ logDiagnostic(lc, debugName, "file-stats", () => ({ files: getSQLiteFileStats(dbPath) }));
38
+ let db;
39
+ try {
40
+ db = new Database(lc, dbPath, { readonly: true });
41
+ db.pragma("busy_timeout = 1000");
42
+ } catch (e) {
43
+ lc.error?.("SQLite corruption diagnostic failed", {
44
+ debugName,
45
+ step: "open-readonly",
46
+ durationMs: Date.now() - start,
47
+ error: summarizeError(e)
48
+ });
49
+ return;
50
+ }
51
+ try {
52
+ logDiagnostic(lc, debugName, "basic-pragmas", () => ({ pragmas: getBasicPragmas(db) }));
53
+ logDiagnostic(lc, debugName, "replica-metadata", () => ({ metadata: getReplicaMetadata(db) }));
54
+ logDiagnostic(lc, debugName, "quick-check", () => ({ check: runCheck(db, "quick_check") }));
55
+ logDiagnostic(lc, debugName, "integrity-check", () => ({ check: runCheck(db, "integrity_check") }));
56
+ lc.error?.("SQLite corruption diagnostics completed", {
57
+ debugName,
58
+ dbPath,
59
+ durationMs: Date.now() - start
60
+ });
61
+ } finally {
62
+ db.close();
63
+ }
64
+ }
65
+ function getSQLiteCorruptionCode(e) {
66
+ let current = e;
67
+ const seen = /* @__PURE__ */ new Set();
68
+ while (current !== void 0 && current !== null && !seen.has(current)) {
69
+ seen.add(current);
70
+ const code = getErrorCode(current);
71
+ if (code !== void 0 && SQLITE_CORRUPTION_CODES.has(code)) return code;
72
+ if (current instanceof SqliteError || current instanceof Error) {
73
+ if (SQLITE_CORRUPTION_MESSAGE.test(current.message)) return code ?? "SQLITE_CORRUPT";
74
+ current = getErrorCause(current);
75
+ continue;
76
+ }
77
+ break;
78
+ }
79
+ }
80
+ function getErrorCode(e) {
81
+ if (e && typeof e === "object" && "code" in e) {
82
+ const code = e.code;
83
+ return typeof code === "string" ? code : void 0;
84
+ }
85
+ }
86
+ function getErrorCause(e) {
87
+ return e.cause;
88
+ }
89
+ function summarizeError(e, depth = 0) {
90
+ if (!(e instanceof Error)) return String(e);
91
+ const cause = getErrorCause(e);
92
+ return {
93
+ name: e.name,
94
+ message: e.message,
95
+ code: getErrorCode(e),
96
+ cause: cause === void 0 || depth >= 4 ? void 0 : summarizeError(cause, depth + 1)
97
+ };
98
+ }
99
+ function getSQLiteFileStats(dbPath) {
100
+ const stats = [];
101
+ for (const suffix of SQLITE_FILE_SUFFIXES) {
102
+ const path = `${dbPath}${suffix}`;
103
+ if (!existsSync(path)) continue;
104
+ const stat = statSync(path);
105
+ stats.push({
106
+ suffix,
107
+ path,
108
+ sizeBytes: stat.size,
109
+ mtimeMs: stat.mtimeMs
110
+ });
111
+ }
112
+ return stats;
113
+ }
114
+ function getBasicPragmas(db) {
115
+ return {
116
+ pageSize: firstPragmaValue(db, "page_size", "page_size"),
117
+ pageCount: firstPragmaValue(db, "page_count", "page_count"),
118
+ freelistCount: firstPragmaValue(db, "freelist_count", "freelist_count"),
119
+ journalMode: firstPragmaValue(db, "journal_mode", "journal_mode"),
120
+ schemaVersion: firstPragmaValue(db, "schema_version", "schema_version"),
121
+ userVersion: firstPragmaValue(db, "user_version", "user_version"),
122
+ applicationID: firstPragmaValue(db, "application_id", "application_id")
123
+ };
124
+ }
125
+ function getReplicaMetadata(db) {
126
+ return {
127
+ versionHistory: queryDiagnostic(() => db.prepare(`SELECT dataVersion, schemaVersion, minSafeVersion
128
+ FROM "_zero.versionHistory"`).get()),
129
+ subscriptionState: queryDiagnostic(() => db.prepare(`SELECT c.replicaVersion, s.stateVersion as watermark
130
+ FROM "_zero.replicationConfig" as c
131
+ JOIN "_zero.replicationState" as s
132
+ ON c.lock = s.lock`).get()),
133
+ sqliteCatalogCounts: queryDiagnostic(() => db.prepare(`SELECT type, count(*) as count
134
+ FROM sqlite_master
135
+ GROUP BY type
136
+ ORDER BY type`).all())
137
+ };
138
+ }
139
+ function queryDiagnostic(query) {
140
+ try {
141
+ return {
142
+ ok: true,
143
+ value: query()
144
+ };
145
+ } catch (e) {
146
+ return {
147
+ ok: false,
148
+ error: summarizeError(e)
149
+ };
150
+ }
151
+ }
152
+ function firstPragmaValue(db, pragma, column) {
153
+ const [row] = db.pragma(pragma);
154
+ return row?.[column];
155
+ }
156
+ function runCheck(db, pragma) {
157
+ const rows = db.pragma(pragma);
158
+ const issues = rows.map((row) => row[pragma] ?? Object.values(row)[0] ?? "").filter((issue) => issue !== "" && issue !== "ok");
159
+ const loggedIssues = issues.slice(0, MAX_LOGGED_CHECK_ISSUES);
160
+ return {
161
+ status: issues.length === 0 ? "ok" : "failed",
162
+ rowCount: rows.length,
163
+ issueCount: issues.length,
164
+ loggedIssueCount: loggedIssues.length,
165
+ truncated: issues.length > loggedIssues.length,
166
+ issues: loggedIssues
167
+ };
168
+ }
169
+ function logDiagnostic(lc, debugName, step, fn) {
170
+ const start = Date.now();
171
+ try {
172
+ const result = fn();
173
+ lc.error?.("SQLite corruption diagnostic", {
174
+ debugName,
175
+ step,
176
+ durationMs: Date.now() - start,
177
+ result
178
+ });
179
+ } catch (e) {
180
+ lc.error?.("SQLite corruption diagnostic failed", {
181
+ debugName,
182
+ step,
183
+ durationMs: Date.now() - start,
184
+ error: summarizeError(e)
185
+ });
186
+ }
187
+ }
188
+ //#endregion
189
+ export { isSQLiteCorruption, logLastChanceSQLiteCorruptionDiagnostics, logSQLiteCorruptionDiagnostics, registerSQLiteCorruptionDiagnosticTarget };
190
+
191
+ //# sourceMappingURL=sqlite-corruption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-corruption.js","names":[],"sources":["../../../../../zero-cache/src/db/sqlite-corruption.ts"],"sourcesContent":["import {existsSync, statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {Database} from '../../../zqlite/src/db.ts';\n\nconst SQLITE_CORRUPTION_CODES = new Set(['SQLITE_CORRUPT', 'SQLITE_NOTADB']);\nconst SQLITE_CORRUPTION_MESSAGE =\n /database disk image is malformed|file is not a database|database is malformed|malformed database/i;\nconst SQLITE_FILE_SUFFIXES = ['', '-wal', '-wal2', '-shm', '-journal'] as const;\nconst MAX_LOGGED_CHECK_ISSUES = 20;\n\ntype ErrorSummary = {\n readonly name: string;\n readonly message: string;\n readonly code?: string | undefined;\n readonly cause?: ErrorSummary | string | undefined;\n};\n\ntype SQLiteFileStats = {\n readonly suffix: string;\n readonly path: string;\n readonly sizeBytes: number;\n readonly mtimeMs: number;\n};\n\ntype CheckSummary = {\n readonly status: 'ok' | 'failed';\n readonly rowCount: number;\n readonly issueCount: number;\n readonly loggedIssueCount: number;\n readonly truncated: boolean;\n readonly issues: string[];\n};\n\ntype DiagnosticTarget = {\n readonly debugName: string;\n readonly dbPath: string;\n};\n\nconst diagnosticTargets = new Map<number, DiagnosticTarget>();\nlet nextDiagnosticTargetID = 0;\n\nexport function isSQLiteCorruption(e: unknown): boolean {\n return getSQLiteCorruptionCode(e) !== undefined;\n}\n\nexport function registerSQLiteCorruptionDiagnosticTarget(\n target: DiagnosticTarget,\n): () => void {\n const id = nextDiagnosticTargetID++;\n diagnosticTargets.set(id, target);\n return () => diagnosticTargets.delete(id);\n}\n\nexport function logLastChanceSQLiteCorruptionDiagnostics(\n lc: LogContext,\n cause: unknown,\n): boolean {\n if (!isSQLiteCorruption(cause) || diagnosticTargets.size === 0) {\n return false;\n }\n for (const {debugName, dbPath} of diagnosticTargets.values()) {\n logSQLiteCorruptionDiagnostics(lc, debugName, dbPath, cause);\n }\n return true;\n}\n\nexport function logSQLiteCorruptionDiagnostics(\n lc: LogContext,\n debugName: string,\n dbPath: string,\n cause: unknown,\n): void {\n const start = Date.now();\n lc.error?.('SQLite replica corruption detected', {\n debugName,\n dbPath,\n cause: summarizeError(cause),\n });\n\n logDiagnostic(lc, debugName, 'file-stats', () => ({\n files: getSQLiteFileStats(dbPath),\n }));\n\n let db: Database | undefined;\n try {\n db = new Database(lc, dbPath, {readonly: true});\n db.pragma('busy_timeout = 1000');\n } catch (e) {\n lc.error?.('SQLite corruption diagnostic failed', {\n debugName,\n step: 'open-readonly',\n durationMs: Date.now() - start,\n error: summarizeError(e),\n });\n return;\n }\n\n try {\n logDiagnostic(lc, debugName, 'basic-pragmas', () => ({\n pragmas: getBasicPragmas(db),\n }));\n logDiagnostic(lc, debugName, 'replica-metadata', () => ({\n metadata: getReplicaMetadata(db),\n }));\n logDiagnostic(lc, debugName, 'quick-check', () => ({\n check: runCheck(db, 'quick_check'),\n }));\n logDiagnostic(lc, debugName, 'integrity-check', () => ({\n check: runCheck(db, 'integrity_check'),\n }));\n lc.error?.('SQLite corruption diagnostics completed', {\n debugName,\n dbPath,\n durationMs: Date.now() - start,\n });\n } finally {\n db.close();\n }\n}\n\nfunction getSQLiteCorruptionCode(e: unknown): string | undefined {\n let current = e;\n const seen = new Set<unknown>();\n\n while (current !== undefined && current !== null && !seen.has(current)) {\n seen.add(current);\n\n const code = getErrorCode(current);\n if (code !== undefined && SQLITE_CORRUPTION_CODES.has(code)) {\n return code;\n }\n\n if (current instanceof SqliteError || current instanceof Error) {\n if (SQLITE_CORRUPTION_MESSAGE.test(current.message)) {\n return code ?? 'SQLITE_CORRUPT';\n }\n current = getErrorCause(current);\n continue;\n }\n\n break;\n }\n\n return undefined;\n}\n\nfunction getErrorCode(e: unknown): string | undefined {\n if (e && typeof e === 'object' && 'code' in e) {\n const code = (e as {readonly code?: unknown}).code;\n return typeof code === 'string' ? code : undefined;\n }\n return undefined;\n}\n\nfunction getErrorCause(e: Error): unknown {\n return (e as {readonly cause?: unknown}).cause;\n}\n\nfunction summarizeError(e: unknown, depth = 0): ErrorSummary | string {\n if (!(e instanceof Error)) {\n return String(e);\n }\n const cause = getErrorCause(e);\n return {\n name: e.name,\n message: e.message,\n code: getErrorCode(e),\n cause:\n cause === undefined || depth >= 4\n ? undefined\n : summarizeError(cause, depth + 1),\n };\n}\n\nfunction getSQLiteFileStats(dbPath: string): SQLiteFileStats[] {\n const stats: SQLiteFileStats[] = [];\n for (const suffix of SQLITE_FILE_SUFFIXES) {\n const path = `${dbPath}${suffix}`;\n if (!existsSync(path)) {\n continue;\n }\n const stat = statSync(path);\n stats.push({\n suffix,\n path,\n sizeBytes: stat.size,\n mtimeMs: stat.mtimeMs,\n });\n }\n return stats;\n}\n\nfunction getBasicPragmas(db: Database) {\n return {\n pageSize: firstPragmaValue<number>(db, 'page_size', 'page_size'),\n pageCount: firstPragmaValue<number>(db, 'page_count', 'page_count'),\n freelistCount: firstPragmaValue<number>(\n db,\n 'freelist_count',\n 'freelist_count',\n ),\n journalMode: firstPragmaValue<string>(db, 'journal_mode', 'journal_mode'),\n schemaVersion: firstPragmaValue<number>(\n db,\n 'schema_version',\n 'schema_version',\n ),\n userVersion: firstPragmaValue<number>(db, 'user_version', 'user_version'),\n applicationID: firstPragmaValue<number>(\n db,\n 'application_id',\n 'application_id',\n ),\n };\n}\n\nfunction getReplicaMetadata(db: Database) {\n return {\n versionHistory: queryDiagnostic(() =>\n db\n .prepare(\n `SELECT dataVersion, schemaVersion, minSafeVersion\n FROM \"_zero.versionHistory\"`,\n )\n .get(),\n ),\n subscriptionState: queryDiagnostic(() =>\n db\n .prepare(\n `SELECT c.replicaVersion, s.stateVersion as watermark\n FROM \"_zero.replicationConfig\" as c\n JOIN \"_zero.replicationState\" as s\n ON c.lock = s.lock`,\n )\n .get(),\n ),\n sqliteCatalogCounts: queryDiagnostic(() =>\n db\n .prepare(\n `SELECT type, count(*) as count\n FROM sqlite_master\n GROUP BY type\n ORDER BY type`,\n )\n .all(),\n ),\n };\n}\n\nfunction queryDiagnostic<T>(query: () => T):\n | {readonly ok: true; readonly value: T}\n | {\n readonly ok: false;\n readonly error: ErrorSummary | string;\n } {\n try {\n return {ok: true, value: query()};\n } catch (e) {\n return {ok: false, error: summarizeError(e)};\n }\n}\n\nfunction firstPragmaValue<T>(\n db: Database,\n pragma: string,\n column: string,\n): T | undefined {\n const [row] = db.pragma<Record<string, T>>(pragma);\n return row?.[column];\n}\n\nfunction runCheck(db: Database, pragma: 'quick_check' | 'integrity_check') {\n const rows = db.pragma<Record<string, string>>(pragma);\n const issues = rows\n .map(row => row[pragma] ?? Object.values(row)[0] ?? '')\n .filter(issue => issue !== '' && issue !== 'ok');\n const loggedIssues = issues.slice(0, MAX_LOGGED_CHECK_ISSUES);\n return {\n status: issues.length === 0 ? 'ok' : 'failed',\n rowCount: rows.length,\n issueCount: issues.length,\n loggedIssueCount: loggedIssues.length,\n truncated: issues.length > loggedIssues.length,\n issues: loggedIssues,\n } satisfies CheckSummary;\n}\n\nfunction logDiagnostic<T>(\n lc: LogContext,\n debugName: string,\n step: string,\n fn: () => T,\n): void {\n const start = Date.now();\n try {\n const result = fn();\n lc.error?.('SQLite corruption diagnostic', {\n debugName,\n step,\n durationMs: Date.now() - start,\n result,\n });\n } catch (e) {\n lc.error?.('SQLite corruption diagnostic failed', {\n debugName,\n step,\n durationMs: Date.now() - start,\n error: summarizeError(e),\n });\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { LogContext } from '@rocicorp/logger';
2
- import { type ZeroConfig } from '../config/zero-config.js';
2
+ import { type ZeroConfig } from '../config/zero-config.ts';
3
3
  export type ActiveUsers = {
4
4
  active_users_last_day: number;
5
5
  users_1da: number;
@@ -1 +1 @@
1
- {"version":3,"file":"anonymous-otel-start.js","names":["#instance","#processId","#lc","#starting","#config","#viewSyncerCount","#cachedAttributes","#run","#stopped","#getAttributes","#meterProvider","#meter","#setupMetrics","#activeUsersGetter","#totalCrudMutations","#totalCustomMutations","#totalCrudQueries","#totalCustomQueries","#totalRowsSynced","#totalConnectionsSuccess","#totalConnectionsAttempted","#activeClientGroupsGetter","#getPlatform","#getGitProjectId","#getOrSetFsID","#findUp","#isInContainer"],"sources":["../../../../../zero-cache/src/server/anonymous-otel-start.ts"],"sourcesContent":["import {execSync} from 'child_process';\nimport {randomUUID} from 'crypto';\nimport {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';\nimport {homedir, platform} from 'os';\nimport {dirname, join} from 'path';\nimport type {ObservableResult} from '@opentelemetry/api';\nimport {type Meter} from '@opentelemetry/api';\nimport {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-http';\nimport {resourceFromAttributes} from '@opentelemetry/resources';\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport type {LogContext} from '@rocicorp/logger';\nimport {h64} from '../../../shared/src/hash.js';\nimport {\n getServerVersion,\n getZeroConfig,\n type ZeroConfig,\n} from '../config/zero-config.js';\nimport {setupOtelDiagnosticLogger} from './otel-diag-logger.ts';\n\nexport type ActiveUsers = {\n active_users_last_day: number;\n users_1da: number;\n users_7da: number;\n users_30da: number;\n users_1da_legacy: number;\n users_7da_legacy: number;\n users_30da_legacy: number;\n};\n\nconst hostNameRe = /^[a-f0-9]{12}$/;\n\nclass AnonymousTelemetryManager {\n static #instance: AnonymousTelemetryManager;\n #starting = false;\n #stopped = false;\n #meter!: Meter;\n #meterProvider!: MeterProvider;\n #totalCrudMutations = 0;\n #totalCustomMutations = 0;\n #totalCrudQueries = 0;\n #totalCustomQueries = 0;\n #totalRowsSynced = 0;\n #totalConnectionsSuccess = 0;\n #totalConnectionsAttempted = 0;\n #activeClientGroupsGetter: (() => number) | undefined;\n #activeUsersGetter: (() => ActiveUsers) | undefined;\n #lc: LogContext | undefined;\n #config: ZeroConfig | undefined;\n #processId: string;\n #cachedAttributes: Record<string, string> | undefined;\n #viewSyncerCount = 1;\n\n private constructor() {\n this.#processId = randomUUID();\n }\n\n static getInstance(): AnonymousTelemetryManager {\n if (!AnonymousTelemetryManager.#instance) {\n AnonymousTelemetryManager.#instance = new AnonymousTelemetryManager();\n }\n return AnonymousTelemetryManager.#instance;\n }\n\n start(lc?: LogContext, config?: ZeroConfig) {\n this.#lc = lc;\n\n // Set up OpenTelemetry diagnostic logger if not already configured\n setupOtelDiagnosticLogger(lc);\n\n if (!config) {\n try {\n config = getZeroConfig();\n } catch (e) {\n this.#lc?.info?.('telemetry: disabled - unable to parse config', e);\n return;\n }\n }\n\n if (process.env.DO_NOT_TRACK) {\n this.#lc?.info?.(\n 'telemetry: disabled - DO_NOT_TRACK environment variable is set',\n );\n return;\n }\n\n if (!config.enableTelemetry) {\n this.#lc?.info?.('telemetry: disabled - enableTelemetry is false');\n return;\n }\n\n if (this.#starting) {\n return;\n }\n\n this.#starting = true;\n this.#config = config;\n this.#viewSyncerCount = config.numSyncWorkers ?? 1;\n this.#cachedAttributes = undefined;\n\n this.#lc?.info?.(`telemetry: starting in 1 minute`);\n\n // Delay telemetry startup by 1 minute to avoid potential boot loop issues\n setTimeout(() => this.#run(), 60000);\n }\n\n #run() {\n if (this.#stopped) {\n return;\n }\n\n const resource = resourceFromAttributes(this.#getAttributes());\n\n // Add a random jitter to the export interval to avoid all view-syncers exporting at the same time\n const exportIntervalMillis =\n 60000 * this.#viewSyncerCount + Math.floor(Math.random() * 10000);\n const readers = [\n new PeriodicExportingMetricReader({\n exportIntervalMillis,\n exporter: new OTLPMetricExporter({\n url: 'https://metrics.rocicorp.dev',\n timeoutMillis: 30000,\n }),\n }),\n ];\n\n // Uncomment this to debug metrics exports.\n\n // readers.push(\n // new PeriodicExportingMetricReader({\n // exportIntervalMillis,\n // exporter: new ConsoleMetricExporter(),\n // }),\n // );\n\n this.#meterProvider = new MeterProvider({resource, readers});\n this.#meter = this.#meterProvider.getMeter('zero-anonymous-telemetry');\n\n this.#setupMetrics();\n this.#lc?.info?.(\n `telemetry: started (exports every ${exportIntervalMillis / 1000} seconds for ${this.#viewSyncerCount} view-syncers)`,\n );\n }\n\n #setupMetrics() {\n // Observable gauges\n const uptimeGauge = this.#meter.createObservableGauge('zero.uptime', {\n description: 'System uptime in seconds',\n unit: 'seconds',\n });\n\n // Observable counters\n const uptimeCounter = this.#meter.createObservableCounter(\n 'zero.uptime_counter',\n {\n description: 'System uptime in seconds',\n unit: 'seconds',\n },\n );\n const crudMutationsCounter = this.#meter.createObservableCounter(\n 'zero.crud_mutations_processed',\n {\n description: 'Total number of CRUD mutations processed',\n },\n );\n const customMutationsCounter = this.#meter.createObservableCounter(\n 'zero.custom_mutations_processed',\n {\n description: 'Total number of custom mutations processed',\n },\n );\n const totalMutationsCounter = this.#meter.createObservableCounter(\n 'zero.mutations_processed',\n {\n description: 'Total number of mutations processed',\n },\n );\n const crudQueriesCounter = this.#meter.createObservableCounter(\n 'zero.crud_queries_processed',\n {\n description: 'Total number of CRUD queries processed',\n },\n );\n const customQueriesCounter = this.#meter.createObservableCounter(\n 'zero.custom_queries_processed',\n {\n description: 'Total number of custom queries processed',\n },\n );\n const totalQueriesCounter = this.#meter.createObservableCounter(\n 'zero.queries_processed',\n {\n description: 'Total number of queries processed',\n },\n );\n const rowsSyncedCounter = this.#meter.createObservableCounter(\n 'zero.rows_synced',\n {\n description: 'Total number of rows synced',\n },\n );\n\n // Observable counters for connections\n const connectionsSuccessCounter = this.#meter.createObservableCounter(\n 'zero.connections_success',\n {\n description: 'Total number of successful connections',\n },\n );\n\n const connectionsAttemptedCounter = this.#meter.createObservableCounter(\n 'zero.connections_attempted',\n {\n description: 'Total number of attempted connections',\n },\n );\n\n const activeClientGroupsGauge = this.#meter.createObservableGauge(\n 'zero.gauge_active_client_groups',\n {\n description: 'Number of currently active client groups',\n },\n );\n\n const attrs = this.#getAttributes();\n const active =\n (metric: keyof ActiveUsers) => (result: ObservableResult) => {\n const actives = this.#activeUsersGetter?.();\n if (actives) {\n const value = actives[metric];\n result.observe(value, attrs);\n }\n };\n this.#meter\n .createObservableGauge('zero.active_users_last_day', {\n description: 'Count of CVR instances active in the last 24h',\n })\n .addCallback(active('active_users_last_day'));\n this.#meter\n .createObservableGauge('zero.users_1da', {\n description: 'Count of 1-day active profiles',\n })\n .addCallback(active('users_1da'));\n this.#meter\n .createObservableGauge('zero.users_7da', {\n description: 'Count of 7-day active profiles',\n })\n .addCallback(active('users_7da'));\n this.#meter\n .createObservableGauge('zero.users_30da', {\n description: 'Count of 30-day active profiles',\n })\n .addCallback(active('users_30da'));\n this.#meter\n .createObservableGauge('zero.users_1da_legacy', {\n description: 'Count of 1-day active profiles with CVR fallback',\n })\n .addCallback(active('users_1da_legacy'));\n this.#meter\n .createObservableGauge('zero.users_7da_legacy', {\n description: 'Count of 7-day active profiles with CVR fallback',\n })\n .addCallback(active('users_7da_legacy'));\n this.#meter\n .createObservableGauge('zero.users_30da_legacy', {\n description: 'Count of 30-day active profiles with CVR fallback',\n })\n .addCallback(active('users_30da_legacy'));\n\n // Callbacks\n uptimeGauge.addCallback((result: ObservableResult) => {\n const uptimeSeconds = Math.floor(process.uptime());\n result.observe(uptimeSeconds, attrs);\n });\n uptimeCounter.addCallback((result: ObservableResult) => {\n const uptimeSeconds = Math.floor(process.uptime());\n result.observe(uptimeSeconds, attrs);\n });\n crudMutationsCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCrudMutations, attrs);\n });\n customMutationsCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCustomMutations, attrs);\n });\n totalMutationsCounter.addCallback((result: ObservableResult) => {\n const totalMutations =\n this.#totalCrudMutations + this.#totalCustomMutations;\n result.observe(totalMutations, attrs);\n });\n crudQueriesCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCrudQueries, attrs);\n });\n customQueriesCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCustomQueries, attrs);\n });\n totalQueriesCounter.addCallback((result: ObservableResult) => {\n const totalQueries = this.#totalCrudQueries + this.#totalCustomQueries;\n result.observe(totalQueries, attrs);\n });\n rowsSyncedCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalRowsSynced, attrs);\n });\n connectionsSuccessCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalConnectionsSuccess, attrs);\n });\n connectionsAttemptedCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalConnectionsAttempted, attrs);\n });\n activeClientGroupsGauge.addCallback((result: ObservableResult) => {\n const activeClientGroups = this.#activeClientGroupsGetter?.() ?? 0;\n result.observe(activeClientGroups, attrs);\n });\n }\n\n recordMutation(type: 'crud' | 'custom', count = 1) {\n if (type === 'crud') {\n this.#totalCrudMutations += count;\n } else {\n this.#totalCustomMutations += count;\n }\n }\n\n recordQuery(type: 'crud' | 'custom', count = 1) {\n if (type === 'crud') {\n this.#totalCrudQueries += count;\n } else {\n this.#totalCustomQueries += count;\n }\n }\n\n recordRowsSynced(count: number) {\n this.#totalRowsSynced += count;\n }\n\n recordConnectionSuccess() {\n this.#totalConnectionsSuccess++;\n }\n\n recordConnectionAttempted() {\n this.#totalConnectionsAttempted++;\n }\n\n setActiveClientGroupsGetter(getter: () => number) {\n this.#activeClientGroupsGetter = getter;\n }\n\n setActiveUsersGetter(getter: () => ActiveUsers) {\n this.#activeUsersGetter = getter;\n }\n\n shutdown() {\n this.#stopped = true;\n if (this.#meterProvider) {\n this.#lc?.info?.('telemetry: shutting down');\n void this.#meterProvider.shutdown();\n }\n }\n\n #getAttributes() {\n if (!this.#cachedAttributes) {\n this.#cachedAttributes = {\n 'zero.app.id': h64(this.#config?.upstream.db || 'unknown').toString(),\n 'zero.machine.os': platform(),\n 'zero.telemetry.type': 'anonymous',\n 'zero.infra.platform': this.#getPlatform(),\n 'zero.version': getServerVersion(this.#config),\n 'zero.task.id': this.#config?.taskID || 'unknown',\n 'zero.project.id': this.#getGitProjectId(),\n 'zero.process.id': this.#processId,\n 'zero.fs.id': this.#getOrSetFsID(),\n };\n this.#lc?.debug?.(\n `telemetry: cached attributes=${JSON.stringify(this.#cachedAttributes)}`,\n );\n }\n return this.#cachedAttributes;\n }\n\n #getPlatform(): string {\n if (process.env.ZERO_ON_CLOUD_ZERO) return 'cloudzero';\n if (process.env.FLY_APP_NAME || process.env.FLY_REGION) return 'fly.io';\n if (\n process.env.ECS_CONTAINER_METADATA_URI_V4 ||\n process.env.ECS_CONTAINER_METADATA_URI ||\n process.env.AWS_EXECUTION_ENV\n ) {\n return 'aws';\n }\n if (process.env.RAILWAY_ENV || process.env.RAILWAY_STATIC_URL) {\n return 'railway';\n }\n if (process.env.RENDER || process.env.RENDER_SERVICE_ID) return 'render';\n if (\n process.env.GCP_PROJECT ||\n process.env.GCLOUD_PROJECT ||\n process.env.GOOGLE_CLOUD_PROJECT\n ) {\n return 'gcp';\n }\n if (process.env.COOLIFY_URL || process.env.COOLIFY_CONTAINER_NAME) {\n return 'coolify';\n }\n if (process.env.CONTAINER_APP_REVISION) return 'azure';\n if (process.env.FLIGHTCONTROL || process.env.FC_URL) return 'flightcontrol';\n return 'unknown';\n }\n\n #findUp(startDir: string, target: string): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n if (existsSync(join(dir, target))) return dir;\n dir = dirname(dir);\n }\n return null;\n }\n\n #getGitProjectId(): string {\n try {\n const cwd = process.cwd();\n const gitRoot = this.#findUp(cwd, '.git');\n if (!gitRoot) {\n return 'unknown';\n }\n\n const rootCommitHash = execSync('git rev-list --max-parents=0 HEAD -1', {\n cwd: gitRoot,\n encoding: 'utf8',\n timeout: 1000,\n stdio: ['ignore', 'pipe', 'ignore'], // Suppress stderr\n }).trim();\n\n return rootCommitHash.length === 40 ? rootCommitHash : 'unknown';\n } catch (error) {\n // execSync throws a child_process.SpawnSyncReturns-shaped error with an\n // output property (an array with stdio buffers) and sometimes a killed\n // property that references back to the error itself — making it\n // circular and causing stringify() in the Logger logic to throw.\n const details =\n error instanceof Error\n ? {message: error.message, name: error.name, stack: error.stack}\n : String(error);\n this.#lc?.debug?.('telemetry: unable to get Git root commit:', details);\n return 'unknown';\n }\n }\n\n #getOrSetFsID(): string {\n try {\n if (this.#isInContainer()) {\n return 'container';\n }\n const fsidPath = join(homedir(), '.rocicorp', 'fsid');\n const fsidDir = dirname(fsidPath);\n\n mkdirSync(fsidDir, {recursive: true});\n\n // Always try atomic file creation first - this eliminates any race conditions\n const newId = randomUUID();\n try {\n writeFileSync(fsidPath, newId, {encoding: 'utf8', flag: 'wx'});\n return newId;\n } catch (writeError) {\n if ((writeError as NodeJS.ErrnoException).code === 'EEXIST') {\n const existingId = readFileSync(fsidPath, 'utf8').trim();\n return existingId;\n }\n throw writeError;\n }\n } catch (error) {\n this.#lc?.debug?.(\n 'telemetry: unable to get or set filesystem ID:',\n error,\n );\n return 'unknown';\n }\n }\n\n #isInContainer(): boolean {\n try {\n if (process.env.ZERO_IN_CONTAINER) {\n return true;\n }\n\n if (existsSync('/.dockerenv')) {\n return true;\n }\n\n if (existsSync('/usr/local/bin/docker-entrypoint.sh')) {\n return true;\n }\n\n if (process.env.KUBERNETES_SERVICE_HOST) {\n return true;\n }\n\n if (\n process.env.DOCKER_CONTAINER_ID ||\n process.env.HOSTNAME?.match(hostNameRe)\n ) {\n return true;\n }\n\n if (existsSync('/proc/1/cgroup')) {\n const cgroup = readFileSync('/proc/1/cgroup', 'utf8');\n if (\n cgroup.includes('docker') ||\n cgroup.includes('kubepods') ||\n cgroup.includes('containerd')\n ) {\n return true;\n }\n }\n\n return false;\n } catch (error) {\n this.#lc?.debug?.(\n 'telemetry: unable to detect container environment:',\n error,\n );\n return false;\n }\n }\n}\n\nconst manager = () => AnonymousTelemetryManager.getInstance();\n\nexport const startAnonymousTelemetry = (lc?: LogContext, config?: ZeroConfig) =>\n manager().start(lc, config);\nexport const recordMutation = (type: 'crud' | 'custom', count = 1) =>\n manager().recordMutation(type, count);\nexport const recordQuery = (type: 'crud' | 'custom', count = 1) =>\n manager().recordQuery(type, count);\nexport const recordRowsSynced = (count: number) =>\n manager().recordRowsSynced(count);\nexport const recordConnectionSuccess = () =>\n manager().recordConnectionSuccess();\nexport const recordConnectionAttempted = () =>\n manager().recordConnectionAttempted();\nexport const setActiveClientGroupsGetter = (getter: () => number) =>\n manager().setActiveClientGroupsGetter(getter);\nexport const setActiveUsersGetter = (getter: () => ActiveUsers) =>\n manager().setActiveUsersGetter(getter);\nexport const shutdownAnonymousTelemetry = () => manager().shutdown();\n"],"mappings":"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"}
1
+ {"version":3,"file":"anonymous-otel-start.js","names":["#instance","#processId","#lc","#starting","#config","#viewSyncerCount","#cachedAttributes","#run","#stopped","#getAttributes","#meterProvider","#meter","#setupMetrics","#activeUsersGetter","#totalCrudMutations","#totalCustomMutations","#totalCrudQueries","#totalCustomQueries","#totalRowsSynced","#totalConnectionsSuccess","#totalConnectionsAttempted","#activeClientGroupsGetter","#getPlatform","#getGitProjectId","#getOrSetFsID","#findUp","#isInContainer"],"sources":["../../../../../zero-cache/src/server/anonymous-otel-start.ts"],"sourcesContent":["import {execSync} from 'child_process';\nimport {randomUUID} from 'crypto';\nimport {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';\nimport {homedir, platform} from 'os';\nimport {dirname, join} from 'path';\nimport type {ObservableResult} from '@opentelemetry/api';\nimport {type Meter} from '@opentelemetry/api';\nimport {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-http';\nimport {resourceFromAttributes} from '@opentelemetry/resources';\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from '@opentelemetry/sdk-metrics';\nimport type {LogContext} from '@rocicorp/logger';\nimport {h64} from '../../../shared/src/hash.ts';\nimport {\n getServerVersion,\n getZeroConfig,\n type ZeroConfig,\n} from '../config/zero-config.ts';\nimport {setupOtelDiagnosticLogger} from './otel-diag-logger.ts';\n\nexport type ActiveUsers = {\n active_users_last_day: number;\n users_1da: number;\n users_7da: number;\n users_30da: number;\n users_1da_legacy: number;\n users_7da_legacy: number;\n users_30da_legacy: number;\n};\n\nconst hostNameRe = /^[a-f0-9]{12}$/;\n\nclass AnonymousTelemetryManager {\n static #instance: AnonymousTelemetryManager;\n #starting = false;\n #stopped = false;\n #meter!: Meter;\n #meterProvider!: MeterProvider;\n #totalCrudMutations = 0;\n #totalCustomMutations = 0;\n #totalCrudQueries = 0;\n #totalCustomQueries = 0;\n #totalRowsSynced = 0;\n #totalConnectionsSuccess = 0;\n #totalConnectionsAttempted = 0;\n #activeClientGroupsGetter: (() => number) | undefined;\n #activeUsersGetter: (() => ActiveUsers) | undefined;\n #lc: LogContext | undefined;\n #config: ZeroConfig | undefined;\n #processId: string;\n #cachedAttributes: Record<string, string> | undefined;\n #viewSyncerCount = 1;\n\n private constructor() {\n this.#processId = randomUUID();\n }\n\n static getInstance(): AnonymousTelemetryManager {\n if (!AnonymousTelemetryManager.#instance) {\n AnonymousTelemetryManager.#instance = new AnonymousTelemetryManager();\n }\n return AnonymousTelemetryManager.#instance;\n }\n\n start(lc?: LogContext, config?: ZeroConfig) {\n this.#lc = lc;\n\n // Set up OpenTelemetry diagnostic logger if not already configured\n setupOtelDiagnosticLogger(lc);\n\n if (!config) {\n try {\n config = getZeroConfig();\n } catch (e) {\n this.#lc?.info?.('telemetry: disabled - unable to parse config', e);\n return;\n }\n }\n\n if (process.env.DO_NOT_TRACK) {\n this.#lc?.info?.(\n 'telemetry: disabled - DO_NOT_TRACK environment variable is set',\n );\n return;\n }\n\n if (!config.enableTelemetry) {\n this.#lc?.info?.('telemetry: disabled - enableTelemetry is false');\n return;\n }\n\n if (this.#starting) {\n return;\n }\n\n this.#starting = true;\n this.#config = config;\n this.#viewSyncerCount = config.numSyncWorkers ?? 1;\n this.#cachedAttributes = undefined;\n\n this.#lc?.info?.(`telemetry: starting in 1 minute`);\n\n // Delay telemetry startup by 1 minute to avoid potential boot loop issues\n setTimeout(() => this.#run(), 60000);\n }\n\n #run() {\n if (this.#stopped) {\n return;\n }\n\n const resource = resourceFromAttributes(this.#getAttributes());\n\n // Add a random jitter to the export interval to avoid all view-syncers exporting at the same time\n const exportIntervalMillis =\n 60000 * this.#viewSyncerCount + Math.floor(Math.random() * 10000);\n const readers = [\n new PeriodicExportingMetricReader({\n exportIntervalMillis,\n exporter: new OTLPMetricExporter({\n url: 'https://metrics.rocicorp.dev',\n timeoutMillis: 30000,\n }),\n }),\n ];\n\n // Uncomment this to debug metrics exports.\n\n // readers.push(\n // new PeriodicExportingMetricReader({\n // exportIntervalMillis,\n // exporter: new ConsoleMetricExporter(),\n // }),\n // );\n\n this.#meterProvider = new MeterProvider({resource, readers});\n this.#meter = this.#meterProvider.getMeter('zero-anonymous-telemetry');\n\n this.#setupMetrics();\n this.#lc?.info?.(\n `telemetry: started (exports every ${exportIntervalMillis / 1000} seconds for ${this.#viewSyncerCount} view-syncers)`,\n );\n }\n\n #setupMetrics() {\n // Observable gauges\n const uptimeGauge = this.#meter.createObservableGauge('zero.uptime', {\n description: 'System uptime in seconds',\n unit: 'seconds',\n });\n\n // Observable counters\n const uptimeCounter = this.#meter.createObservableCounter(\n 'zero.uptime_counter',\n {\n description: 'System uptime in seconds',\n unit: 'seconds',\n },\n );\n const crudMutationsCounter = this.#meter.createObservableCounter(\n 'zero.crud_mutations_processed',\n {\n description: 'Total number of CRUD mutations processed',\n },\n );\n const customMutationsCounter = this.#meter.createObservableCounter(\n 'zero.custom_mutations_processed',\n {\n description: 'Total number of custom mutations processed',\n },\n );\n const totalMutationsCounter = this.#meter.createObservableCounter(\n 'zero.mutations_processed',\n {\n description: 'Total number of mutations processed',\n },\n );\n const crudQueriesCounter = this.#meter.createObservableCounter(\n 'zero.crud_queries_processed',\n {\n description: 'Total number of CRUD queries processed',\n },\n );\n const customQueriesCounter = this.#meter.createObservableCounter(\n 'zero.custom_queries_processed',\n {\n description: 'Total number of custom queries processed',\n },\n );\n const totalQueriesCounter = this.#meter.createObservableCounter(\n 'zero.queries_processed',\n {\n description: 'Total number of queries processed',\n },\n );\n const rowsSyncedCounter = this.#meter.createObservableCounter(\n 'zero.rows_synced',\n {\n description: 'Total number of rows synced',\n },\n );\n\n // Observable counters for connections\n const connectionsSuccessCounter = this.#meter.createObservableCounter(\n 'zero.connections_success',\n {\n description: 'Total number of successful connections',\n },\n );\n\n const connectionsAttemptedCounter = this.#meter.createObservableCounter(\n 'zero.connections_attempted',\n {\n description: 'Total number of attempted connections',\n },\n );\n\n const activeClientGroupsGauge = this.#meter.createObservableGauge(\n 'zero.gauge_active_client_groups',\n {\n description: 'Number of currently active client groups',\n },\n );\n\n const attrs = this.#getAttributes();\n const active =\n (metric: keyof ActiveUsers) => (result: ObservableResult) => {\n const actives = this.#activeUsersGetter?.();\n if (actives) {\n const value = actives[metric];\n result.observe(value, attrs);\n }\n };\n this.#meter\n .createObservableGauge('zero.active_users_last_day', {\n description: 'Count of CVR instances active in the last 24h',\n })\n .addCallback(active('active_users_last_day'));\n this.#meter\n .createObservableGauge('zero.users_1da', {\n description: 'Count of 1-day active profiles',\n })\n .addCallback(active('users_1da'));\n this.#meter\n .createObservableGauge('zero.users_7da', {\n description: 'Count of 7-day active profiles',\n })\n .addCallback(active('users_7da'));\n this.#meter\n .createObservableGauge('zero.users_30da', {\n description: 'Count of 30-day active profiles',\n })\n .addCallback(active('users_30da'));\n this.#meter\n .createObservableGauge('zero.users_1da_legacy', {\n description: 'Count of 1-day active profiles with CVR fallback',\n })\n .addCallback(active('users_1da_legacy'));\n this.#meter\n .createObservableGauge('zero.users_7da_legacy', {\n description: 'Count of 7-day active profiles with CVR fallback',\n })\n .addCallback(active('users_7da_legacy'));\n this.#meter\n .createObservableGauge('zero.users_30da_legacy', {\n description: 'Count of 30-day active profiles with CVR fallback',\n })\n .addCallback(active('users_30da_legacy'));\n\n // Callbacks\n uptimeGauge.addCallback((result: ObservableResult) => {\n const uptimeSeconds = Math.floor(process.uptime());\n result.observe(uptimeSeconds, attrs);\n });\n uptimeCounter.addCallback((result: ObservableResult) => {\n const uptimeSeconds = Math.floor(process.uptime());\n result.observe(uptimeSeconds, attrs);\n });\n crudMutationsCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCrudMutations, attrs);\n });\n customMutationsCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCustomMutations, attrs);\n });\n totalMutationsCounter.addCallback((result: ObservableResult) => {\n const totalMutations =\n this.#totalCrudMutations + this.#totalCustomMutations;\n result.observe(totalMutations, attrs);\n });\n crudQueriesCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCrudQueries, attrs);\n });\n customQueriesCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalCustomQueries, attrs);\n });\n totalQueriesCounter.addCallback((result: ObservableResult) => {\n const totalQueries = this.#totalCrudQueries + this.#totalCustomQueries;\n result.observe(totalQueries, attrs);\n });\n rowsSyncedCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalRowsSynced, attrs);\n });\n connectionsSuccessCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalConnectionsSuccess, attrs);\n });\n connectionsAttemptedCounter.addCallback((result: ObservableResult) => {\n result.observe(this.#totalConnectionsAttempted, attrs);\n });\n activeClientGroupsGauge.addCallback((result: ObservableResult) => {\n const activeClientGroups = this.#activeClientGroupsGetter?.() ?? 0;\n result.observe(activeClientGroups, attrs);\n });\n }\n\n recordMutation(type: 'crud' | 'custom', count = 1) {\n if (type === 'crud') {\n this.#totalCrudMutations += count;\n } else {\n this.#totalCustomMutations += count;\n }\n }\n\n recordQuery(type: 'crud' | 'custom', count = 1) {\n if (type === 'crud') {\n this.#totalCrudQueries += count;\n } else {\n this.#totalCustomQueries += count;\n }\n }\n\n recordRowsSynced(count: number) {\n this.#totalRowsSynced += count;\n }\n\n recordConnectionSuccess() {\n this.#totalConnectionsSuccess++;\n }\n\n recordConnectionAttempted() {\n this.#totalConnectionsAttempted++;\n }\n\n setActiveClientGroupsGetter(getter: () => number) {\n this.#activeClientGroupsGetter = getter;\n }\n\n setActiveUsersGetter(getter: () => ActiveUsers) {\n this.#activeUsersGetter = getter;\n }\n\n shutdown() {\n this.#stopped = true;\n if (this.#meterProvider) {\n this.#lc?.info?.('telemetry: shutting down');\n void this.#meterProvider.shutdown();\n }\n }\n\n #getAttributes() {\n if (!this.#cachedAttributes) {\n this.#cachedAttributes = {\n 'zero.app.id': h64(this.#config?.upstream.db || 'unknown').toString(),\n 'zero.machine.os': platform(),\n 'zero.telemetry.type': 'anonymous',\n 'zero.infra.platform': this.#getPlatform(),\n 'zero.version': getServerVersion(this.#config),\n 'zero.task.id': this.#config?.taskID || 'unknown',\n 'zero.project.id': this.#getGitProjectId(),\n 'zero.process.id': this.#processId,\n 'zero.fs.id': this.#getOrSetFsID(),\n };\n this.#lc?.debug?.(\n `telemetry: cached attributes=${JSON.stringify(this.#cachedAttributes)}`,\n );\n }\n return this.#cachedAttributes;\n }\n\n #getPlatform(): string {\n if (process.env.ZERO_ON_CLOUD_ZERO) return 'cloudzero';\n if (process.env.FLY_APP_NAME || process.env.FLY_REGION) return 'fly.io';\n if (\n process.env.ECS_CONTAINER_METADATA_URI_V4 ||\n process.env.ECS_CONTAINER_METADATA_URI ||\n process.env.AWS_EXECUTION_ENV\n ) {\n return 'aws';\n }\n if (process.env.RAILWAY_ENV || process.env.RAILWAY_STATIC_URL) {\n return 'railway';\n }\n if (process.env.RENDER || process.env.RENDER_SERVICE_ID) return 'render';\n if (\n process.env.GCP_PROJECT ||\n process.env.GCLOUD_PROJECT ||\n process.env.GOOGLE_CLOUD_PROJECT\n ) {\n return 'gcp';\n }\n if (process.env.COOLIFY_URL || process.env.COOLIFY_CONTAINER_NAME) {\n return 'coolify';\n }\n if (process.env.CONTAINER_APP_REVISION) return 'azure';\n if (process.env.FLIGHTCONTROL || process.env.FC_URL) return 'flightcontrol';\n return 'unknown';\n }\n\n #findUp(startDir: string, target: string): string | null {\n let dir = startDir;\n while (dir !== dirname(dir)) {\n if (existsSync(join(dir, target))) return dir;\n dir = dirname(dir);\n }\n return null;\n }\n\n #getGitProjectId(): string {\n try {\n const cwd = process.cwd();\n const gitRoot = this.#findUp(cwd, '.git');\n if (!gitRoot) {\n return 'unknown';\n }\n\n const rootCommitHash = execSync('git rev-list --max-parents=0 HEAD -1', {\n cwd: gitRoot,\n encoding: 'utf8',\n timeout: 1000,\n stdio: ['ignore', 'pipe', 'ignore'], // Suppress stderr\n }).trim();\n\n return rootCommitHash.length === 40 ? rootCommitHash : 'unknown';\n } catch (error) {\n // execSync throws a child_process.SpawnSyncReturns-shaped error with an\n // output property (an array with stdio buffers) and sometimes a killed\n // property that references back to the error itself — making it\n // circular and causing stringify() in the Logger logic to throw.\n const details =\n error instanceof Error\n ? {message: error.message, name: error.name, stack: error.stack}\n : String(error);\n this.#lc?.debug?.('telemetry: unable to get Git root commit:', details);\n return 'unknown';\n }\n }\n\n #getOrSetFsID(): string {\n try {\n if (this.#isInContainer()) {\n return 'container';\n }\n const fsidPath = join(homedir(), '.rocicorp', 'fsid');\n const fsidDir = dirname(fsidPath);\n\n mkdirSync(fsidDir, {recursive: true});\n\n // Always try atomic file creation first - this eliminates any race conditions\n const newId = randomUUID();\n try {\n writeFileSync(fsidPath, newId, {encoding: 'utf8', flag: 'wx'});\n return newId;\n } catch (writeError) {\n if ((writeError as NodeJS.ErrnoException).code === 'EEXIST') {\n const existingId = readFileSync(fsidPath, 'utf8').trim();\n return existingId;\n }\n throw writeError;\n }\n } catch (error) {\n this.#lc?.debug?.(\n 'telemetry: unable to get or set filesystem ID:',\n error,\n );\n return 'unknown';\n }\n }\n\n #isInContainer(): boolean {\n try {\n if (process.env.ZERO_IN_CONTAINER) {\n return true;\n }\n\n if (existsSync('/.dockerenv')) {\n return true;\n }\n\n if (existsSync('/usr/local/bin/docker-entrypoint.sh')) {\n return true;\n }\n\n if (process.env.KUBERNETES_SERVICE_HOST) {\n return true;\n }\n\n if (\n process.env.DOCKER_CONTAINER_ID ||\n process.env.HOSTNAME?.match(hostNameRe)\n ) {\n return true;\n }\n\n if (existsSync('/proc/1/cgroup')) {\n const cgroup = readFileSync('/proc/1/cgroup', 'utf8');\n if (\n cgroup.includes('docker') ||\n cgroup.includes('kubepods') ||\n cgroup.includes('containerd')\n ) {\n return true;\n }\n }\n\n return false;\n } catch (error) {\n this.#lc?.debug?.(\n 'telemetry: unable to detect container environment:',\n error,\n );\n return false;\n }\n }\n}\n\nconst manager = () => AnonymousTelemetryManager.getInstance();\n\nexport const startAnonymousTelemetry = (lc?: LogContext, config?: ZeroConfig) =>\n manager().start(lc, config);\nexport const recordMutation = (type: 'crud' | 'custom', count = 1) =>\n manager().recordMutation(type, count);\nexport const recordQuery = (type: 'crud' | 'custom', count = 1) =>\n manager().recordQuery(type, count);\nexport const recordRowsSynced = (count: number) =>\n manager().recordRowsSynced(count);\nexport const recordConnectionSuccess = () =>\n manager().recordConnectionSuccess();\nexport const recordConnectionAttempted = () =>\n manager().recordConnectionAttempted();\nexport const setActiveClientGroupsGetter = (getter: () => number) =>\n manager().setActiveClientGroupsGetter(getter);\nexport const setActiveUsersGetter = (getter: () => ActiveUsers) =>\n manager().setActiveUsersGetter(getter);\nexport const shutdownAnonymousTelemetry = () => manager().shutdown();\n"],"mappings":"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"}
@@ -28,7 +28,7 @@ async function runWorker(parent, env, ...argv) {
28
28
  });
29
29
  await runUntilKilled(lc, parent ?? process, new VfsBackupWatermarkWorkerService(lc, parent, createReader, litestream.vfsProbeIntervalMs));
30
30
  }
31
- if (shouldStartWorker(parentWorker, singleProcessMode())) exitAfter(lc, () => runWorker(parentWorker, process.env, ...process.argv.slice(2)));
31
+ if (shouldStartWorker(parentWorker, singleProcessMode())) exitAfter(() => lc, () => runWorker(parentWorker, process.env, ...process.argv.slice(2)));
32
32
  //#endregion
33
33
  export { runWorker as default };
34
34
 
@@ -1 +1 @@
1
- {"version":3,"file":"backup-watermark-reader.js","names":[],"sources":["../../../../../zero-cache/src/server/backup-watermark-reader.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {must} from '../../../shared/src/must.ts';\nimport {getZeroConfig} from '../config/zero-config.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n buildLitestreamVfsReplicaURL,\n VfsBackupWatermarkReader,\n} from '../services/litestream/vfs-watermark-reader.ts';\nimport {VfsBackupWatermarkWorkerService} from '../services/litestream/vfs-watermark-worker.ts';\nimport {\n parentWorker,\n shouldStartWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {createLogContext} from './logging.ts';\n\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getZeroConfig({env, argv});\n lc = createLogContext(config, 'backup-watermark-reader');\n const {litestream} = config;\n\n const replicaURL = buildLitestreamVfsReplicaURL({\n backupURL: must(\n litestream.backupURL,\n 'Missing --litestream-backup-url for backup watermark reader',\n ),\n endpoint: litestream.endpoint,\n region: litestream.region,\n });\n\n const createReader = () =>\n new VfsBackupWatermarkReader(lc, {\n replicaURL,\n extensionPath: litestream.vfsExtensionPath,\n logLevel: litestream.logLevel,\n logFile: litestream.vfsLogFile,\n });\n\n await runUntilKilled(\n lc,\n parent ?? process,\n new VfsBackupWatermarkWorkerService(\n lc,\n parent,\n createReader,\n litestream.vfsProbeIntervalMs,\n ),\n );\n}\n\n// Unlike the other workers, the backup watermark reader is *always* run as its\n// own OS process — forked on demand by the `VfsBackupMonitor` (via\n// `forkChildWorker`), or launched directly as a standalone debug tool — so that\n// the native Litestream VFS extension and its process-global `LITESTREAM_*` env\n// stay isolated from the rest of zero-cache. See `shouldStartWorker`.\nif (shouldStartWorker(parentWorker, singleProcessMode())) {\n void exitAfter(lc, () =>\n runWorker(parentWorker, process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;AAiBA,IAAI,KAAK,IAAI,WAAW,QAAQ,CAAC,GAAG,cAAc;AAElD,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;CACf,MAAM,SAAS,cAAc;EAAC;EAAK;CAAI,CAAC;CACxC,KAAK,iBAAiB,QAAQ,yBAAyB;CACvD,MAAM,EAAC,eAAc;CAErB,MAAM,aAAa,6BAA6B;EAC9C,WAAW,KACT,WAAW,WACX,6DACF;EACA,UAAU,WAAW;EACrB,QAAQ,WAAW;CACrB,CAAC;CAED,MAAM,qBACJ,IAAI,yBAAyB,IAAI;EAC/B;EACA,eAAe,WAAW;EAC1B,UAAU,WAAW;EACrB,SAAS,WAAW;CACtB,CAAC;CAEH,MAAM,eACJ,IACA,UAAU,SACV,IAAI,gCACF,IACA,QACA,cACA,WAAW,kBACb,CACF;AACF;AAOA,IAAI,kBAAkB,cAAc,kBAAkB,CAAC,GACrD,UAAe,UACb,UAAU,cAAc,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAC/D"}
1
+ {"version":3,"file":"backup-watermark-reader.js","names":[],"sources":["../../../../../zero-cache/src/server/backup-watermark-reader.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {must} from '../../../shared/src/must.ts';\nimport {getZeroConfig} from '../config/zero-config.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n buildLitestreamVfsReplicaURL,\n VfsBackupWatermarkReader,\n} from '../services/litestream/vfs-watermark-reader.ts';\nimport {VfsBackupWatermarkWorkerService} from '../services/litestream/vfs-watermark-worker.ts';\nimport {\n parentWorker,\n shouldStartWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {createLogContext} from './logging.ts';\n\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const config = getZeroConfig({env, argv});\n lc = createLogContext(config, 'backup-watermark-reader');\n const {litestream} = config;\n\n const replicaURL = buildLitestreamVfsReplicaURL({\n backupURL: must(\n litestream.backupURL,\n 'Missing --litestream-backup-url for backup watermark reader',\n ),\n endpoint: litestream.endpoint,\n region: litestream.region,\n });\n\n const createReader = () =>\n new VfsBackupWatermarkReader(lc, {\n replicaURL,\n extensionPath: litestream.vfsExtensionPath,\n logLevel: litestream.logLevel,\n logFile: litestream.vfsLogFile,\n });\n\n await runUntilKilled(\n lc,\n parent ?? process,\n new VfsBackupWatermarkWorkerService(\n lc,\n parent,\n createReader,\n litestream.vfsProbeIntervalMs,\n ),\n );\n}\n\n// Unlike the other workers, the backup watermark reader is *always* run as its\n// own OS process — forked on demand by the `VfsBackupMonitor` (via\n// `forkChildWorker`), or launched directly as a standalone debug tool — so that\n// the native Litestream VFS extension and its process-global `LITESTREAM_*` env\n// stay isolated from the rest of zero-cache. See `shouldStartWorker`.\nif (shouldStartWorker(parentWorker, singleProcessMode())) {\n void exitAfter(\n () => lc,\n () => runWorker(parentWorker, process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;AAiBA,IAAI,KAAK,IAAI,WAAW,QAAQ,CAAC,GAAG,cAAc;AAElD,eAA8B,UAC5B,QACA,KACA,GAAG,MACY;CACf,MAAM,SAAS,cAAc;EAAC;EAAK;CAAI,CAAC;CACxC,KAAK,iBAAiB,QAAQ,yBAAyB;CACvD,MAAM,EAAC,eAAc;CAErB,MAAM,aAAa,6BAA6B;EAC9C,WAAW,KACT,WAAW,WACX,6DACF;EACA,UAAU,WAAW;EACrB,QAAQ,WAAW;CACrB,CAAC;CAED,MAAM,qBACJ,IAAI,yBAAyB,IAAI;EAC/B;EACA,eAAe,WAAW;EAC1B,UAAU,WAAW;EACrB,SAAS,WAAW;CACtB,CAAC;CAEH,MAAM,eACJ,IACA,UAAU,SACV,IAAI,gCACF,IACA,QACA,cACA,WAAW,kBACb,CACF;AACF;AAOA,IAAI,kBAAkB,cAAc,kBAAkB,CAAC,GACrD,gBACQ,UACA,UAAU,cAAc,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CACrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA8BA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAQ/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAkMf"}
1
+ {"version":3,"file":"change-streamer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"names":[],"mappings":"AA+BA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAQ/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA2Mf"}
@@ -5,6 +5,7 @@ import { getShardConfig } from "../types/shards.js";
5
5
  import { getNormalizedZeroConfig } from "../config/zero-config.js";
6
6
  import { DatabaseInitError } from "../../../zqlite/src/db.js";
7
7
  import { connectPgClient } from "../types/pg.js";
8
+ import { registerSQLiteCorruptionDiagnosticTarget } from "../db/sqlite-corruption.js";
8
9
  import { exitAfter, runUntilKilled } from "../services/life-cycle.js";
9
10
  import { createLogContext } from "./logging.js";
10
11
  import { getServerContext } from "../config/server-context.js";
@@ -36,13 +37,17 @@ async function runWorker(parent, env, ...argv) {
36
37
  const { taskID, changeStreamer: { port, address, protocol, startupDelayMs, backPressureLimitHeapProportion, flowControlConsensusPaddingSeconds }, upstream, change, replica, initialSync, keepaliveTimeoutMs } = config;
37
38
  startOtelAuto(createLogContext(config, "change-streamer", 0, false), "change-streamer", 0);
38
39
  lc = createLogContext(config, "change-streamer");
40
+ registerSQLiteCorruptionDiagnosticTarget({
41
+ debugName: "change-streamer replica",
42
+ dbPath: replica.file
43
+ });
39
44
  initEventSink(lc, config);
40
45
  const changeDB = await connectPgClient(lc, change.db, "change-streamer", { max: change.maxConns }, { sendStringAsJson: true });
41
46
  warmupConnections(lc, changeDB, "change").catch(() => {});
42
47
  const { autoReset, replicationLag } = config;
43
48
  const shard = getShardConfig(config);
44
49
  await initChangeStreamerSchema(lc, changeDB, shard);
45
- let purgeLock = await new PurgeLocker(lc, shard, changeDB).acquire();
50
+ let purgeLock = config.litestream.backupURL && config.litestream.executable ? await new PurgeLocker(lc, shard, changeDB).acquire() : null;
46
51
  if (purgeLock) try {
47
52
  await restoreReplica(lc, config, purgeLock);
48
53
  } catch (e) {
@@ -96,7 +101,7 @@ async function runWorker(parent, env, ...argv) {
96
101
  parent.send(["ready", { ready: true }]);
97
102
  return runUntilKilled(lc, parent, changeStreamerWebServer, monitor);
98
103
  }
99
- if (!singleProcessMode()) exitAfter(lc, () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)).catch(async (e) => {
104
+ if (!singleProcessMode()) exitAfter(() => lc, () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)).catch(async (e) => {
100
105
  await publishCriticalEvent(lc, replicationStatusError(lc, "Initializing", e));
101
106
  throw e;
102
107
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {ReplicaMonitor} from '../services/change-streamer/replica-monitor.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {PurgeLocker} from '../services/change-streamer/storer.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n BackupNotFoundException,\n restoreReplica,\n} from '../services/litestream/commands.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const workerStartTime = Date.now();\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n },\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer',\n {\n max: change.maxConns,\n },\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const {autoReset, replicationLag} = config;\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date, then acquire\n // a lock to prevent change-lock purges. This ensures that (this)\n // change-streamer will be able to resume from the backup.\n await initChangeStreamerSchema(lc, changeDB, shard);\n let purgeLock = await new PurgeLocker(lc, shard, changeDB).acquire();\n\n // Restore from litestream if the change-log has entries.\n if (purgeLock) {\n try {\n await restoreReplica(lc, config, purgeLock);\n } catch (e) {\n // If the restore failed, e.g. due to a corrupt or missing backup, the\n // replication-manager recovers by re-syncing.\n const log = e instanceof BackupNotFoundException ? 'warn' : 'error';\n lc[log]?.(\n `error restoring backup. resyncing the replica: ${String(e)}`,\n e,\n );\n\n // The purgeLock must be released if the backup could not be restored,\n // or it will otherwise prevent the change-db update after the resync\n // completes.\n await purgeLock.release();\n purgeLock = null;\n }\n }\n\n let changeStreamer: ChangeStreamerService | undefined;\n\n const context = getServerContext(config);\n\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {changeSource, subscriptionState} =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n {\n ...initialSync,\n replicationSlotFailover: upstream.pgReplicationSlotFailover,\n },\n context,\n replicationLag.reportIntervalMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n purgeLock,\n autoReset ?? false,\n {\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n },\n setTimeout,\n );\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n // Release the purge lock before retrying. This is safe because the\n // purge lock exists to preserve change-log entries so the new\n // change-streamer can resume from the backup replica's watermark.\n // An AutoResetSignal means we cant resume from the backup replica\n // (e.g. its replication slot is gone), so the change-log entries the lock\n // was protecting are no longer needed. The retry performs a fresh\n // initial sync with a new replication slot, independent of the old\n // change-log. Releasing is also necessary to avoid a\n // self-deadlock when CHANGE_DB == UPSTREAM_DB:\n // CREATE_REPLICATION_SLOT waits for all older transactions to\n // finish, including this lock's open transaction.\n await purgeLock?.release();\n purgeLock = null;\n continue; // execute again with a fresh initial-sync\n }\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n // Perform any upgrades to the replica in case it was restored from an\n // earlier version. Note that this upgrade is done by the replicator worker\n // as well (in both the replication-manager and the view-syncer), but the\n // change-streamer independently reads the replica, and it is fine run the\n // upgrade logic redundantly since it is idempotent.\n await upgradeReplica(lc, 'change-streamer-init', replica.file);\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n config,\n replicaFile: replica.file,\n changeStreamer,\n // The time between when the zero-cache was started to when the\n // change-streamer is ready to start serves as the initial delay for\n // watermark cleanup (as it either includes a similar replica\n // restoration/preparation step, or an initial-sync, which\n // generally takes longer).\n //\n // Consider: Also account for permanent volumes?\n initialCleanupDelayMs: Date.now() - workerStartTime,\n env,\n });\n const monitor =\n backupMonitor ?? new ReplicaMonitor(lc, replica.file, changeStreamer);\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {port, keepaliveTimeoutMs, startupDelayMs},\n parent,\n changeStreamer,\n backupMonitor,\n );\n\n parent.send(['ready', {ready: true}]);\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer.\n return runUntilKilled(lc, parent, changeStreamerWebServer, monitor);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(lc, () =>\n runWorker(must(parentWorker), process.env, ...process.argv.slice(2)).catch(\n async e => {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n throw e;\n },\n ),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../zero-cache/src/server/change-streamer.ts"],"sourcesContent":["import {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {DatabaseInitError} from '../../../zqlite/src/db.ts';\nimport {getServerContext} from '../config/server-context.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {warmupConnections} from '../db/warmup.ts';\nimport {initEventSink, publishCriticalEvent} from '../observability/events.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {initializeCustomChangeSource} from '../services/change-source/custom/change-source.ts';\nimport {initializePostgresChangeSource} from '../services/change-source/pg/change-source.ts';\nimport {createBackupCleanupMonitor} from '../services/change-streamer/backup-cleanup-monitor-factory.ts';\nimport {ChangeStreamerHttpServer} from '../services/change-streamer/change-streamer-http.ts';\nimport {initializeStreamer} from '../services/change-streamer/change-streamer-service.ts';\nimport type {ChangeStreamerService} from '../services/change-streamer/change-streamer.ts';\nimport {ReplicaMonitor} from '../services/change-streamer/replica-monitor.ts';\nimport {initChangeStreamerSchema} from '../services/change-streamer/schema/init.ts';\nimport {AutoResetSignal} from '../services/change-streamer/schema/tables.ts';\nimport {PurgeLocker} from '../services/change-streamer/storer.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n BackupNotFoundException,\n restoreReplica,\n} from '../services/litestream/commands.ts';\nimport {\n replicationStatusError,\n ReplicationStatusPublisher,\n} from '../services/replicator/replication-status.ts';\nimport {connectPgClient} from '../types/pg.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...argv: string[]\n): Promise<void> {\n const workerStartTime = Date.now();\n const config = getNormalizedZeroConfig({env, argv});\n const {\n taskID,\n changeStreamer: {\n port,\n address,\n protocol,\n startupDelayMs,\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n },\n upstream,\n change,\n replica,\n initialSync,\n keepaliveTimeoutMs,\n } = config;\n\n startOtelAuto(\n createLogContext(config, 'change-streamer', 0, false),\n 'change-streamer',\n 0,\n );\n lc = createLogContext(config, 'change-streamer');\n registerSQLiteCorruptionDiagnosticTarget({\n debugName: 'change-streamer replica',\n dbPath: replica.file,\n });\n initEventSink(lc, config);\n\n // Kick off DB connection warmup in the background.\n const changeDB = await connectPgClient(\n lc,\n change.db,\n 'change-streamer',\n {\n max: change.maxConns,\n },\n {sendStringAsJson: true},\n );\n void warmupConnections(lc, changeDB, 'change').catch(() => {});\n\n const {autoReset, replicationLag} = config;\n const shard = getShardConfig(config);\n\n // Ensure the change DB schema is initialized/up-to-date.\n await initChangeStreamerSchema(lc, changeDB, shard);\n\n // When restoring from litestream, acquire a lock to prevent change-log\n // purges. This ensures that (this) change-streamer will be able to resume\n // from the backup.\n let purgeLock =\n config.litestream.backupURL && config.litestream.executable\n ? await new PurgeLocker(lc, shard, changeDB).acquire()\n : null;\n\n // Restore from litestream if the change-log has entries.\n if (purgeLock) {\n try {\n await restoreReplica(lc, config, purgeLock);\n } catch (e) {\n // If the restore failed, e.g. due to a corrupt or missing backup, the\n // replication-manager recovers by re-syncing.\n const log = e instanceof BackupNotFoundException ? 'warn' : 'error';\n lc[log]?.(\n `error restoring backup. resyncing the replica: ${String(e)}`,\n e,\n );\n\n // The purgeLock must be released if the backup could not be restored,\n // or it will otherwise prevent the change-db update after the resync\n // completes.\n await purgeLock.release();\n purgeLock = null;\n }\n }\n\n let changeStreamer: ChangeStreamerService | undefined;\n\n const context = getServerContext(config);\n\n for (const first of [true, false]) {\n try {\n // Note: This performs initial sync of the replica if necessary.\n const {changeSource, subscriptionState} =\n upstream.type === 'pg'\n ? await initializePostgresChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n {\n ...initialSync,\n replicationSlotFailover: upstream.pgReplicationSlotFailover,\n },\n context,\n replicationLag.reportIntervalMs,\n )\n : await initializeCustomChangeSource(\n lc,\n upstream.db,\n shard,\n replica.file,\n context,\n );\n\n const replicationStatusPublisher =\n ReplicationStatusPublisher.forReplicaFile(replica.file);\n\n changeStreamer = await initializeStreamer(\n lc,\n shard,\n taskID,\n address,\n protocol,\n changeDB,\n changeSource,\n replicationStatusPublisher,\n subscriptionState,\n purgeLock,\n autoReset ?? false,\n {\n backPressureLimitHeapProportion,\n flowControlConsensusPaddingSeconds,\n statementTimeoutMs: change.statementTimeoutMs,\n changeLogBatchSize: change.logBatchSize,\n },\n setTimeout,\n );\n break;\n } catch (e) {\n if (first && e instanceof AutoResetSignal) {\n lc.warn?.(`resetting replica ${replica.file}`, e);\n // TODO: Make deleteLiteDB work with litestream. It will probably have to be\n // a semantic wipe instead of a file delete.\n deleteLiteDB(replica.file);\n // Release the purge lock before retrying. This is safe because the\n // purge lock exists to preserve change-log entries so the new\n // change-streamer can resume from the backup replica's watermark.\n // An AutoResetSignal means we cant resume from the backup replica\n // (e.g. its replication slot is gone), so the change-log entries the lock\n // was protecting are no longer needed. The retry performs a fresh\n // initial sync with a new replication slot, independent of the old\n // change-log. Releasing is also necessary to avoid a\n // self-deadlock when CHANGE_DB == UPSTREAM_DB:\n // CREATE_REPLICATION_SLOT waits for all older transactions to\n // finish, including this lock's open transaction.\n await purgeLock?.release();\n purgeLock = null;\n continue; // execute again with a fresh initial-sync\n }\n if (e instanceof DatabaseInitError) {\n throw new Error(\n `Cannot open ZERO_REPLICA_FILE at \"${replica.file}\". Please check that the path is valid.`,\n {cause: e},\n );\n }\n throw e;\n }\n }\n // impossible: upstream must have advanced in order for replication to be stuck.\n assert(changeStreamer, `resetting replica did not advance replicaVersion`);\n\n // Perform any upgrades to the replica in case it was restored from an\n // earlier version. Note that this upgrade is done by the replicator worker\n // as well (in both the replication-manager and the view-syncer), but the\n // change-streamer independently reads the replica, and it is fine run the\n // upgrade logic redundantly since it is idempotent.\n await upgradeReplica(lc, 'change-streamer-init', replica.file);\n\n const backupMonitor = createBackupCleanupMonitor({\n lc,\n config,\n replicaFile: replica.file,\n changeStreamer,\n // The time between when the zero-cache was started to when the\n // change-streamer is ready to start serves as the initial delay for\n // watermark cleanup (as it either includes a similar replica\n // restoration/preparation step, or an initial-sync, which\n // generally takes longer).\n //\n // Consider: Also account for permanent volumes?\n initialCleanupDelayMs: Date.now() - workerStartTime,\n env,\n });\n const monitor =\n backupMonitor ?? new ReplicaMonitor(lc, replica.file, changeStreamer);\n\n const changeStreamerWebServer = new ChangeStreamerHttpServer(\n lc,\n {port, keepaliveTimeoutMs, startupDelayMs},\n parent,\n changeStreamer,\n backupMonitor,\n );\n\n parent.send(['ready', {ready: true}]);\n\n // Note: The changeStreamer itself is not started here; it is started by the\n // changeStreamerWebServer.\n return runUntilKilled(lc, parent, changeStreamerWebServer, monitor);\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () =>\n runWorker(\n must(parentWorker),\n process.env,\n ...process.argv.slice(2),\n ).catch(async e => {\n await publishCriticalEvent(\n lc,\n replicationStatusError(lc, 'Initializing', e),\n );\n throw e;\n }),\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,14 @@
1
1
  import type { LogContext } from '@rocicorp/logger';
2
+ import { type Context, type LogLevel, type LogSink } from '@rocicorp/logger';
2
3
  import { type LogConfig } from '../../../shared/src/logging.ts';
3
4
  export declare function createLogContext({ log }: {
4
5
  log: LogConfig;
5
6
  }, worker: string, workerIndex?: number, includeOtel?: boolean): LogContext;
7
+ export declare function logUncaughtException(lc: LogContext, logSink: LogSink, err: unknown, origin: string, includeLastChanceDiagnostics?: boolean): Promise<void>;
8
+ export declare class CompositeLogSink implements LogSink {
9
+ #private;
10
+ constructor(sinks: LogSink[]);
11
+ log(level: LogLevel, context: Context | undefined, ...args: unknown[]): void;
12
+ flush(): Promise<void>;
13
+ }
6
14
  //# sourceMappingURL=logging.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,gCAAgC,CAAC;AAIxC,wBAAgB,gBAAgB,CAC9B,EAAC,GAAG,EAAC,EAAE;IAAC,GAAG,EAAE,SAAS,CAAA;CAAC,EACvB,MAAM,EAAE,MAAM,EACd,WAAW,SAAI,EACf,WAAW,UAAO,GACjB,UAAU,CASZ"}
1
+ {"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAC,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,gCAAgC,CAAC;AAYxC,wBAAgB,gBAAgB,CAC9B,EAAC,GAAG,EAAC,EAAE;IAAC,GAAG,EAAE,SAAS,CAAA;CAAC,EACvB,MAAM,EAAE,MAAM,EACd,WAAW,SAAI,EACf,WAAW,UAAO,GACjB,UAAU,CAyBZ;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,4BAA4B,UAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAYf;AAWD,qBAAa,gBAAiB,YAAW,OAAO;;gBAGlC,KAAK,EAAE,OAAO,EAAE;IAI5B,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAMtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}