@rocicorp/zero 1.7.0 → 1.8.0-canary.1

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 (139) hide show
  1. package/README.md +1 -1
  2. package/out/replicache/src/kv/op-sqlite/store.js +3 -3
  3. package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
  4. package/out/replicache/src/kv/op-sqlite/types.d.ts +16 -2
  5. package/out/replicache/src/kv/op-sqlite/types.d.ts.map +1 -1
  6. package/out/replicache/src/kv/op-sqlite/types.js +5 -1
  7. package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
  8. package/out/zero/package.js +1 -1
  9. package/out/zero/package.js.map +1 -1
  10. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  11. package/out/zero-cache/src/config/normalize.js +2 -0
  12. package/out/zero-cache/src/config/normalize.js.map +1 -1
  13. package/out/zero-cache/src/config/zero-config.d.ts +45 -0
  14. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  15. package/out/zero-cache/src/config/zero-config.js +54 -1
  16. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  17. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  18. package/out/zero-cache/src/custom/fetch.js +1 -14
  19. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  20. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  21. package/out/zero-cache/src/custom-queries/transform-query.js +5 -6
  22. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  23. package/out/zero-cache/src/db/migration-lite.d.ts +6 -0
  24. package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
  25. package/out/zero-cache/src/db/migration-lite.js +22 -2
  26. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  27. package/out/zero-cache/src/server/backup-watermark-reader.js +35 -0
  28. package/out/zero-cache/src/server/backup-watermark-reader.js.map +1 -0
  29. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  30. package/out/zero-cache/src/server/change-streamer.js +15 -7
  31. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  32. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  33. package/out/zero-cache/src/server/syncer.js +3 -2
  34. package/out/zero-cache/src/server/syncer.js.map +1 -1
  35. package/out/zero-cache/src/server/worker-urls.d.ts +1 -0
  36. package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
  37. package/out/zero-cache/src/server/worker-urls.js +2 -1
  38. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  39. package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
  40. package/out/zero-cache/src/services/change-source/common/replica-schema.js +13 -6
  41. package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
  42. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  43. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +13 -2
  44. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  45. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +18 -0
  46. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -0
  47. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +17 -0
  48. package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -0
  49. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts +4 -89
  50. package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
  51. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts +114 -0
  53. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +1 -0
  54. package/out/zero-cache/src/services/change-streamer/{backup-monitor.js → litestream3-backup-monitor.js} +122 -13
  55. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +1 -0
  56. package/out/zero-cache/src/services/change-streamer/replica-monitor.d.ts +2 -2
  57. package/out/zero-cache/src/services/change-streamer/replica-monitor.js +2 -2
  58. package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +1 -1
  59. package/out/zero-cache/src/services/change-streamer/storer.d.ts +2 -1
  60. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  61. package/out/zero-cache/src/services/change-streamer/storer.js +44 -4
  62. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  63. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts +25 -0
  64. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +1 -0
  65. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +140 -0
  66. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +1 -0
  67. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/litestream/commands.js +10 -7
  69. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  70. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts +44 -0
  71. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.d.ts.map +1 -0
  72. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +65 -0
  73. package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js.map +1 -0
  74. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts +11 -0
  75. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.d.ts.map +1 -0
  76. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js +74 -0
  77. package/out/zero-cache/src/services/litestream/vfs-watermark-worker-source.js.map +1 -0
  78. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts +42 -0
  79. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.d.ts.map +1 -0
  80. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js +117 -0
  81. package/out/zero-cache/src/services/litestream/vfs-watermark-worker.js.map +1 -0
  82. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/replicator/incremental-sync.js +9 -2
  84. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  85. package/out/zero-cache/src/services/replicator/notifier.d.ts +1 -0
  86. package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
  87. package/out/zero-cache/src/services/replicator/notifier.js +7 -1
  88. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  89. package/out/zero-cache/src/services/replicator/replicator.d.ts +11 -1
  90. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  91. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  92. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/view-syncer/client-handler.js +6 -2
  94. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +3 -1
  96. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  97. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +12 -8
  98. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  99. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +4 -2
  100. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  101. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +108 -19
  102. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  103. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +4 -0
  104. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  105. package/out/zero-cache/src/services/view-syncer/view-syncer.js +17 -4
  106. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  107. package/out/zero-cache/src/types/processes.d.ts +25 -0
  108. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  109. package/out/zero-cache/src/types/processes.js +35 -2
  110. package/out/zero-cache/src/types/processes.js.map +1 -1
  111. package/out/zero-cache/src/workers/connect-params.d.ts +2 -1
  112. package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
  113. package/out/zero-cache/src/workers/connect-params.js +15 -1
  114. package/out/zero-cache/src/workers/connect-params.js.map +1 -1
  115. package/out/zero-cache/src/workers/replicator.d.ts +2 -2
  116. package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
  117. package/out/zero-cache/src/workers/replicator.js +34 -28
  118. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  119. package/out/zero-cache/src/workers/syncer.d.ts +17 -0
  120. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  121. package/out/zero-cache/src/workers/syncer.js +116 -1
  122. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  123. package/out/zero-client/src/client/query-manager.d.ts +10 -0
  124. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  125. package/out/zero-client/src/client/query-manager.js +19 -2
  126. package/out/zero-client/src/client/query-manager.js.map +1 -1
  127. package/out/zero-client/src/client/version.js +1 -1
  128. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  129. package/out/zero-client/src/client/zero.js +5 -1
  130. package/out/zero-client/src/client/zero.js.map +1 -1
  131. package/out/zero-server/src/adapters/drizzle.d.ts +30 -7
  132. package/out/zero-server/src/adapters/drizzle.d.ts.map +1 -1
  133. package/out/zero-server/src/adapters/drizzle.js +4 -2
  134. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  135. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  136. package/out/zql/src/ivm/memory-source.js +11 -0
  137. package/out/zql/src/ivm/memory-source.js.map +1 -1
  138. package/package.json +1 -1
  139. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nconst DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n lc.info?.(\n `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`,\n );\n\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n const livenessTimer = setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n }, manualKeepaliveTimeout / 5);\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => parseStreamMessage(lc, buffer, parser),\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nasync function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(10);\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nfunction makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":";;;;;;;;;;;;;AAmBA,IAAM,6CAA6C;AAcnD,eAAsB,UACpB,IACA,IACA,MACA,cACA,KACA,iCAAiC,4CACjC,kBAAkB,mBAIjB;CACD,MAAM,UAAU,SACd,KACE;EACE,KAAK;GACJ,gBAAgB;GAChB,iBAAiB;GACjB,iBAAiB;EAClB,YAAY;IACT,qBAAqB;GACtB,aAAa;EACf;CACF,GAKA,GAAG,OACL,CACF;CAQA,MAAM,CAAC,EAAC,wBAAuB,MAAM,OAEpC;;2CAEwC,OAAO;CAChD,MAAM,yBAAyB,KAAK,MAAM,qBAAqB,GAAI;CACnE,GAAG,OACD,uBAAuB,mBAAmB,gDACK,uBAAuB,GACxE;CAEA,MAAM,CAAC,UAAU,YAAY,MAAM,uBACjC,IACA,SACA,MACA,cACA,KACA,iCAAiC,CACnC;CAEA,IAAI,cAAc,KAAK,IAAI;CAC3B,SAAS,QAAQ,KAAa;EAC5B,SAAS,MAAM,QAAQ,GAAG,CAAC;EAC3B,cAAc,KAAK,IAAI;CACzB;CAEA,MAAM,gBAAgB,kBAAkB;EAEtC,IADY,KAAK,IACb,IAAM,cAAc,wBAAwB;GAC9C,QAAQ,EAAE;GACV,GAAG,QAAQ,uBAAuB;EACpC;CACF,GAAG,yBAAyB,CAAC;CAE7B,IAAI,YAAY;CAEhB,MAAM,SAAS,IAAI,eAAe,MADR,eAAe,IAAI,EAAC,oBAAoB,KAAI,CAAC,CAC1B;CAC7C,MAAM,WAAW,aAAa,OAAsB,EAClD,eAAe;EACb,YAAY;EACZ,SAAS,aAAa,SAAS,QAAQ;EACvC,cAAc,aAAa;EAC3B,OAAO,QAAQ,IAAI;CACrB,EACF,CAAC;CAED,SAAS,KACP,eAGE,aAAa,GAAG,OAAO,gCAAgC,GAAG,QAAQ,MAAM,CAC5E;CACA,SAAS,KACP,UACA,MAGG,aAAa,SAAS,iBAAiB,EAAE,SAAS,qBACnD,GAAG,OAAO,cAAc,GAAG,QAAQ,QAAQ,CAAC,CAChD;CAEA,KAAK;EACH,QAAQ;EACR,MAAM;EACN,QAAO,WAAU,mBAAmB,IAAI,QAAQ,MAAM;EAItD,gBAAgB;CAClB,CAAC;CAED,OAAO;EACL;EACA,MAAM,EAAC,MAAM,QAAO;CACtB;AACF;;;;;;AAOA,SAAS,uBAAuB,cAAgC;CAG9D,OAAO,aAAa,KAAI,MAAK,IAAI,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,GAAG;AAC5D;AAEA,eAAe,uBACb,IACA,SACA,MACA,cACA,KACA,aACA;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAC/B,IAAI;EACF,MAAM,SAAS,QACZ,OACC,0BAA0B,GAAG,IAAI,EAAE,WAAW,WAAW,GAAG,EAAE;;6BAE3C,uBAAuB,YAAY,EAAE;;QAG1D,EACC,QAAQ;EACX,OAAO,MAAM,QAAQ,IAAI,CAAC,OAAO,SAAS,GAAG,OAAO,SAAS,CAAC,CAAC;CACjE,SAAS,GAAG;EACV,IAAI,aAAa,SAAS,eAAe;GAEvC,IAAI,EAAE,SAAS,kBAAkB;IAI/B,GAAG,OAAO,WAAW,IAAI,EAAE,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C,MAAM,MAAM,EAAE;IACd;GACF;GAIA,IAAI,EAAE,SAAS,qCAAqC;IAClD,GAAG,QAAQ,qCAAqC,CAAC;IACjD,MAAM,IAAI,gBAAgB,sCAAsC,EAC9D,OAAO,EACT,CAAC;GACH;EACF;EACA,MAAM;CACR;CAEF,MAAM,IAAI,MACR,0BAA0B,YAAY,+BACxC;AACF;AAEA,SAAS,mBACP,IACA,QACA,QACsB;CAEtB,IAAI,OAAO,OAAO,OAAQ,OAAO,OAAO,KAAM;EAC5C,GAAG,OAAO,mBAAmB,OAAO,EAAE;EACtC,OAAO;CACT;CACA,MAAM,MAAM,OAAO,gBAAgB,CAAC;CACpC,IAAI,OAAO,OAAO,KAGhB,OAAO,CAAC,KAAK,OAAO,MAAM,OAAO,SAAS,EAAE,CAAC,CAAC;CAKhD,OAAO,CAAC,KAAK;EAAC,KAAK;EAAa,eADV,OAAO,SAAS,EAAE,MAAM;CACD,CAAC;AAChD;AAGA,SAAS,QAAQ,KAAqB;CACpC,MAAM,WAAW,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,KAAM,GAAG,CAAC,CAAC,IAAI,OAAO,GAAI;CAExE,MAAM,IAAI,OAAO,MAAM,EAAE;CACzB,EAAE,KAAK,IAAI,WAAW,CAAC;CACvB,EAAE,gBAAgB,KAAK,CAAC;CACxB,EAAE,gBAAgB,KAAK,CAAC;CACxB,EAAE,gBAAgB,KAAK,EAAE;CACzB,EAAE,gBAAgB,UAAU,EAAE;CAC9B,OAAO;AACT"}
1
+ {"version":3,"file":"stream.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nconst DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n lc.info?.(\n `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`,\n );\n\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n // Postgres promises to send a keepalive at roughly wal_sender_timeout / 2.\n // If we go long enough without receiving anything (data or keepalive), the\n // inbound half of the connection is silently dead — our outbound acks keep\n // flowing into the void, neither side errors, and the change-source sits\n // there forever. Tear down the readable to force a reconnect.\n //\n // The threshold is 2x wal_sender_timeout — i.e. two consecutive keepalives\n // missed — to avoid spurious reconnects from a single delayed keepalive\n // (PG scheduling jitter under load, our own event-loop stalls, etc.).\n const inboundTimeoutMs = walSenderTimeoutMs * 2;\n let lastReceivedTime = Date.now();\n\n const livenessTimer = setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n const sinceLastReceived = now - lastReceivedTime;\n if (sinceLastReceived > inboundTimeoutMs && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> 2x wal_sender_timeout ${walSenderTimeoutMs}ms). Destroying ` +\n `replication stream to force reconnect.`,\n );\n readable.destroy(\n new Error(\n `replication stream inbound timeout after ${sinceLastReceived}ms`,\n ),\n );\n }\n }, manualKeepaliveTimeout / 5);\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => {\n lastReceivedTime = Date.now();\n return parseStreamMessage(lc, buffer, parser);\n },\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nasync function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(10);\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nfunction makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { LogContext } from '@rocicorp/logger';
2
+ import type { NormalizedZeroConfig } from '../../config/normalize.ts';
3
+ import type { BackupMonitor } from './backup-monitor.ts';
4
+ import type { ChangeStreamerService } from './change-streamer.ts';
5
+ import { type BackupStateVerifier } from './litestream3-backup-monitor.ts';
6
+ import { type VfsBackupWatermarkSource } from './vfs-backup-monitor.ts';
7
+ export type BackupCleanupMonitorFactoryOptions = {
8
+ lc: LogContext;
9
+ config: NormalizedZeroConfig;
10
+ replicaFile: string;
11
+ changeStreamer: ChangeStreamerService;
12
+ initialCleanupDelayMs: number;
13
+ verifyBackupState?: BackupStateVerifier | undefined;
14
+ vfsBackupWatermarkSource?: VfsBackupWatermarkSource | undefined;
15
+ env?: NodeJS.ProcessEnv | undefined;
16
+ };
17
+ export declare function createBackupCleanupMonitor({ lc, config, replicaFile, changeStreamer, initialCleanupDelayMs, verifyBackupState, vfsBackupWatermarkSource, env, }: BackupCleanupMonitorFactoryOptions): BackupMonitor | null;
18
+ //# sourceMappingURL=backup-cleanup-monitor-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-cleanup-monitor-factory.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AAKpE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,kCAAkC,GAAG;IAC/C,EAAE,EAAE,UAAU,CAAC;IACf,MAAM,EAAE,oBAAoB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,qBAAqB,CAAC;IACtC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACpD,wBAAwB,CAAC,EAAE,wBAAwB,GAAG,SAAS,CAAC;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,SAAS,CAAC;CACrC,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,EACzC,EAAE,EACF,MAAM,EACN,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,GAAG,GACJ,EAAE,kCAAkC,GAAG,aAAa,GAAG,IAAI,CAgC3D"}
@@ -0,0 +1,17 @@
1
+ import { forkChildWorker } from "../../types/processes.js";
2
+ import { BACKUP_WATERMARK_READER_URL } from "../../server/worker-urls.js";
3
+ import { getLastBackupTime } from "../litestream/commands.js";
4
+ import { VfsBackupWatermarkWorkerSource } from "../litestream/vfs-watermark-worker-source.js";
5
+ import { Litestream3BackupMonitor } from "./litestream3-backup-monitor.js";
6
+ import { VfsBackupMonitor } from "./vfs-backup-monitor.js";
7
+ //#region ../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts
8
+ function createBackupCleanupMonitor({ lc, config, replicaFile, changeStreamer, initialCleanupDelayMs, verifyBackupState, vfsBackupWatermarkSource, env }) {
9
+ const { backupURL, port: metricsPort } = config.litestream;
10
+ if (!backupURL) return null;
11
+ if (config.litestream.backupUsingV5) return new VfsBackupMonitor(lc, backupURL, changeStreamer, initialCleanupDelayMs, config.litestream.vfsProbeIntervalMs, vfsBackupWatermarkSource ?? new VfsBackupWatermarkWorkerSource(lc, () => forkChildWorker(BACKUP_WATERMARK_READER_URL, env ?? process.env), config.litestream.vfsProbeTimeoutMs));
12
+ return new Litestream3BackupMonitor(lc, replicaFile, backupURL, `http://localhost:${metricsPort}/metrics`, changeStreamer, initialCleanupDelayMs, verifyBackupState ?? (() => getLastBackupTime(lc, config)));
13
+ }
14
+ //#endregion
15
+ export { createBackupCleanupMonitor };
16
+
17
+ //# sourceMappingURL=backup-cleanup-monitor-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-cleanup-monitor-factory.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport {BACKUP_WATERMARK_READER_URL} from '../../server/worker-urls.ts';\nimport {forkChildWorker} from '../../types/processes.ts';\nimport {getLastBackupTime} from '../litestream/commands.ts';\nimport {VfsBackupWatermarkWorkerSource} from '../litestream/vfs-watermark-worker-source.ts';\nimport type {BackupMonitor} from './backup-monitor.ts';\nimport type {ChangeStreamerService} from './change-streamer.ts';\nimport {\n Litestream3BackupMonitor,\n type BackupStateVerifier,\n} from './litestream3-backup-monitor.ts';\nimport {\n VfsBackupMonitor,\n type VfsBackupWatermarkSource,\n} from './vfs-backup-monitor.ts';\n\nexport type BackupCleanupMonitorFactoryOptions = {\n lc: LogContext;\n config: NormalizedZeroConfig;\n replicaFile: string;\n changeStreamer: ChangeStreamerService;\n initialCleanupDelayMs: number;\n verifyBackupState?: BackupStateVerifier | undefined;\n vfsBackupWatermarkSource?: VfsBackupWatermarkSource | undefined;\n env?: NodeJS.ProcessEnv | undefined;\n};\n\nexport function createBackupCleanupMonitor({\n lc,\n config,\n replicaFile,\n changeStreamer,\n initialCleanupDelayMs,\n verifyBackupState,\n vfsBackupWatermarkSource,\n env,\n}: BackupCleanupMonitorFactoryOptions): BackupMonitor | null {\n const {backupURL, port: metricsPort} = config.litestream;\n if (!backupURL) {\n return null;\n }\n\n if (config.litestream.backupUsingV5) {\n return new VfsBackupMonitor(\n lc,\n backupURL,\n changeStreamer,\n initialCleanupDelayMs,\n config.litestream.vfsProbeIntervalMs,\n vfsBackupWatermarkSource ??\n new VfsBackupWatermarkWorkerSource(\n lc,\n () =>\n forkChildWorker(BACKUP_WATERMARK_READER_URL, env ?? process.env),\n config.litestream.vfsProbeTimeoutMs,\n ),\n );\n }\n\n return new Litestream3BackupMonitor(\n lc,\n replicaFile,\n backupURL,\n `http://localhost:${metricsPort}/metrics`,\n changeStreamer,\n initialCleanupDelayMs,\n verifyBackupState ?? (() => getLastBackupTime(lc, config)),\n );\n}\n"],"mappings":";;;;;;;AA4BA,SAAgB,2BAA2B,EACzC,IACA,QACA,aACA,gBACA,uBACA,mBACA,0BACA,OAC2D;CAC3D,MAAM,EAAC,WAAW,MAAM,gBAAe,OAAO;CAC9C,IAAI,CAAC,WACH,OAAO;CAGT,IAAI,OAAO,WAAW,eACpB,OAAO,IAAI,iBACT,IACA,WACA,gBACA,uBACA,OAAO,WAAW,oBAClB,4BACE,IAAI,+BACF,UAEE,gBAAgB,6BAA6B,OAAO,QAAQ,GAAG,GACjE,OAAO,WAAW,iBACpB,CACJ;CAGF,OAAO,IAAI,yBACT,IACA,aACA,WACA,oBAAoB,YAAY,WAChC,gBACA,uBACA,4BAA4B,kBAAkB,IAAI,MAAM,EAC1D;AACF"}
@@ -1,93 +1,8 @@
1
- import type { LogContext } from '@rocicorp/logger';
2
- import { Subscription } from '../../types/subscription.ts';
3
- import type { Service } from '../service.ts';
4
- import type { ChangeStreamerService } from './change-streamer.ts';
1
+ import type { Subscription } from '../../types/subscription.ts';
2
+ import type { SingletonService } from '../service.ts';
5
3
  import type { SnapshotMessage } from './snapshot.ts';
6
- export declare const CHECK_INTERVAL_MS = 60000;
7
- /**
8
- * Allowance for clock skew between the machine reporting litestream metrics
9
- * and the timestamps reported by the backup destination (e.g. S3).
10
- */
11
- export declare const BACKUP_VERIFICATION_SLACK_MS = 60000;
12
- /**
13
- * How long the actual backup state may remain *continuously* behind the
14
- * backup progress claimed by litestream metrics before the backup is
15
- * considered genuinely wedged and the process is shut down.
16
- *
17
- * A wedged backup is dangerous: litestream believes it is making durable
18
- * progress when it is not, which can silently corrupt the backup (the WAL
19
- * format only makes *some* such gaps detectable). Once that is confirmed to
20
- * be the persistent state, continuing to run is worse than crashing, so the
21
- * process exits loudly (non-zero, with a logged error) and the wedged backup
22
- * destination becomes the priority to investigate.
23
- *
24
- * The change-log is conservatively *not* purged for the entire time the
25
- * backup is stale, so the only cost of a generous grace period is unbounded
26
- * change-log growth. We therefore err well on the side of slack: the backup
27
- * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear
28
- * the server down, so that a transient hiccup (a slow or briefly unreachable
29
- * destination, or litestream restarting) does not trigger a shutdown. The
30
- * staleness clock is reset the moment a purge is confirmed against a real
31
- * upload.
32
- */
33
- export declare const WEDGED_SHUTDOWN_GRACE_MS: number;
34
- /**
35
- * Returns the time of the most recent object actually uploaded to the
36
- * backup replica destination (e.g. as determined by listing the snapshots
37
- * and WAL segments in S3). Rejects if the backup state cannot be determined.
38
- *
39
- * See `getLastBackupTime()` in `../litestream/commands.ts` for the
40
- * production implementation.
41
- */
42
- export type BackupStateVerifier = () => Promise<Date>;
43
- /**
44
- * The BackupMonitor polls the litestream "/metrics" endpoint to track the
45
- * watermark (label) value of the `litestream_replica_progress` gauge and
46
- * schedules cleanup of change log entries that can be purged as a result.
47
- *
48
- * See: https://github.com/rocicorp/litestream/pull/3
49
- *
50
- * Note that change log entries cannot simply be purged as soon as they
51
- * have been applied and backed up by litestream. Consider the case in which
52
- * litestream backs up new wal segments every minute, but it takes 5 minutes
53
- * to restore a replica: if a zero-cache starts restoring a replica at
54
- * minute 0, and new watermarks are replicated at minutes 1, 2, 3, 4, and 5,
55
- * purging changelog records as soon as those watermarks are replicated would
56
- * result in the zero-cache not being able to catch up from minute 0 once it
57
- * has finished restoring the replica.
58
- *
59
- * The `/snapshot` reservation protocol is used to prevent premature change
60
- * log cleanup:
61
- * - Clients restoring a snapshot initiate a `/snapshot` request and hold that
62
- * request open while it restores its snapshot, prepares it, and
63
- * starts its subscription to the change stream. During this time, no
64
- * cleanups are scheduled.
65
- * - When the subscription is started, the interval since the beginning of
66
- * of the reservation is tracked to increase the background cleanup delay
67
- * interval if needed. The reservation is ended (and request closed), and
68
- * cleanup scheduling is resumed with the current delay interval.
69
- *
70
- * Note that the reservation request is the primary mechanism by which
71
- * premature change log cleanup is prevented. The cleanup delay interval is
72
- * a secondary safeguard.
73
- *
74
- * Additionally, because the watermarks reported by litestream metrics
75
- * reflect what litestream *believes* has been backed up (which has been
76
- * observed to diverge from reality when uploads silently fail), the cleanup
77
- * watermark is only advanced after verifying it against the actual backup
78
- * state in the replica destination via a {@link BackupStateVerifier}. If the
79
- * actual backup state stays behind the claimed progress for longer than
80
- * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the
81
- * process is shut down rather than risk corrupting the backup.
82
- */
83
- export declare class BackupMonitor implements Service {
84
- #private;
85
- readonly id = "backup-monitor";
86
- constructor(lc: LogContext, replicaFile: string, backupURL: string, metricsEndpoint: string, changeStreamer: ChangeStreamerService, initialCleanupDelayMs: number, verifyBackupState: BackupStateVerifier);
87
- run(): Promise<void>;
4
+ export interface BackupMonitor extends SingletonService {
88
5
  startSnapshotReservation(taskID: string): Subscription<SnapshotMessage>;
89
- endReservation(taskID: string, updateCleanupDelay?: boolean): void;
90
- readonly checkWatermarksAndScheduleCleanup: () => Promise<void>;
91
- stop(): Promise<void>;
6
+ endReservation(taskID: string, updateCleanupDelay?: boolean | undefined): void;
92
7
  }
93
8
  //# sourceMappingURL=backup-monitor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backup-monitor.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/backup-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAUjD,OAAO,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAEzD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAEnD,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAGxC;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AAOtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAC3C,QAAQ,CAAC,EAAE,oBAAoB;gBAsC7B,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,qBAAqB,EACrC,qBAAqB,EAAE,MAAM,EAC7B,iBAAiB,EAAE,mBAAmB;IAkBxC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAcpB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,eAAe,CAAC;IA6BvE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,kBAAkB,UAAO;IAoBxD,QAAQ,CAAC,iCAAiC,sBAWxC;IAqMF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAkDtB"}
1
+ {"version":3,"file":"backup-monitor.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/backup-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAEnD,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;IACxE,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,GACvC,IAAI,CAAC;CACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer.ts"],"sourcesContent":["import type {Enum} from '../../../../shared/src/enum.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {changeStreamDataSchema} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport {changeSourceTimingsSchema} from '../replicator/reporter/report-schema.ts';\nimport type {Service} from '../service.ts';\nimport * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * The ChangeStreamer is the component between replicators (\"subscribers\")\n * and a canonical upstream source of changes (e.g. a Postgres logical\n * replication slot). It facilitates multiple subscribers without incurring\n * the associated upstream expense (e.g. PG replication slots are resource\n * intensive) with a \"forward-store-ack\" procedure.\n *\n * * Changes from the upstream source are immediately **forwarded** to\n * connected subscribers to minimize latency.\n *\n * * They are then **stored** in a separate DB to facilitate catchup\n * of connecting subscribers that are behind.\n *\n * * **Acknowledgements** are sent upstream after they are successfully\n * stored.\n *\n * Unlike Postgres replication slots, in which the progress of a static\n * subscriber is tracked in the replication slot, the ChangeStreamer\n * supports a dynamic set of subscribers (i.e.. zero-caches) that can\n * can continually change.\n *\n * However, it is not the case that the ChangeStreamer needs to support\n * arbitrarily old subscribers. Because the replica is continually\n * backed up to a global location and used to initialize new subscriber\n * tasks, an initial subscription request from a subscriber constitutes\n * a signal for how \"behind\" a new subscriber task can be. This is\n * reflected in the {@link SubscriberContext}, which indicates whether\n * the watermark corresponds to an \"initial\" watermark derived from the\n * replica at task startup.\n *\n * The ChangeStreamer uses a combination of this signal with ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\n */\nexport interface ChangeStreamer {\n /**\n * Subscribes to changes based on the supplied subscriber `ctx`,\n * which indicates the watermark at which the subscriber is up to\n * date.\n */\n subscribe(ctx: SubscriberContext): Promise<Source<Downstream>>;\n}\n\n// v1: v0.18\n// - Client-side support for JSON_FORMAT. Introduced in 0.18.\n// v2: v0.19\n// - Adds the \"status\" message which is initially used to signal that the\n// subscription is valid (i.e. starting at the requested watermark).\n// v3: v0.20\n// - Adds the \"taskID\" to the subscription context, and support for\n// the BackupMonitor-mediated \"/snapshot\" request.\n// v4: v0.25\n// - Adds the \"replicaVersion\" and \"minWatermark\" fields to the \"/snapshot\"\n// status request so that a subscriber can verify whether its replica,\n// whether it be restored or existing in a permanent volume, is compatible\n// with the change-streamer.\n// v5: v0.26\n// - Moves relation.keyColumns and relation.replicaIdentity to\n// relation.rowKey: { columns, type }.\n// - Adds `metadata` to `create-table` message\n// - Adds `tableMetadata` to `add-column` message\n// - Adds `table-update-metadata` message\n// v6: v0.26\n// - Adds support for `backfill` messages\n// v6: v1.0.1 (backwards compatible, no version change)\n// - Adds lag reporting to status messages\n\nexport const PROTOCOL_VERSION = 6;\n\nexport type SubscriberContext = {\n /**\n * The supported change-streamer protocol version.\n */\n protocolVersion: number;\n\n /**\n * Task ID. This is used to link the request with a preceding snapshot\n * reservation.\n */\n taskID: string | null; // TODO: Make required when v3 is min.\n\n /**\n * Subscriber id. This is only used for debugging.\n */\n id: string;\n\n /**\n * The ReplicatorMode of the subscriber. 'backup' indicates that the\n * subscriber is local to the `change-streamer` in the `replication-manager`,\n * while 'serving' indicates that user-facing requests depend on the subscriber.\n */\n mode: ReplicatorMode;\n\n /**\n * The ChangeStreamer will return an Error if the subscriber is\n * on a different replica version (i.e. the initial snapshot associated\n * with the replication slot).\n */\n replicaVersion: string;\n\n /**\n * The watermark up to which the subscriber is up to date.\n * Only changes after the watermark will be streamed.\n */\n watermark: string;\n\n /**\n * Whether this is the first subscription request made by the task,\n * i.e. indicating that the watermark comes from a restored replica\n * backup. The ChangeStreamer uses this to determine which changes\n * are safe to purge from the Storer.\n */\n initial: boolean;\n};\n\n/**\n * The StatusMessage payload for now is empty, but can be extended to\n * include meta-level information in the future.\n */\nexport const statusSchema = v.object({\n tag: v.literal('status'),\n\n lagReport: v\n .object({\n lastTimings: changeSourceTimingsSchema.optional(),\n nextSendTimeMs: v.number(),\n })\n .optional(),\n});\n\nexport type Status = v.Infer<typeof statusSchema>;\n\nexport const statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\n/**\n * A StatusMessage will be immediately sent on a (v2+) subscription to\n * indicate that the subscription is valid (i.e. starting at the requested\n * watermark). Invalid subscriptions will instead result in a\n * SubscriptionError as the first message.\n */\nexport type StatusMessage = v.Infer<typeof statusMessageSchema>;\n\nconst subscriptionErrorSchema = v.object({\n type: v.number(), // ErrorType\n message: v.string().optional(),\n});\n\nexport type SubscriptionError = v.Infer<typeof subscriptionErrorSchema>;\n\nconst errorSchema = v.tuple([v.literal('error'), subscriptionErrorSchema]);\n\nexport const downstreamSchema = v.union(\n statusMessageSchema,\n changeStreamDataSchema,\n errorSchema,\n);\n\nexport type Error = v.Infer<typeof errorSchema>;\n\nexport function errorTypeToReadableName(val: ErrorType) {\n switch (val) {\n case ErrorType.WrongReplicaVersion:\n return 'WrongReplicaVersion';\n case ErrorType.WatermarkTooOld:\n return 'WatermarkTooOld';\n case ErrorType.Unknown:\n return 'Unknown';\n default:\n return 'Unknown';\n }\n}\n\n/**\n * A stream of transactions, each starting with a {@link Begin} message,\n * containing one or more {@link Data} messages, and ending with a\n * {@link Commit} or {@link Rollback} message. The 'commit' tuple\n * includes a `watermark` that should be stored with the committed\n * data and used for resuming a subscription (e.g. in the\n * {@link SubscriberContext}).\n *\n * A {@link SubscriptionError} indicates an unrecoverable error that requires\n * manual intervention (e.g. configuration / operational error).\n */\nexport type Downstream = v.Infer<typeof downstreamSchema>;\n\nexport interface ChangeStreamerService\n extends Omit<ChangeStreamer, 'subscribe'>, Service {\n /**\n * The server-side interface overrides `subscribe()` to return a stream\n * of already-stringified {@link Downstream} payloads.\n */\n subscribe(ctx: SubscriberContext): Promise<Source<string>>;\n\n /**\n * Notifies the change streamer of a watermark that has been backed up,\n * indicating that changes before the watermark can be purged if active\n * subscribers have progressed beyond the watermark.\n */\n scheduleCleanup(watermark: string): void;\n\n getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }>;\n}\n"],"mappings":";;;;;;;;AAkIA,IAAa,eAAe,eAAE,OAAO;CACnC,KAAK,eAAE,QAAQ,QAAQ;CAEvB,WAAW,eACR,OAAO;EACN,aAAa,0BAA0B,SAAS;EAChD,gBAAgB,eAAE,OAAO;CAC3B,CAAC,EACA,SAAS;AACd,CAAC;AAID,IAAa,sBAAsB,eAAE,MAAM,CAAC,eAAE,QAAQ,QAAQ,GAAG,YAAY,CAAC;AAU9E,IAAM,0BAA0B,eAAE,OAAO;CACvC,MAAM,eAAE,OAAO;CACf,SAAS,eAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAID,IAAM,cAAc,eAAE,MAAM,CAAC,eAAE,QAAQ,OAAO,GAAG,uBAAuB,CAAC;AAEzE,IAAa,mBAAmB,eAAE,MAChC,qBACA,wBACA,WACF;AAIA,SAAgB,wBAAwB,KAAgB;CACtD,QAAQ,KAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF"}
1
+ {"version":3,"file":"change-streamer.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer.ts"],"sourcesContent":["import type {Enum} from '../../../../shared/src/enum.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {changeStreamDataSchema} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport {changeSourceTimingsSchema} from '../replicator/reporter/report-schema.ts';\nimport type {Service} from '../service.ts';\nimport * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * The ChangeStreamer is the component between replicators (\"subscribers\")\n * and a canonical upstream source of changes (e.g. a Postgres logical\n * replication slot). It facilitates multiple subscribers without incurring\n * the associated upstream expense (e.g. PG replication slots are resource\n * intensive) with a \"forward-store-ack\" procedure.\n *\n * * Changes from the upstream source are immediately **forwarded** to\n * connected subscribers to minimize latency.\n *\n * * They are then **stored** in a separate DB to facilitate catchup\n * of connecting subscribers that are behind.\n *\n * * **Acknowledgements** are sent upstream after they are successfully\n * stored.\n *\n * Unlike Postgres replication slots, in which the progress of a static\n * subscriber is tracked in the replication slot, the ChangeStreamer\n * supports a dynamic set of subscribers (i.e.. zero-caches) that can\n * can continually change.\n *\n * However, it is not the case that the ChangeStreamer needs to support\n * arbitrarily old subscribers. Because the replica is continually\n * backed up to a global location and used to initialize new subscriber\n * tasks, an initial subscription request from a subscriber constitutes\n * a signal for how \"behind\" a new subscriber task can be. This is\n * reflected in the {@link SubscriberContext}, which indicates whether\n * the watermark corresponds to an \"initial\" watermark derived from the\n * replica at task startup.\n *\n * The ChangeStreamer uses a combination of this signal with ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\n */\nexport interface ChangeStreamer {\n /**\n * Subscribes to changes based on the supplied subscriber `ctx`,\n * which indicates the watermark at which the subscriber is up to\n * date.\n */\n subscribe(ctx: SubscriberContext): Promise<Source<Downstream>>;\n}\n\n// v1: v0.18\n// - Client-side support for JSON_FORMAT. Introduced in 0.18.\n// v2: v0.19\n// - Adds the \"status\" message which is initially used to signal that the\n// subscription is valid (i.e. starting at the requested watermark).\n// v3: v0.20\n// - Adds the \"taskID\" to the subscription context, and support for\n// the backup monitor-mediated \"/snapshot\" request.\n// v4: v0.25\n// - Adds the \"replicaVersion\" and \"minWatermark\" fields to the \"/snapshot\"\n// status request so that a subscriber can verify whether its replica,\n// whether it be restored or existing in a permanent volume, is compatible\n// with the change-streamer.\n// v5: v0.26\n// - Moves relation.keyColumns and relation.replicaIdentity to\n// relation.rowKey: { columns, type }.\n// - Adds `metadata` to `create-table` message\n// - Adds `tableMetadata` to `add-column` message\n// - Adds `table-update-metadata` message\n// v6: v0.26\n// - Adds support for `backfill` messages\n// v6: v1.0.1 (backwards compatible, no version change)\n// - Adds lag reporting to status messages\n\nexport const PROTOCOL_VERSION = 6;\n\nexport type SubscriberContext = {\n /**\n * The supported change-streamer protocol version.\n */\n protocolVersion: number;\n\n /**\n * Task ID. This is used to link the request with a preceding snapshot\n * reservation.\n */\n taskID: string | null; // TODO: Make required when v3 is min.\n\n /**\n * Subscriber id. This is only used for debugging.\n */\n id: string;\n\n /**\n * The ReplicatorMode of the subscriber. 'backup' indicates that the\n * subscriber is local to the `change-streamer` in the `replication-manager`,\n * while 'serving' indicates that user-facing requests depend on the subscriber.\n */\n mode: ReplicatorMode;\n\n /**\n * The ChangeStreamer will return an Error if the subscriber is\n * on a different replica version (i.e. the initial snapshot associated\n * with the replication slot).\n */\n replicaVersion: string;\n\n /**\n * The watermark up to which the subscriber is up to date.\n * Only changes after the watermark will be streamed.\n */\n watermark: string;\n\n /**\n * Whether this is the first subscription request made by the task,\n * i.e. indicating that the watermark comes from a restored replica\n * backup. The ChangeStreamer uses this to determine which changes\n * are safe to purge from the Storer.\n */\n initial: boolean;\n};\n\n/**\n * The StatusMessage payload for now is empty, but can be extended to\n * include meta-level information in the future.\n */\nexport const statusSchema = v.object({\n tag: v.literal('status'),\n\n lagReport: v\n .object({\n lastTimings: changeSourceTimingsSchema.optional(),\n nextSendTimeMs: v.number(),\n })\n .optional(),\n});\n\nexport type Status = v.Infer<typeof statusSchema>;\n\nexport const statusMessageSchema = v.tuple([v.literal('status'), statusSchema]);\n\n/**\n * A StatusMessage will be immediately sent on a (v2+) subscription to\n * indicate that the subscription is valid (i.e. starting at the requested\n * watermark). Invalid subscriptions will instead result in a\n * SubscriptionError as the first message.\n */\nexport type StatusMessage = v.Infer<typeof statusMessageSchema>;\n\nconst subscriptionErrorSchema = v.object({\n type: v.number(), // ErrorType\n message: v.string().optional(),\n});\n\nexport type SubscriptionError = v.Infer<typeof subscriptionErrorSchema>;\n\nconst errorSchema = v.tuple([v.literal('error'), subscriptionErrorSchema]);\n\nexport const downstreamSchema = v.union(\n statusMessageSchema,\n changeStreamDataSchema,\n errorSchema,\n);\n\nexport type Error = v.Infer<typeof errorSchema>;\n\nexport function errorTypeToReadableName(val: ErrorType) {\n switch (val) {\n case ErrorType.WrongReplicaVersion:\n return 'WrongReplicaVersion';\n case ErrorType.WatermarkTooOld:\n return 'WatermarkTooOld';\n case ErrorType.Unknown:\n return 'Unknown';\n default:\n return 'Unknown';\n }\n}\n\n/**\n * A stream of transactions, each starting with a {@link Begin} message,\n * containing one or more {@link Data} messages, and ending with a\n * {@link Commit} or {@link Rollback} message. The 'commit' tuple\n * includes a `watermark` that should be stored with the committed\n * data and used for resuming a subscription (e.g. in the\n * {@link SubscriberContext}).\n *\n * A {@link SubscriptionError} indicates an unrecoverable error that requires\n * manual intervention (e.g. configuration / operational error).\n */\nexport type Downstream = v.Infer<typeof downstreamSchema>;\n\nexport interface ChangeStreamerService\n extends Omit<ChangeStreamer, 'subscribe'>, Service {\n /**\n * The server-side interface overrides `subscribe()` to return a stream\n * of already-stringified {@link Downstream} payloads.\n */\n subscribe(ctx: SubscriberContext): Promise<Source<string>>;\n\n /**\n * Notifies the change streamer of a watermark that has been backed up,\n * indicating that changes before the watermark can be purged if active\n * subscribers have progressed beyond the watermark.\n */\n scheduleCleanup(watermark: string): void;\n\n getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }>;\n}\n"],"mappings":";;;;;;;;AAkIA,IAAa,eAAe,eAAE,OAAO;CACnC,KAAK,eAAE,QAAQ,QAAQ;CAEvB,WAAW,eACR,OAAO;EACN,aAAa,0BAA0B,SAAS;EAChD,gBAAgB,eAAE,OAAO;CAC3B,CAAC,EACA,SAAS;AACd,CAAC;AAID,IAAa,sBAAsB,eAAE,MAAM,CAAC,eAAE,QAAQ,QAAQ,GAAG,YAAY,CAAC;AAU9E,IAAM,0BAA0B,eAAE,OAAO;CACvC,MAAM,eAAE,OAAO;CACf,SAAS,eAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAID,IAAM,cAAc,eAAE,MAAM,CAAC,eAAE,QAAQ,OAAO,GAAG,uBAAuB,CAAC;AAEzE,IAAa,mBAAmB,eAAE,MAChC,qBACA,wBACA,WACF;AAIA,SAAgB,wBAAwB,KAAgB;CACtD,QAAQ,KAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF"}
@@ -0,0 +1,114 @@
1
+ import type { LogContext } from '@rocicorp/logger';
2
+ import { Subscription } from '../../types/subscription.ts';
3
+ import type { BackupMonitor } from './backup-monitor.ts';
4
+ import type { ChangeStreamerService } from './change-streamer.ts';
5
+ import type { SnapshotMessage } from './snapshot.ts';
6
+ export declare const CHECK_INTERVAL_MS = 60000;
7
+ /**
8
+ * Allowance for clock skew between the machine reporting litestream metrics
9
+ * and the timestamps reported by the backup destination (e.g. S3).
10
+ */
11
+ export declare const BACKUP_VERIFICATION_SLACK_MS = 60000;
12
+ /**
13
+ * How long the actual backup state may remain *continuously* behind the
14
+ * backup progress claimed by litestream metrics before the backup is
15
+ * considered genuinely wedged and the process is shut down.
16
+ *
17
+ * A wedged backup is dangerous: litestream believes it is making durable
18
+ * progress when it is not, which can silently corrupt the backup (the WAL
19
+ * format only makes *some* such gaps detectable). Once that is confirmed to
20
+ * be the persistent state, continuing to run is worse than crashing, so the
21
+ * process exits loudly (non-zero, with a logged error) and the wedged backup
22
+ * destination becomes the priority to investigate.
23
+ *
24
+ * The change-log is conservatively *not* purged for the entire time the
25
+ * backup is stale, so the only cost of a generous grace period is unbounded
26
+ * change-log growth. We therefore err well on the side of slack: the backup
27
+ * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear
28
+ * the server down, so that a transient hiccup (a slow or briefly unreachable
29
+ * destination, or litestream restarting) does not trigger a shutdown. The
30
+ * staleness clock is reset the moment a purge is confirmed against a real
31
+ * upload.
32
+ */
33
+ export declare const WEDGED_SHUTDOWN_GRACE_MS: number;
34
+ /**
35
+ * How long the BackupMonitor waits for the *first* restorable backup to appear
36
+ * before concluding that the backup pipeline is broken and shutting the process
37
+ * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).
38
+ *
39
+ * On a fresh stack the first backup is not restorable until the initial
40
+ * Postgres->replica sync completes and litestream finishes uploading the
41
+ * initial snapshot — a multipart upload that is not listable in the destination
42
+ * until it is committed. The producer cannot observe that sub-snapshot progress,
43
+ * so this is necessarily a generous timeout. It is scaled by replica size to
44
+ * mirror the platform's storage-scaled startup-probe allowance (~1 hour per
45
+ * 50GB) so that it never fires before the platform would restart a view-syncer
46
+ * that is waiting on the backup.
47
+ */
48
+ export declare const INITIAL_BACKUP_GRACE_MS_PER_UNIT: number;
49
+ /**
50
+ * How often the BackupMonitor re-checks whether the first restorable backup has
51
+ * appeared while a view-syncer holds a snapshot reservation open during a cold
52
+ * start.
53
+ */
54
+ export declare const RESTORABLE_BACKUP_POLL_INTERVAL_MS = 10000;
55
+ /**
56
+ * Returns the time of the most recent object actually uploaded to the
57
+ * backup replica destination (e.g. as determined by listing the snapshots
58
+ * and WAL segments in S3). Rejects if the backup state cannot be determined.
59
+ *
60
+ * See `getLastBackupTime()` in `../litestream/commands.ts` for the
61
+ * production implementation.
62
+ */
63
+ export type BackupStateVerifier = () => Promise<Date>;
64
+ /**
65
+ * The Litestream3BackupMonitor polls the litestream "/metrics" endpoint to
66
+ * track the watermark (label) value of the `litestream_replica_progress` gauge
67
+ * and schedules cleanup of change log entries that can be purged as a result.
68
+ *
69
+ * See: https://github.com/rocicorp/litestream/pull/3
70
+ *
71
+ * Note that change log entries cannot simply be purged as soon as they
72
+ * have been applied and backed up by litestream. Consider the case in which
73
+ * litestream backs up new wal segments every minute, but it takes 5 minutes
74
+ * to restore a replica: if a zero-cache starts restoring a replica at
75
+ * minute 0, and new watermarks are replicated at minutes 1, 2, 3, 4, and 5,
76
+ * purging changelog records as soon as those watermarks are replicated would
77
+ * result in the zero-cache not being able to catch up from minute 0 once it
78
+ * has finished restoring the replica.
79
+ *
80
+ * The `/snapshot` reservation protocol is used to prevent premature change
81
+ * log cleanup:
82
+ * - Clients restoring a snapshot initiate a `/snapshot` request and hold that
83
+ * request open while it restores its snapshot, prepares it, and
84
+ * starts its subscription to the change stream. During this time, no
85
+ * cleanups are scheduled.
86
+ * - When the subscription is started, the interval since the beginning of
87
+ * of the reservation is tracked to increase the background cleanup delay
88
+ * interval if needed. The reservation is ended (and request closed), and
89
+ * cleanup scheduling is resumed with the current delay interval.
90
+ *
91
+ * Note that the reservation request is the primary mechanism by which
92
+ * premature change log cleanup is prevented. The cleanup delay interval is
93
+ * a secondary safeguard.
94
+ *
95
+ * Additionally, because the watermarks reported by litestream metrics
96
+ * reflect what litestream *believes* has been backed up (which has been
97
+ * observed to diverge from reality when uploads silently fail), the cleanup
98
+ * watermark is only advanced after verifying it against the actual backup
99
+ * state in the replica destination via a {@link BackupStateVerifier}. If the
100
+ * actual backup state stays behind the claimed progress for longer than
101
+ * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the
102
+ * process is shut down rather than risk corrupting the backup.
103
+ */
104
+ export declare class Litestream3BackupMonitor implements BackupMonitor {
105
+ #private;
106
+ readonly id = "backup-monitor";
107
+ constructor(lc: LogContext, replicaFile: string, backupURL: string, metricsEndpoint: string, changeStreamer: ChangeStreamerService, initialCleanupDelayMs: number, verifyBackupState: BackupStateVerifier);
108
+ run(): Promise<void>;
109
+ startSnapshotReservation(taskID: string): Subscription<SnapshotMessage>;
110
+ endReservation(taskID: string, updateCleanupDelay?: boolean): void;
111
+ readonly checkWatermarksAndScheduleCleanup: () => Promise<void>;
112
+ stop(): Promise<void>;
113
+ }
114
+ //# sourceMappingURL=litestream3-backup-monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"litestream3-backup-monitor.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-backup-monitor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAEzD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAEnD,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAGxC;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC;AAIpD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gCAAgC,QAAc,CAAC;AAG5D;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AAOtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBAAa,wBAAyB,YAAW,aAAa;;IAC5D,QAAQ,CAAC,EAAE,oBAAoB;gBA8C7B,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,qBAAqB,EACrC,qBAAqB,EAAE,MAAM,EAC7B,iBAAiB,EAAE,mBAAmB;IAkBxC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAepB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,eAAe,CAAC;IAgFvE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,kBAAkB,UAAO;IAoBxD,QAAQ,CAAC,iCAAiC,sBAgBxC;IAuQF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAkDtB"}
@@ -1,12 +1,14 @@
1
1
  import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
2
+ import { sleep } from "../../../../shared/src/sleep.js";
2
3
  import { must } from "../../../../shared/src/must.js";
3
4
  import { Database } from "../../../../zqlite/src/db.js";
4
5
  import { getOrCreateCounter, getOrCreateGauge } from "../../observability/metrics.js";
5
6
  import { RunningState, UnrecoverableError } from "../running-state.js";
6
7
  import { Subscription } from "../../types/subscription.js";
7
8
  import { resolver } from "@rocicorp/resolver";
9
+ import { statSync } from "node:fs";
8
10
  import parsePrometheusTextFormat from "parse-prometheus-text-format";
9
- //#region ../zero-cache/src/services/change-streamer/backup-monitor.ts
11
+ //#region ../zero-cache/src/services/change-streamer/litestream3-backup-monitor.ts
10
12
  var CHECK_INTERVAL_MS = 6e4;
11
13
  var MIN_CLEANUP_DELAY_MS = 3e4;
12
14
  /**
@@ -36,10 +38,33 @@ var BACKUP_VERIFICATION_SLACK_MS = 6e4;
36
38
  * upload.
37
39
  */
38
40
  var WEDGED_SHUTDOWN_GRACE_MS = 15 * 6e4;
41
+ var GiB = 1024 ** 3;
39
42
  /**
40
- * The BackupMonitor polls the litestream "/metrics" endpoint to track the
41
- * watermark (label) value of the `litestream_replica_progress` gauge and
42
- * schedules cleanup of change log entries that can be purged as a result.
43
+ * How long the BackupMonitor waits for the *first* restorable backup to appear
44
+ * before concluding that the backup pipeline is broken and shutting the process
45
+ * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).
46
+ *
47
+ * On a fresh stack the first backup is not restorable until the initial
48
+ * Postgres->replica sync completes and litestream finishes uploading the
49
+ * initial snapshot — a multipart upload that is not listable in the destination
50
+ * until it is committed. The producer cannot observe that sub-snapshot progress,
51
+ * so this is necessarily a generous timeout. It is scaled by replica size to
52
+ * mirror the platform's storage-scaled startup-probe allowance (~1 hour per
53
+ * 50GB) so that it never fires before the platform would restart a view-syncer
54
+ * that is waiting on the backup.
55
+ */
56
+ var INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 6e4;
57
+ var INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB;
58
+ /**
59
+ * How often the BackupMonitor re-checks whether the first restorable backup has
60
+ * appeared while a view-syncer holds a snapshot reservation open during a cold
61
+ * start.
62
+ */
63
+ var RESTORABLE_BACKUP_POLL_INTERVAL_MS = 1e4;
64
+ /**
65
+ * The Litestream3BackupMonitor polls the litestream "/metrics" endpoint to
66
+ * track the watermark (label) value of the `litestream_replica_progress` gauge
67
+ * and schedules cleanup of change log entries that can be purged as a result.
43
68
  *
44
69
  * See: https://github.com/rocicorp/litestream/pull/3
45
70
  *
@@ -76,7 +101,7 @@ var WEDGED_SHUTDOWN_GRACE_MS = 15 * 6e4;
76
101
  * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the
77
102
  * process is shut down rather than risk corrupting the backup.
78
103
  */
79
- var BackupMonitor = class {
104
+ var Litestream3BackupMonitor = class {
80
105
  id = "backup-monitor";
81
106
  #lc;
82
107
  #replicaFile;
@@ -93,6 +118,7 @@ var BackupMonitor = class {
93
118
  #latestBackupTime = null;
94
119
  #lastVerifiedUploadTime = null;
95
120
  #backupStaleSince = null;
121
+ #runStartTime = null;
96
122
  #cleanupDelayMs;
97
123
  #checkMetricsTimer;
98
124
  constructor(lc, replicaFile, backupURL, metricsEndpoint, changeStreamer, initialCleanupDelayMs, verifyBackupState) {
@@ -106,6 +132,7 @@ var BackupMonitor = class {
106
132
  this.#lc.info?.(`backup monitor started ${initialCleanupDelayMs} ms after snapshot restore`);
107
133
  }
108
134
  run() {
135
+ this.#runStartTime = Date.now();
109
136
  this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint} with ${this.#cleanupDelayMs} ms cleanup delay`);
110
137
  this.#checkMetricsTimer = setInterval(this.checkWatermarksAndScheduleCleanup, CHECK_INTERVAL_MS);
111
138
  this.#initBackupLagMetric();
@@ -119,17 +146,53 @@ var BackupMonitor = class {
119
146
  start: /* @__PURE__ */ new Date(),
120
147
  sub
121
148
  });
122
- this.#changeStreamer.getChangeLogState().then((changeLogState) => {
123
- sub.push(["status", {
149
+ this.#pushStatusWhenRestorable(taskID, sub);
150
+ return sub;
151
+ }
152
+ /**
153
+ * Pushes the `status` snapshot signal once a restorable backup is confirmed
154
+ * to exist in the backup destination. On a warm start this is immediate; on a
155
+ * cold start (a fresh stack whose first backup is still being produced) the
156
+ * reservation is held open — kept alive by websocket liveness pings — and the
157
+ * view-syncer stays unready until the first backup lands, rather than being
158
+ * told to restore a backup that does not yet exist.
159
+ */
160
+ async #pushStatusWhenRestorable(taskID, sub) {
161
+ try {
162
+ while (this.#lastVerifiedUploadTime === null) {
163
+ if (!sub.active) return;
164
+ if (await this.#confirmRestorableBackup()) break;
165
+ this.#lc.info?.(`no restorable backup at ${this.#backupURL} yet; holding ${taskID}'s reservation open until it lands`);
166
+ await sleep(RESTORABLE_BACKUP_POLL_INTERVAL_MS, this.#state.signal);
167
+ }
168
+ const changeLogState = await this.#changeStreamer.getChangeLogState();
169
+ if (sub.active) sub.push(["status", {
124
170
  tag: "status",
125
171
  backupURL: this.#backupURL,
126
172
  ...changeLogState
127
173
  }]);
128
- }).catch((e) => {
174
+ } catch (e) {
175
+ if (this.#state.signal.aborted) return;
129
176
  this.#lc.warn?.(`failing snapshot reservation`, e);
130
- sub.fail(e);
131
- });
132
- return sub;
177
+ sub.fail(e instanceof Error ? e : new Error(String(e)));
178
+ }
179
+ }
180
+ /**
181
+ * Verifies whether a restorable backup actually exists in the destination
182
+ * (as opposed to what litestream metrics merely claim). On success the
183
+ * verified upload time is cached — seeding the cleanup-verification fast path
184
+ * ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.
185
+ * Returns false if no backup is listable yet (e.g. the initial snapshot is
186
+ * still uploading).
187
+ */
188
+ async #confirmRestorableBackup() {
189
+ try {
190
+ this.#lastVerifiedUploadTime = await this.#verifyBackupState();
191
+ return true;
192
+ } catch (e) {
193
+ this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);
194
+ return false;
195
+ }
133
196
  }
134
197
  endReservation(taskID, updateCleanupDelay = true) {
135
198
  const res = this.#reservations.get(taskID);
@@ -157,6 +220,11 @@ var BackupMonitor = class {
157
220
  } catch (e) {
158
221
  this.#lc.warn?.(`error scheduling cleanup`, e);
159
222
  }
223
+ try {
224
+ await this.#checkInitialBackupDeadline();
225
+ } catch (e) {
226
+ this.#lc.warn?.(`error checking initial backup deadline`, e);
227
+ }
160
228
  };
161
229
  async *#fetchWatermarks() {
162
230
  const metricsEndpoint = this.#metricsEndpoint;
@@ -241,6 +309,47 @@ var BackupMonitor = class {
241
309
  this.#fatal.reject(err);
242
310
  }
243
311
  /**
312
+ * On a fresh stack, fails loudly if no restorable backup has appeared within
313
+ * a generous, replica-size-scaled grace period (see
314
+ * {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the "give up" decision
315
+ * onto the replication-manager — the producer responsible for creating the
316
+ * backup — instead of leaving view-syncers to wait (and the platform to keep
317
+ * restarting them) while the real fault is here. Does nothing once a
318
+ * restorable backup has been confirmed.
319
+ */
320
+ async #checkInitialBackupDeadline() {
321
+ if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) return;
322
+ const elapsed = Date.now() - this.#runStartTime;
323
+ if (elapsed < this.#initialBackupGraceMs()) return;
324
+ if (await this.#confirmRestorableBackup()) return;
325
+ this.#shutDownOnMissingInitialBackup(elapsed);
326
+ }
327
+ /**
328
+ * The grace period for the first restorable backup, scaled by replica size to
329
+ * mirror the platform's storage-scaled startup-probe allowance. Falls back to
330
+ * the minimum (one unit) if the replica file size cannot be determined.
331
+ */
332
+ #initialBackupGraceMs() {
333
+ let bytes = 0;
334
+ try {
335
+ bytes = statSync(this.#replicaFile).size;
336
+ } catch {}
337
+ return Math.max(1, Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES)) * INITIAL_BACKUP_GRACE_MS_PER_UNIT;
338
+ }
339
+ /**
340
+ * Called when no restorable backup has appeared within
341
+ * {@link #initialBackupGraceMs}. Continuing to run is pointless — no
342
+ * view-syncer can restore — and the fault is the backup pipeline, so the
343
+ * process exits non-zero with a logged error for alerting, mirroring
344
+ * {@link #shutDownOnWedgedBackup}.
345
+ */
346
+ #shutDownOnMissingInitialBackup(elapsedMs) {
347
+ const err = new UnrecoverableError(`no restorable backup has appeared at ${this.#backupURL} within ${elapsedMs} ms of startup (grace period ${this.#initialBackupGraceMs()} ms). The initial backup pipeline appears to be broken; shutting down so that the replication-manager is flagged as the cause rather than the view-syncers waiting on it. Investigate litestream replication to ${this.#backupURL}.`);
348
+ this.#lc.error?.(err.message);
349
+ clearInterval(this.#checkMetricsTimer);
350
+ this.#fatal.reject(err);
351
+ }
352
+ /**
244
353
  * Returns the newest watermark whose backup time is at or before `cutoff`
245
354
  * (epoch ms), or `''` if there is none.
246
355
  */
@@ -290,6 +399,6 @@ var BackupMonitor = class {
290
399
  }
291
400
  };
292
401
  //#endregion
293
- export { BackupMonitor };
402
+ export { Litestream3BackupMonitor };
294
403
 
295
- //# sourceMappingURL=backup-monitor.js.map
404
+ //# sourceMappingURL=litestream3-backup-monitor.js.map