@rocicorp/zero 1.10.0-canary.2 → 1.10.0-canary.21
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.
- package/out/replicache/src/btree/read.d.ts +12 -1
- package/out/replicache/src/btree/read.d.ts.map +1 -1
- package/out/replicache/src/btree/read.js +24 -12
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/db/rebase.d.ts +14 -2
- package/out/replicache/src/db/rebase.d.ts.map +1 -1
- package/out/replicache/src/db/rebase.js +27 -5
- package/out/replicache/src/db/rebase.js.map +1 -1
- package/out/replicache/src/db/write.js.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js +27 -4
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/sqlite-store.d.ts +11 -0
- package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
- package/out/replicache/src/kv/sqlite-store.js +43 -3
- package/out/replicache/src/kv/sqlite-store.js.map +1 -1
- package/out/replicache/src/persist/persist.d.ts.map +1 -1
- package/out/replicache/src/persist/persist.js +12 -5
- package/out/replicache/src/persist/persist.js.map +1 -1
- package/out/replicache/src/persist/refresh.js +6 -2
- package/out/replicache/src/persist/refresh.js.map +1 -1
- package/out/replicache/src/replicache-impl.d.ts.map +1 -1
- package/out/replicache/src/replicache-impl.js +14 -2
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/replicache/src/replicache-options.d.ts +9 -0
- package/out/replicache/src/replicache-options.d.ts.map +1 -1
- package/out/replicache/src/transactions.d.ts.map +1 -1
- package/out/replicache/src/transactions.js +1 -1
- package/out/replicache/src/transactions.js.map +1 -1
- package/out/shared/src/error.d.ts +11 -0
- package/out/shared/src/error.d.ts.map +1 -1
- package/out/shared/src/error.js +35 -1
- package/out/shared/src/error.js.map +1 -1
- package/out/shared/src/hash.d.ts.map +1 -1
- package/out/shared/src/hash.js +105 -10
- package/out/shared/src/hash.js.map +1 -1
- package/out/shared/src/map.d.ts +7 -2
- package/out/shared/src/map.d.ts.map +1 -1
- package/out/shared/src/map.js.map +1 -1
- package/out/shared/src/promise-race.d.ts +14 -0
- package/out/shared/src/promise-race.d.ts.map +1 -1
- package/out/shared/src/promise-race.js +32 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/zero/package.js +2 -3
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js +8 -1
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts +5 -0
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +16 -3
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +36 -7
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +121 -46
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/create.d.ts.map +1 -1
- package/out/zero-cache/src/db/create.js +4 -1
- package/out/zero-cache/src/db/create.js.map +1 -1
- package/out/zero-cache/src/db/index-predicate.d.ts +4 -0
- package/out/zero-cache/src/db/index-predicate.d.ts.map +1 -0
- package/out/zero-cache/src/db/index-predicate.js +46 -0
- package/out/zero-cache/src/db/index-predicate.js.map +1 -0
- package/out/zero-cache/src/db/lite-tables.d.ts +10 -2
- package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
- package/out/zero-cache/src/db/lite-tables.js +8 -3
- package/out/zero-cache/src/db/lite-tables.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +5 -3
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/specs.d.ts +41 -0
- package/out/zero-cache/src/db/specs.d.ts.map +1 -1
- package/out/zero-cache/src/db/specs.js +30 -3
- package/out/zero-cache/src/db/specs.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.d.ts +5 -15
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +39 -73
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +27 -15
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +4 -4
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +8 -4
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +1 -1
- package/out/zero-cache/src/server/worker-urls.d.ts +0 -1
- package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js +1 -2
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/analyze.d.ts.map +1 -1
- package/out/zero-cache/src/services/analyze.js +1 -0
- package/out/zero-cache/src/services/analyze.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts +12 -2
- package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +14 -3
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +10 -41
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts +3 -2
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +42 -16
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +34 -2
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +47 -19
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/decommission.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/decommission.js +20 -11
- package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +58 -80
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +20 -0
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +43 -3
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +24 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js +35 -5
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +1513 -229
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts +19 -0
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js +581 -0
- package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +15 -6
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts +137 -20
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js +82 -13
- package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +128 -21
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +5 -5
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +2 -0
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +3 -0
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts +1 -4
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +11 -18
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +8 -3
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +4 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-compare-digest.d.ts +34 -0
- package/out/zero-cache/src/services/change-streamer/change-log-compare-digest.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-log-compare-digest.js +67 -0
- package/out/zero-cache/src/services/change-streamer/change-log-compare-digest.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +2 -4
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +8 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +34 -13
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +165 -36
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +4 -9
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js +12 -3
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +11 -5
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/shard-sampling.d.ts +11 -0
- package/out/zero-cache/src/services/change-streamer/shard-sampling.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/shard-sampling.js +19 -0
- package/out/zero-cache/src/services/change-streamer/shard-sampling.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +11 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +38 -5
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts +25 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +95 -12
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.d.ts +83 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js +417 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-comparator.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.d.ts +73 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js +157 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js +8 -3
- package/out/zero-cache/src/services/change-streamer/sqlite-change-log-reader.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +63 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +217 -49
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +8 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +10 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts +29 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +154 -0
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +1 -0
- package/out/zero-cache/src/services/http-service.d.ts +1 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +7 -3
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +16 -7
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/config-v5.yml +15 -1
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js +131 -0
- package/out/zero-cache/src/services/litestream/litestream-checkpointer.js.map +1 -0
- package/out/zero-cache/src/services/litestream/litestream-controller.d.ts +70 -0
- package/out/zero-cache/src/services/litestream/litestream-controller.d.ts.map +1 -0
- package/out/zero-cache/src/services/litestream/litestream-controller.js +101 -0
- package/out/zero-cache/src/services/litestream/litestream-controller.js.map +1 -0
- package/out/zero-cache/src/services/litestream/metrics.d.ts +1 -0
- package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.js +4 -1
- package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-db.d.ts +10 -0
- package/out/zero-cache/src/services/replicator/change-log-db.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-log-db.js +15 -1
- package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +18 -21
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +2 -6
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +2 -2
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts +3 -2
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js +8 -6
- package/out/zero-cache/src/services/replicator/sqlite-change-log-observability.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.d.ts +27 -2
- package/out/zero-cache/src/services/replicator/write-worker-client.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js +3 -2
- package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +24 -5
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +3 -2
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +25 -17
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +14 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +3 -3
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +6 -2
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/downstream.d.ts +11 -0
- package/out/zero-cache/src/types/downstream.d.ts.map +1 -0
- package/out/zero-cache/src/types/pg.d.ts +15 -2
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +21 -8
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.d.ts +18 -0
- package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -0
- package/out/zero-cache/src/types/poke-chunk.js +57 -0
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -0
- package/out/zero-cache/src/types/processes.js +1 -4
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +4 -9
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts +2 -11
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +4 -25
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +22 -4
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +112 -14
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts +1 -0
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +4 -2
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-client/src/client/connection-manager.d.ts.map +1 -1
- package/out/zero-client/src/client/connection-manager.js +46 -0
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/context.d.ts +15 -0
- package/out/zero-client/src/client/context.d.ts.map +1 -1
- package/out/zero-client/src/client/context.js +44 -0
- package/out/zero-client/src/client/context.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js +19 -12
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.d.ts +7 -2
- package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js +52 -2
- package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
- package/out/zero-client/src/client/zero-rep.d.ts +1 -0
- package/out/zero-client/src/client/zero-rep.d.ts.map +1 -1
- package/out/zero-client/src/client/zero-rep.js +9 -6
- package/out/zero-client/src/client/zero-rep.js.map +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +98 -25
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/custom-queries.d.ts +1 -1
- package/out/zero-protocol/src/down.d.ts +1 -1
- package/out/zero-protocol/src/error.d.ts +4 -4
- package/out/zero-protocol/src/inspect-down.js +1 -1
- package/out/zero-protocol/src/mutate-server.d.ts +2 -2
- package/out/zero-protocol/src/poke.d.ts +89 -7
- package/out/zero-protocol/src/poke.d.ts.map +1 -1
- package/out/zero-protocol/src/poke.js +16 -8
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-protocol/src/protocol-version.d.ts +12 -7
- package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
- package/out/zero-protocol/src/protocol-version.js.map +1 -1
- package/out/zero-protocol/src/push.d.ts +1 -1
- package/out/zero-protocol/src/query-server.d.ts +2 -2
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +5 -3
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts.map +1 -1
- package/out/zql/src/ivm/data.js +8 -3
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/deferred-input.d.ts +44 -0
- package/out/zql/src/ivm/deferred-input.d.ts.map +1 -0
- package/out/zql/src/ivm/deferred-input.js +88 -0
- package/out/zql/src/ivm/deferred-input.js.map +1 -0
- package/out/zql/src/ivm/memory-source.d.ts +10 -2
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +19 -16
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/stream.d.ts +0 -1
- package/out/zql/src/ivm/stream.d.ts.map +1 -1
- package/out/zql/src/ivm/stream.js +1 -7
- package/out/zql/src/ivm/stream.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +11 -2
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +6 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.d.ts +6 -0
- package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +73 -10
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-delegate.d.ts +22 -0
- package/out/zql/src/query/query-delegate.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js +2 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.d.ts.map +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.js +1 -0
- package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +3 -2
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +2 -3
- package/out/zero-cache/src/server/backup-watermark-poller.js +0 -31
- package/out/zero-cache/src/server/backup-watermark-poller.js.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts +0 -57
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.d.ts.map +0 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js +0 -137
- package/out/zero-cache/src/services/litestream/vfs-watermark-poller.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-comparator.js","names":["#lc","#shard","#changeLogFile","#identity","#pg","#opts","#now","#setTimeoutFn","#clearTimeoutFn","#yield","#compareResults","#compareCycles","#cycleDuration","#scheduleNext","#stopped","#running","#runCycle","#continueWithoutDelay","#rescheduleNext","#timer","#fileGeneration","#cursor","#skipped","#pinBounds","#reconfirm","#compared","#enumerateCommits","#compareTransaction","#readSQLiteBounds"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-comparator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport {\n CHANGE_LOG_DB_SCHEMA_VERSION,\n CHANGE_LOG_STREAM_TABLE,\n readChangeLogMeta,\n type ChangeLogIdentity,\n type ChangeLogMeta,\n} from '../replicator/change-log-db.ts';\nimport {\n reconstructWatermarkedChange,\n type ChangeLogEntry,\n} from './change-log-codec.ts';\nimport {\n digestCatchupRange,\n type CatchupRangeDigest,\n} from './change-log-compare-digest.ts';\nimport type {WatermarkedChange} from './change-streamer.ts';\nimport {isSampledForShard} from './shard-sampling.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\n\n/** Interval between comparison cycles. */\nconst DEFAULT_COMPARE_INTERVAL_MS = 30_000;\n\n/** Maximum transactions enumerated per cycle. */\nconst DEFAULT_MAX_TRANSACTIONS_PER_CYCLE = 256;\n\n/** Maximum catchup rows read from each store per cycle. */\nconst DEFAULT_MAX_ROWS_PER_SOURCE_PER_CYCLE = 1000;\n\n/**\n * Maximum catchup bytes read from each store per cycle.\n *\n * Rows bound the per-row overhead. Bytes bound the read and hash work,\n * which scales with payload size instead of row count. Normal traffic\n * reaches the row limit first. Wide payloads reach this one.\n */\nconst DEFAULT_MAX_BYTES_PER_SOURCE_PER_CYCLE = 32 * 1024 * 1024;\n\n/** Rows per SQLite catchup batch. */\nconst DEFAULT_READ_BATCH_ROWS = 1000;\n\n/**\n * Result for one committed transaction.\n *\n * `mismatch` covers missing, extra, changed, or reordered rows.\n * `inconclusive` means that store bounds changed during the comparison.\n */\nexport type CompareOutcome =\n | 'match'\n | 'mismatch'\n | 'inconclusive'\n | 'deferred'\n | 'oversized';\n\nexport type CompareSkipReason =\n // The comparator was stopped.\n | 'stopped'\n // The writer replaced the file during this cycle.\n | 'log-rebuilt'\n // The file is absent, unreadable, or uninitialized.\n | 'log-unavailable'\n // The schema version does not match this build.\n | 'ineligible-schema'\n // The log belongs to a different replica.\n | 'ineligible-identity'\n // The log is younger than the warm-up period.\n | 'cold-log'\n // The stores have no complete committed range in common.\n | 'nothing-to-compare'\n // The cycle failed. The error was logged and counted.\n | 'error';\n\nexport type CompareCycleResult =\n | {readonly kind: 'skipped'; readonly reason: CompareSkipReason}\n | {\n readonly kind: 'compared';\n /** Exclusive lower bound of the compared range. */\n readonly fromWatermark: string;\n /** Inclusive upper bound handled. The next cycle resumes from here. */\n readonly throughWatermark: string;\n /** Committed transactions handled through `throughWatermark`. */\n readonly transactions: number;\n /** Transactions with compared catchup output. */\n readonly sampled: number;\n readonly matched: number;\n readonly mismatched: number;\n /** Suspects rejected because store bounds changed. */\n readonly inconclusive: number;\n /** Sampled transactions deferred to a fresh budget. */\n readonly deferred: number;\n /** Sampled transactions that exceed a fresh budget. */\n readonly oversized: number;\n };\n\n/** Provides the Postgres side of the catchup comparison. */\nexport interface PGChangeLogRangeReader {\n getCatchupBounds(): Promise<{\n minWatermark: string | null;\n lastWatermark: string;\n }>;\n listCommitWatermarks(\n afterWatermark: string,\n throughWatermark: string,\n limit: number,\n ): Promise<string[]>;\n readCatchupRange(\n afterWatermark: string,\n throughWatermark: string,\n batchRows?: number | undefined,\n ): AsyncIterable<ChangeLogEntry[]>;\n}\n\nexport type SQLiteChangeLogCompareOptions = {\n /** Stable percentage of transactions selected for payload comparison. */\n readonly comparePercent: number;\n /** Warm-up time after a seed. The comparator skips a younger log. */\n readonly retentionMs: number;\n readonly intervalMs?: number | undefined;\n readonly maxTransactionsPerCycle?: number | undefined;\n /** Maximum catchup rows read from each store in one cycle. */\n readonly maxRowsPerSourcePerCycle?: number | undefined;\n /** Maximum catchup bytes read from each store in one cycle. */\n readonly maxBytesPerSourcePerCycle?: number | undefined;\n readonly readBatchRows?: number | undefined;\n /** Clock for the warm-up period. Defaults to `Date.now`. */\n readonly now?: (() => number) | undefined;\n readonly setTimeoutFn?: typeof setTimeout | undefined;\n readonly clearTimeoutFn?: typeof clearTimeout | undefined;\n /** Yields between sampled transactions. Defaults to `setImmediate`. */\n readonly yieldFn?: (() => Promise<void>) | undefined;\n};\n\n/** Uses separate aggregates so SQLite can use an index for each value. */\nconst SQLITE_BOUNDS_SQL = /*sql*/ `\n SELECT\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\"\n`;\n\n/** Lists committed transaction watermarks in `(@after, @through]`. */\nconst SQLITE_COMMITS_SQL = /*sql*/ `\n SELECT \"watermark\" FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"precommit\" IS NOT NULL\n AND \"watermark\" > @after\n AND \"watermark\" <= @through\n ORDER BY \"watermark\"\n LIMIT @limit\n`;\n\ntype SQLiteBounds = {\n readonly minWatermark: string;\n readonly headWatermark: string;\n};\n\ntype PinnedBounds = {\n readonly meta: ChangeLogMeta;\n readonly sqlite: SQLiteBounds;\n readonly pgMinWatermark: string;\n readonly pgLastWatermark: string;\n};\n\ntype SampledComparison = {\n readonly outcome: CompareOutcome;\n readonly sqliteRowsRead: number;\n readonly pgRowsRead: number;\n readonly sqliteBytesRead: number;\n readonly pgBytesRead: number;\n};\n\n/**\n * Compares SQLite and Postgres catchup output without affecting subscribers.\n *\n * Each cycle compares complete ranges through the lower store head.\n * It checks every transaction for presence and samples payloads.\n * A bounds change makes the affected result `inconclusive`.\n * Postgres remains authoritative.\n */\nexport class SQLiteChangeLogComparator {\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeLogFile: string;\n readonly #identity: ChangeLogIdentity;\n readonly #pg: PGChangeLogRangeReader;\n readonly #opts: SQLiteChangeLogCompareOptions;\n readonly #now: () => number;\n readonly #setTimeoutFn: typeof setTimeout;\n readonly #clearTimeoutFn: typeof clearTimeout;\n readonly #yield: () => Promise<void>;\n\n readonly #compareResults = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.compare_result',\n 'Outcomes of the dark comparison between what SQLite catchup serves and ' +\n 'what PG catchup serves, per committed transaction. `inconclusive` ' +\n 'means a purge or reseed moved the pinned bounds mid-comparison.',\n );\n readonly #compareCycles = getOrCreateCounter(\n 'replica',\n 'sqlite_change_log.compare_cycles',\n 'SQLite change-log comparison cycles, by result.',\n );\n readonly #cycleDuration = getOrCreateLatencyHistogram(\n 'replica',\n 'sqlite_change_log.compare_cycle_duration',\n 'Duration of one SQLite change-log comparison cycle.',\n );\n\n /** Last handled commit watermark. A restart derives it from current bounds. */\n #cursor: string | undefined;\n #running: Promise<CompareCycleResult> | undefined;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #stopped = false;\n #continueWithoutDelay = false;\n #fileGeneration = 0;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n changeLogFile: string,\n identity: ChangeLogIdentity,\n pg: PGChangeLogRangeReader,\n opts: SQLiteChangeLogCompareOptions,\n ) {\n this.#lc = lc.withContext('component', 'sqlite-change-log-compare');\n this.#shard = shard;\n this.#changeLogFile = changeLogFile;\n this.#identity = identity;\n this.#pg = pg;\n this.#opts = opts;\n this.#now = opts.now ?? Date.now;\n this.#setTimeoutFn = opts.setTimeoutFn ?? setTimeout;\n this.#clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;\n this.#yield =\n opts.yieldFn ??\n (() => new Promise<void>(resolve => setImmediate(resolve)));\n this.#scheduleNext();\n }\n\n /**\n * Runs one cycle. Concurrent calls share the same promise.\n * Errors produce `skipped: 'error'`.\n */\n compareOnce(): Promise<CompareCycleResult> {\n if (this.#stopped) {\n return Promise.resolve({kind: 'skipped', reason: 'stopped'});\n }\n if (this.#running === undefined) {\n this.#running = this.#runCycle()\n .then(result => {\n if (this.#continueWithoutDelay) {\n this.#rescheduleNext(0);\n }\n return result;\n })\n .finally(() => {\n this.#running = undefined;\n });\n }\n return this.#running;\n }\n\n stop(): void {\n this.#stopped = true;\n if (this.#timer !== undefined) {\n this.#clearTimeoutFn(this.#timer);\n this.#timer = undefined;\n }\n }\n\n /** Invalidates cycles that can still read a replaced file. */\n invalidate(): void {\n this.#fileGeneration++;\n this.#cursor = undefined;\n this.#continueWithoutDelay = false;\n }\n\n #scheduleNext(\n delayMs = this.#opts.intervalMs ?? DEFAULT_COMPARE_INTERVAL_MS,\n ): void {\n if (this.#stopped || this.#timer !== undefined) {\n return;\n }\n this.#timer = this.#setTimeoutFn(() => {\n this.#timer = undefined;\n void this.compareOnce().finally(() => this.#scheduleNext());\n }, delayMs);\n }\n\n #rescheduleNext(delayMs: number): void {\n if (this.#timer !== undefined) {\n this.#clearTimeoutFn(this.#timer);\n this.#timer = undefined;\n }\n this.#scheduleNext(delayMs);\n }\n\n async #runCycle(): Promise<CompareCycleResult> {\n const startedAt = performance.now();\n const fileGeneration = this.#fileGeneration;\n this.#continueWithoutDelay = false;\n let db: Database | undefined;\n let reader: SQLiteChangeLogReader | undefined;\n try {\n try {\n // A readonly handle does not create a missing file.\n db = new Database(this.#lc, this.#changeLogFile, {readonly: true});\n } catch {\n return this.#skipped('log-unavailable');\n }\n\n const pinned = await this.#pinBounds(db);\n if (fileGeneration !== this.#fileGeneration) {\n return this.#skipped('log-rebuilt');\n }\n if (typeof pinned === 'string') {\n return this.#skipped(pinned);\n }\n const {meta} = pinned;\n\n const from = maxWatermark(\n this.#cursor ?? meta.seedWatermark,\n // Exclude the synthetic SQLite seed transaction.\n // The Postgres seed has no `precommit`, so enumeration also excludes it.\n meta.seedWatermark,\n // A store cannot serve the transaction at its minimum watermark.\n pinned.pgMinWatermark,\n pinned.sqlite.minWatermark,\n );\n const through = minWatermark(\n pinned.pgLastWatermark,\n pinned.sqlite.headWatermark,\n );\n if (through <= from) {\n return this.#skipped('nothing-to-compare');\n }\n\n let transactions = 0;\n let sampled = 0;\n const outcomes: Record<CompareOutcome, number> = {\n match: 0,\n mismatch: 0,\n inconclusive: 0,\n deferred: 0,\n oversized: 0,\n };\n const record = (watermark: string, outcome: CompareOutcome) => {\n this.#compareResults.add(1, {outcome});\n outcomes[outcome]++;\n if (outcome === 'mismatch') {\n // Log only the watermark because payloads contain customer data.\n this.#lc.error?.(\n 'SQLite change-log catchup output diverged from Postgres',\n {sqliteChangeLogCompare: {watermark}},\n );\n }\n };\n\n reader = new SQLiteChangeLogReader(this.#lc, this.#changeLogFile);\n const plan = reader.plan(from);\n if (plan.kind === 'not-ready') {\n return this.#skipped('log-unavailable');\n }\n if (plan.kind === 'ahead') {\n return this.#skipped('nothing-to-compare');\n }\n if (plan.kind === 'too-old') {\n // A fixed lower bound makes this result a mismatch.\n // A changed lower bound makes it inconclusive.\n const reconfirmed = await this.#reconfirm(db, pinned, from);\n const outcome =\n fileGeneration === this.#fileGeneration\n ? reconfirmed\n : 'inconclusive';\n record(from, outcome);\n if (outcome === 'inconclusive') {\n // Keep the cursor unchanged because the bounds moved.\n return this.#compared(from, from, transactions, sampled, outcomes);\n }\n // Positional reads can continue after a missing boundary.\n // Advancing avoids reporting the same boundary in every cycle.\n }\n\n const limit = Math.max(\n 1,\n this.#opts.maxTransactionsPerCycle ??\n DEFAULT_MAX_TRANSACTIONS_PER_CYCLE,\n );\n const union = await this.#enumerateCommits(db, from, through, limit);\n if (fileGeneration !== this.#fileGeneration) {\n return this.#skipped('log-rebuilt');\n }\n const maxRowsPerSource = Math.max(\n 1,\n this.#opts.maxRowsPerSourcePerCycle ??\n DEFAULT_MAX_ROWS_PER_SOURCE_PER_CYCLE,\n );\n const maxBytesPerSource = Math.max(\n 1,\n this.#opts.maxBytesPerSourcePerCycle ??\n DEFAULT_MAX_BYTES_PER_SOURCE_PER_CYCLE,\n );\n\n let sqliteRowsRead = 0;\n let pgRowsRead = 0;\n let sqliteBytesRead = 0;\n let pgBytesRead = 0;\n let previousBoundary = from;\n let handledThrough = from;\n for (const {watermark, inSqlite, inPg} of union) {\n if (this.#stopped) {\n break;\n }\n let stopAfterTransaction = false;\n if (!inSqlite || !inPg) {\n // Check every transaction for presence because this needs no payload read.\n const outcome = await this.#reconfirm(db, pinned, watermark);\n record(\n watermark,\n fileGeneration === this.#fileGeneration ? outcome : 'inconclusive',\n );\n } else if (\n isSampledForShard(this.#shard, watermark, this.#opts.comparePercent)\n ) {\n const sqliteRowsRemaining = maxRowsPerSource - sqliteRowsRead;\n const pgRowsRemaining = maxRowsPerSource - pgRowsRead;\n // A read may pass the byte budget by its last row, so this\n // compares against zero rather than checking for equality.\n const sqliteBytesRemaining = maxBytesPerSource - sqliteBytesRead;\n const pgBytesRemaining = maxBytesPerSource - pgBytesRead;\n if (\n sqliteRowsRemaining === 0 ||\n pgRowsRemaining === 0 ||\n sqliteBytesRemaining <= 0 ||\n pgBytesRemaining <= 0\n ) {\n record(watermark, 'deferred');\n break;\n }\n sampled++;\n const comparison = await this.#compareTransaction(\n db,\n reader,\n pinned,\n previousBoundary,\n watermark,\n sqliteRowsRemaining,\n pgRowsRemaining,\n sqliteBytesRemaining,\n pgBytesRemaining,\n maxRowsPerSource,\n maxBytesPerSource,\n );\n sqliteRowsRead += comparison.sqliteRowsRead;\n pgRowsRead += comparison.pgRowsRead;\n sqliteBytesRead += comparison.sqliteBytesRead;\n pgBytesRead += comparison.pgBytesRead;\n const outcome =\n fileGeneration === this.#fileGeneration\n ? comparison.outcome\n : 'inconclusive';\n record(watermark, outcome);\n if (outcome === 'inconclusive') {\n break;\n }\n if (outcome === 'deferred') {\n break;\n }\n if (outcome === 'oversized') {\n // Advance past a transaction that cannot fit in a fresh budget.\n stopAfterTransaction = true;\n }\n // Yield between sampled reads so other stream work can continue.\n await this.#yield();\n }\n previousBoundary = watermark;\n handledThrough = watermark;\n transactions++;\n if (stopAfterTransaction) {\n break;\n }\n }\n\n if (\n !this.#stopped &&\n fileGeneration === this.#fileGeneration &&\n outcomes.inconclusive === 0\n ) {\n this.#cursor = handledThrough;\n this.#continueWithoutDelay = handledThrough < through;\n }\n return this.#compared(\n from,\n handledThrough,\n transactions,\n sampled,\n outcomes,\n );\n } catch (e) {\n this.#lc.warn?.('error comparing the SQLite change log', e);\n return this.#skipped('error');\n } finally {\n reader?.close();\n db?.close();\n this.#cycleDuration.recordMs(performance.now() - startedAt);\n }\n }\n\n /** Checks log eligibility and reads the bounds from both stores. */\n async #pinBounds(db: Database): Promise<PinnedBounds | CompareSkipReason> {\n let meta: ChangeLogMeta;\n try {\n meta = readChangeLogMeta(db);\n } catch {\n // The writer has not initialized the log.\n return 'log-unavailable';\n }\n if (meta.schemaVersion !== CHANGE_LOG_DB_SCHEMA_VERSION) {\n return 'ineligible-schema';\n }\n const {epoch, generation, replicaID} = this.#identity;\n if (\n meta.epoch !== epoch ||\n meta.generation !== generation ||\n meta.replicaID !== replicaID\n ) {\n return 'ineligible-identity';\n }\n if (this.#now() - meta.seededAtMs < this.#opts.retentionMs) {\n return 'cold-log';\n }\n const sqlite = this.#readSQLiteBounds(db);\n if (sqlite === undefined) {\n return 'log-unavailable';\n }\n const {minWatermark, lastWatermark} = await this.#pg.getCatchupBounds();\n if (minWatermark === null) {\n // Only a new replica has an empty Postgres change log.\n return 'nothing-to-compare';\n }\n return {\n meta,\n sqlite,\n pgMinWatermark: minWatermark,\n pgLastWatermark: lastWatermark,\n };\n }\n\n #readSQLiteBounds(db: Database): SQLiteBounds | undefined {\n const bounds = db\n .prepare(SQLITE_BOUNDS_SQL)\n .get<{minWatermark: string | null; headWatermark: string | null}>();\n return bounds.minWatermark === null || bounds.headWatermark === null\n ? undefined\n : {\n minWatermark: bounds.minWatermark,\n headWatermark: bounds.headWatermark,\n };\n }\n\n /**\n * Merges committed transaction watermarks from both stores.\n * If a list reaches the limit, the result ends at the last complete common range.\n */\n async #enumerateCommits(\n db: Database,\n from: string,\n through: string,\n limit: number,\n ): Promise<{watermark: string; inSqlite: boolean; inPg: boolean}[]> {\n let sqlite: string[] = db\n .prepare(SQLITE_COMMITS_SQL)\n .all<{watermark: string}>({after: from, through, limit: limit + 1})\n .map(({watermark}) => watermark);\n let pg = await this.#pg.listCommitWatermarks(from, through, limit + 1);\n\n let effectiveThrough = through;\n if (sqlite.length > limit) {\n sqlite.length = limit;\n effectiveThrough = minWatermark(effectiveThrough, sqlite[limit - 1]);\n }\n if (pg.length > limit) {\n pg.length = limit;\n effectiveThrough = minWatermark(effectiveThrough, pg[limit - 1]);\n }\n if (effectiveThrough !== through) {\n sqlite = sqlite.filter(w => w <= effectiveThrough);\n pg = pg.filter(w => w <= effectiveThrough);\n }\n\n const union: {watermark: string; inSqlite: boolean; inPg: boolean}[] = [];\n let s = 0;\n let p = 0;\n while (s < sqlite.length || p < pg.length) {\n const sw = s < sqlite.length ? sqlite[s] : undefined;\n const pw = p < pg.length ? pg[p] : undefined;\n if (sw !== undefined && (pw === undefined || sw < pw)) {\n union.push({watermark: sw, inSqlite: true, inPg: false});\n s++;\n } else if (pw !== undefined && (sw === undefined || pw < sw)) {\n union.push({watermark: pw, inSqlite: false, inPg: true});\n p++;\n } else if (sw !== undefined) {\n union.push({watermark: sw, inSqlite: true, inPg: true});\n s++;\n p++;\n }\n }\n return union;\n }\n\n /**\n * Compares the output in `(previousBoundary, watermark]`.\n * Positional reads do not require either store to contain the boundary row.\n */\n async #compareTransaction(\n db: Database,\n reader: SQLiteChangeLogReader,\n pinned: PinnedBounds,\n previousBoundary: string,\n watermark: string,\n sqliteMaxRows: number,\n pgMaxRows: number,\n sqliteMaxBytes: number,\n pgMaxBytes: number,\n maxRowsPerSource: number,\n maxBytesPerSource: number,\n ): Promise<SampledComparison> {\n let sqlite: CatchupRangeDigest;\n let pg: CatchupRangeDigest;\n try {\n const readBatchRows = this.#opts.readBatchRows ?? DEFAULT_READ_BATCH_ROWS;\n sqlite = await digestCatchupRange(\n reader.read(\n previousBoundary,\n watermark,\n Math.min(readBatchRows, sqliteMaxRows),\n undefined,\n sqliteMaxRows,\n ),\n watermark,\n sqliteMaxRows,\n sqliteMaxBytes,\n );\n pg = await digestCatchupRange(\n reconstructBatches(\n this.#pg.readCatchupRange(previousBoundary, watermark, pgMaxRows),\n ),\n watermark,\n pgMaxRows,\n pgMaxBytes,\n );\n } catch (e) {\n // A failed catchup read makes the transaction suspect.\n this.#lc.warn?.(\n `error reading transaction ${watermark} for comparison`,\n e,\n );\n return {\n outcome: await this.#reconfirm(db, pinned, watermark),\n sqliteRowsRead: 0,\n pgRowsRead: 0,\n sqliteBytesRead: 0,\n pgBytesRead: 0,\n };\n }\n const read = {\n sqliteRowsRead: sqlite.rows,\n pgRowsRead: pg.rows,\n sqliteBytesRead: sqlite.bytes,\n pgBytesRead: pg.bytes,\n };\n if (sqlite.limitReached || pg.limitReached) {\n // Skip a transaction that cannot fit in a fresh budget.\n // Defer one that does not fit only in the remaining budget.\n // A budget is fresh when neither dimension has been spent.\n const sqliteFresh =\n sqliteMaxRows === maxRowsPerSource &&\n sqliteMaxBytes === maxBytesPerSource;\n const pgFresh =\n pgMaxRows === maxRowsPerSource && pgMaxBytes === maxBytesPerSource;\n const outcome =\n (sqlite.limitReached && sqliteFresh) || (pg.limitReached && pgFresh)\n ? 'oversized'\n : 'deferred';\n return {outcome, ...read};\n }\n if (sqlite.digest === pg.digest) {\n return {outcome: 'match', ...read};\n }\n return {\n outcome: await this.#reconfirm(db, pinned, watermark),\n ...read,\n };\n }\n\n /**\n * Rechecks a suspect result against current store bounds.\n * A moved bound, changed seed, failed read, or lower head makes the result inconclusive.\n */\n async #reconfirm(\n db: Database,\n pinned: PinnedBounds,\n watermark: string,\n ): Promise<'mismatch' | 'inconclusive'> {\n try {\n const meta = readChangeLogMeta(db);\n if (\n meta.seedWatermark !== pinned.meta.seedWatermark ||\n meta.seededAtMs !== pinned.meta.seededAtMs\n ) {\n return 'inconclusive';\n }\n const sqlite = this.#readSQLiteBounds(db);\n const {minWatermark, lastWatermark} = await this.#pg.getCatchupBounds();\n if (sqlite === undefined || minWatermark === null) {\n return 'inconclusive';\n }\n if (\n (sqlite.minWatermark > pinned.sqlite.minWatermark &&\n watermark <= sqlite.minWatermark) ||\n (minWatermark > pinned.pgMinWatermark && watermark <= minWatermark) ||\n watermark > sqlite.headWatermark ||\n watermark > lastWatermark\n ) {\n return 'inconclusive';\n }\n return 'mismatch';\n } catch {\n return 'inconclusive';\n }\n }\n\n #skipped(reason: CompareSkipReason): CompareCycleResult {\n this.#compareCycles.add(1, {result: reason});\n if (reason === 'error') {\n // The caller logged the error.\n } else if (reason !== 'nothing-to-compare') {\n this.#lc.debug?.(`skipping SQLite change-log comparison: ${reason}`);\n }\n return {kind: 'skipped', reason};\n }\n\n #compared(\n fromWatermark: string,\n throughWatermark: string,\n transactions: number,\n sampled: number,\n outcomes: Record<CompareOutcome, number>,\n ): CompareCycleResult {\n this.#compareCycles.add(1, {result: 'compared'});\n const result = {\n kind: 'compared',\n fromWatermark,\n throughWatermark,\n transactions,\n sampled,\n matched: outcomes.match,\n mismatched: outcomes.mismatch,\n inconclusive: outcomes.inconclusive,\n deferred: outcomes.deferred,\n oversized: outcomes.oversized,\n } as const;\n this.#lc.debug?.('SQLite change-log comparison cycle', {\n sqliteChangeLogCompare: result,\n });\n return result;\n }\n}\n\n/** Applies the same reconstruction as Postgres catchup. */\nasync function* reconstructBatches(\n batches: AsyncIterable<ChangeLogEntry[]>,\n): AsyncIterable<WatermarkedChange[]> {\n for await (const batch of batches) {\n yield batch.map(reconstructWatermarkedChange);\n }\n}\n\nfunction minWatermark(a: string, b: string): string {\n return a < b ? a : b;\n}\n\nfunction maxWatermark(...watermarks: string[]): string {\n let max = watermarks[0];\n for (const w of watermarks) {\n if (w > max) {\n max = w;\n }\n }\n return max;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import type { ShardID } from '../../types/shards.ts';
|
|
3
|
+
import { type ChangeLogIdentity } from '../replicator/change-log-db.ts';
|
|
4
|
+
export type ChangeLogReadSource = 'pg' | 'sqlite';
|
|
5
|
+
export type SQLiteChangeLogCoverage = {
|
|
6
|
+
readonly seededAtMs: number;
|
|
7
|
+
readonly seedWatermark: string;
|
|
8
|
+
readonly minWatermark: string;
|
|
9
|
+
readonly headWatermark: string;
|
|
10
|
+
};
|
|
11
|
+
export type ChangeLogReadRouteReason = 'selected' | 'selected-cold' | 'percentage' | 'cold-log' | 'log-unavailable' | 'breaker-open' | 'backup-uncovered';
|
|
12
|
+
export type ChangeLogReadRoute = {
|
|
13
|
+
readonly source: ChangeLogReadSource;
|
|
14
|
+
readonly reason: ChangeLogReadRouteReason;
|
|
15
|
+
readonly pinned?: boolean | undefined;
|
|
16
|
+
readonly coverage?: SQLiteChangeLogCoverage | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type SQLiteChangeLogReadRouterOptions = {
|
|
19
|
+
readonly shard: ShardID;
|
|
20
|
+
readonly readPercent: number;
|
|
21
|
+
/**
|
|
22
|
+
* Percentage of eligible tasks served from a log that has not yet aged
|
|
23
|
+
* through {@link retentionMs}. Defaults to zero, which keeps every such
|
|
24
|
+
* task on PG.
|
|
25
|
+
*/
|
|
26
|
+
readonly coldReadPercent?: number | undefined;
|
|
27
|
+
readonly retentionMs: number;
|
|
28
|
+
readonly inspect: () => SQLiteChangeLogCoverage | undefined;
|
|
29
|
+
readonly failureCooldownMs?: number | undefined;
|
|
30
|
+
readonly now?: (() => number) | undefined;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Selects and pins the catchup source shared by `/snapshot` and `/changes`.
|
|
34
|
+
*
|
|
35
|
+
* A post-registration SQLite failure opens a process-local breaker. Requests
|
|
36
|
+
* use PG during the cooldown; the first request after it expires is the
|
|
37
|
+
* bounded probe. A successful readiness inspection closes the breaker, while
|
|
38
|
+
* another unavailable result rearms it. A writer failure opens it permanently
|
|
39
|
+
* because the writer remains disabled and its file stays absent for the life
|
|
40
|
+
* of the process.
|
|
41
|
+
*/
|
|
42
|
+
export declare class SQLiteChangeLogReadRouter {
|
|
43
|
+
#private;
|
|
44
|
+
constructor(opts: SQLiteChangeLogReadRouterOptions);
|
|
45
|
+
/** Chooses and pins a source for a snapshot reservation. */
|
|
46
|
+
pin(taskID: string): ChangeLogReadRoute;
|
|
47
|
+
/** Returns the source pinned by an active snapshot reservation. */
|
|
48
|
+
peek(taskID: string): ChangeLogReadRoute | undefined;
|
|
49
|
+
/** Consumes a snapshot pin, or makes the stable choice for a direct retry. */
|
|
50
|
+
consume(taskID: string): ChangeLogReadRoute;
|
|
51
|
+
/** Releases a pin whose snapshot reservation ended before subscribing. */
|
|
52
|
+
release(taskID: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Replaces a pinned SQLite route with PG, for a reservation whose backup
|
|
55
|
+
* the log cannot cover -- most often a log seeded after that backup.
|
|
56
|
+
*
|
|
57
|
+
* The pin moves, not just the reservation's advertised bounds. Confirming
|
|
58
|
+
* with PG bounds while the task's `/changes` request still resolves to
|
|
59
|
+
* SQLite is exactly the mismatch the reservation path exists to prevent,
|
|
60
|
+
* so the two have to change together.
|
|
61
|
+
*
|
|
62
|
+
* A task with no pin is left alone: `consume` makes it a fresh choice.
|
|
63
|
+
*/
|
|
64
|
+
demote(taskID: string): ChangeLogReadRoute;
|
|
65
|
+
/** Opens the post-registration failure breaker. */
|
|
66
|
+
trip(permanent?: boolean): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reads the coverage and validates the v2 schema and RMv2 identity in one
|
|
70
|
+
* short-lived readonly connection. It never creates the log.
|
|
71
|
+
*/
|
|
72
|
+
export declare function inspectSQLiteChangeLog(lc: LogContext, changeLogFile: string, identity: ChangeLogIdentity): SQLiteChangeLogCoverage | undefined;
|
|
73
|
+
//# sourceMappingURL=sqlite-change-log-read-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-read-router.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-read-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,gCAAgC,CAAC;AAGxC,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,QAAQ,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAChC,UAAU,GAIV,eAAe,GACf,YAAY,GACZ,UAAU,GACV,iBAAiB,GACjB,cAAc,GAId,kBAAkB,CAAC;AAEvB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;CACzD,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,uBAAuB,GAAG,SAAS,CAAC;IAC5D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3C,CAAC;AAIF;;;;;;;;;GASG;AACH,qBAAa,yBAAyB;;gBAaxB,IAAI,EAAE,gCAAgC;IAWlD,4DAA4D;IAC5D,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB;IAMvC,mEAAmE;IACnE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAKpD,8EAA8E;IAC9E,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB;IAS3C,0EAA0E;IAC1E,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI7B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB;IAW1C,mDAAmD;IACnD,IAAI,CAAC,SAAS,UAAQ,GAAG,IAAI;CAsE9B;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,iBAAiB,GAC1B,uBAAuB,GAAG,SAAS,CAgCrC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Database } from "../../../../zqlite/src/db.js";
|
|
2
|
+
import { readChangeLogBounds, readChangeLogMeta } from "../replicator/change-log-db.js";
|
|
3
|
+
import { isSampledForShard } from "./shard-sampling.js";
|
|
4
|
+
//#region ../zero-cache/src/services/change-streamer/sqlite-change-log-read-router.ts
|
|
5
|
+
var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
|
|
6
|
+
/**
|
|
7
|
+
* Selects and pins the catchup source shared by `/snapshot` and `/changes`.
|
|
8
|
+
*
|
|
9
|
+
* A post-registration SQLite failure opens a process-local breaker. Requests
|
|
10
|
+
* use PG during the cooldown; the first request after it expires is the
|
|
11
|
+
* bounded probe. A successful readiness inspection closes the breaker, while
|
|
12
|
+
* another unavailable result rearms it. A writer failure opens it permanently
|
|
13
|
+
* because the writer remains disabled and its file stays absent for the life
|
|
14
|
+
* of the process.
|
|
15
|
+
*/
|
|
16
|
+
var SQLiteChangeLogReadRouter = class {
|
|
17
|
+
#shard;
|
|
18
|
+
#readPercent;
|
|
19
|
+
#coldReadPercent;
|
|
20
|
+
#retentionMs;
|
|
21
|
+
#inspect;
|
|
22
|
+
#failureCooldownMs;
|
|
23
|
+
#now;
|
|
24
|
+
#pins = /* @__PURE__ */ new Map();
|
|
25
|
+
#breakerUntilMs;
|
|
26
|
+
#permanentlyDisabled = false;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.#shard = opts.shard;
|
|
29
|
+
this.#readPercent = opts.readPercent;
|
|
30
|
+
this.#coldReadPercent = opts.coldReadPercent ?? 0;
|
|
31
|
+
this.#retentionMs = opts.retentionMs;
|
|
32
|
+
this.#inspect = opts.inspect;
|
|
33
|
+
this.#failureCooldownMs = opts.failureCooldownMs ?? DEFAULT_FAILURE_COOLDOWN_MS;
|
|
34
|
+
this.#now = opts.now ?? Date.now;
|
|
35
|
+
}
|
|
36
|
+
/** Chooses and pins a source for a snapshot reservation. */
|
|
37
|
+
pin(taskID) {
|
|
38
|
+
const route = this.#choose(taskID);
|
|
39
|
+
this.#pins.set(taskID, route);
|
|
40
|
+
return route;
|
|
41
|
+
}
|
|
42
|
+
/** Returns the source pinned by an active snapshot reservation. */
|
|
43
|
+
peek(taskID) {
|
|
44
|
+
const route = this.#pins.get(taskID);
|
|
45
|
+
return route && this.#asPinned(route);
|
|
46
|
+
}
|
|
47
|
+
/** Consumes a snapshot pin, or makes the stable choice for a direct retry. */
|
|
48
|
+
consume(taskID) {
|
|
49
|
+
const route = this.#pins.get(taskID);
|
|
50
|
+
if (route === void 0) return this.#choose(taskID);
|
|
51
|
+
this.#pins.delete(taskID);
|
|
52
|
+
return this.#asPinned(route);
|
|
53
|
+
}
|
|
54
|
+
/** Releases a pin whose snapshot reservation ended before subscribing. */
|
|
55
|
+
release(taskID) {
|
|
56
|
+
this.#pins.delete(taskID);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Replaces a pinned SQLite route with PG, for a reservation whose backup
|
|
60
|
+
* the log cannot cover -- most often a log seeded after that backup.
|
|
61
|
+
*
|
|
62
|
+
* The pin moves, not just the reservation's advertised bounds. Confirming
|
|
63
|
+
* with PG bounds while the task's `/changes` request still resolves to
|
|
64
|
+
* SQLite is exactly the mismatch the reservation path exists to prevent,
|
|
65
|
+
* so the two have to change together.
|
|
66
|
+
*
|
|
67
|
+
* A task with no pin is left alone: `consume` makes it a fresh choice.
|
|
68
|
+
*/
|
|
69
|
+
demote(taskID) {
|
|
70
|
+
const route = {
|
|
71
|
+
source: "pg",
|
|
72
|
+
reason: "backup-uncovered"
|
|
73
|
+
};
|
|
74
|
+
if (this.#pins.has(taskID)) this.#pins.set(taskID, route);
|
|
75
|
+
return route;
|
|
76
|
+
}
|
|
77
|
+
/** Opens the post-registration failure breaker. */
|
|
78
|
+
trip(permanent = false) {
|
|
79
|
+
if (permanent) {
|
|
80
|
+
this.#permanentlyDisabled = true;
|
|
81
|
+
this.#breakerUntilMs = void 0;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!this.#permanentlyDisabled) this.#breakerUntilMs = this.#now() + this.#failureCooldownMs;
|
|
85
|
+
}
|
|
86
|
+
#choose(taskID) {
|
|
87
|
+
const now = this.#now();
|
|
88
|
+
if (this.#permanentlyDisabled || this.#breakerUntilMs !== void 0 && now < this.#breakerUntilMs) return {
|
|
89
|
+
source: "pg",
|
|
90
|
+
reason: "breaker-open"
|
|
91
|
+
};
|
|
92
|
+
let coverage;
|
|
93
|
+
try {
|
|
94
|
+
coverage = this.#inspect();
|
|
95
|
+
} catch {
|
|
96
|
+
coverage = void 0;
|
|
97
|
+
}
|
|
98
|
+
if (coverage === void 0) {
|
|
99
|
+
if (this.#breakerUntilMs !== void 0) this.#breakerUntilMs = now + this.#failureCooldownMs;
|
|
100
|
+
return {
|
|
101
|
+
source: "pg",
|
|
102
|
+
reason: "log-unavailable"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
this.#breakerUntilMs = void 0;
|
|
106
|
+
const cold = now - coverage.seededAtMs < this.#retentionMs;
|
|
107
|
+
if (cold && !isSampledForShard(this.#shard, taskID, this.#coldReadPercent)) return {
|
|
108
|
+
source: "pg",
|
|
109
|
+
reason: "cold-log",
|
|
110
|
+
coverage
|
|
111
|
+
};
|
|
112
|
+
if (!isSampledForShard(this.#shard, taskID, this.#readPercent)) return {
|
|
113
|
+
source: "pg",
|
|
114
|
+
reason: "percentage",
|
|
115
|
+
coverage
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
source: "sqlite",
|
|
119
|
+
reason: cold ? "selected-cold" : "selected",
|
|
120
|
+
coverage
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
#asPinned(route) {
|
|
124
|
+
return {
|
|
125
|
+
...route,
|
|
126
|
+
pinned: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Reads the coverage and validates the v2 schema and RMv2 identity in one
|
|
132
|
+
* short-lived readonly connection. It never creates the log.
|
|
133
|
+
*/
|
|
134
|
+
function inspectSQLiteChangeLog(lc, changeLogFile, identity) {
|
|
135
|
+
let db;
|
|
136
|
+
try {
|
|
137
|
+
db = new Database(lc.withContext("component", "sqlite-change-log-read-router"), changeLogFile, { readonly: true });
|
|
138
|
+
const meta = readChangeLogMeta(db);
|
|
139
|
+
if (meta.schemaVersion !== 4 || meta.epoch !== identity.epoch || meta.generation !== identity.generation || meta.replicaID !== identity.replicaID) return;
|
|
140
|
+
const bounds = readChangeLogBounds(db);
|
|
141
|
+
if (bounds.minWatermark === null || bounds.headWatermark === null) return;
|
|
142
|
+
return {
|
|
143
|
+
seededAtMs: meta.seededAtMs,
|
|
144
|
+
seedWatermark: meta.seedWatermark,
|
|
145
|
+
minWatermark: bounds.minWatermark,
|
|
146
|
+
headWatermark: bounds.headWatermark
|
|
147
|
+
};
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
} finally {
|
|
151
|
+
db?.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { SQLiteChangeLogReadRouter, inspectSQLiteChangeLog };
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=sqlite-change-log-read-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-read-router.js","names":["#shard","#readPercent","#coldReadPercent","#retentionMs","#inspect","#failureCooldownMs","#now","#pins","#choose","#asPinned","#permanentlyDisabled","#breakerUntilMs"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-read-router.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport {\n CHANGE_LOG_DB_SCHEMA_VERSION,\n readChangeLogBounds,\n readChangeLogMeta,\n type ChangeLogIdentity,\n} from '../replicator/change-log-db.ts';\nimport {isSampledForShard} from './shard-sampling.ts';\n\nexport type ChangeLogReadSource = 'pg' | 'sqlite';\n\nexport type SQLiteChangeLogCoverage = {\n readonly seededAtMs: number;\n readonly seedWatermark: string;\n readonly minWatermark: string;\n readonly headWatermark: string;\n};\n\nexport type ChangeLogReadRouteReason =\n | 'selected'\n // Selected while the log was still inside its warm-up window. Kept\n // distinct from 'selected' so bootstrap serving is separable in metrics\n // from steady-state serving.\n | 'selected-cold'\n | 'percentage'\n | 'cold-log'\n | 'log-unavailable'\n | 'breaker-open'\n // Pinned to SQLite, then demoted because the log could not cover the\n // backup the reservation is restoring from. See {@link\n // SQLiteChangeLogReadRouter.demote}.\n | 'backup-uncovered';\n\nexport type ChangeLogReadRoute = {\n readonly source: ChangeLogReadSource;\n readonly reason: ChangeLogReadRouteReason;\n readonly pinned?: boolean | undefined;\n readonly coverage?: SQLiteChangeLogCoverage | undefined;\n};\n\nexport type SQLiteChangeLogReadRouterOptions = {\n readonly shard: ShardID;\n readonly readPercent: number;\n /**\n * Percentage of eligible tasks served from a log that has not yet aged\n * through {@link retentionMs}. Defaults to zero, which keeps every such\n * task on PG.\n */\n readonly coldReadPercent?: number | undefined;\n readonly retentionMs: number;\n readonly inspect: () => SQLiteChangeLogCoverage | undefined;\n readonly failureCooldownMs?: number | undefined;\n readonly now?: (() => number) | undefined;\n};\n\nconst DEFAULT_FAILURE_COOLDOWN_MS = 30_000;\n\n/**\n * Selects and pins the catchup source shared by `/snapshot` and `/changes`.\n *\n * A post-registration SQLite failure opens a process-local breaker. Requests\n * use PG during the cooldown; the first request after it expires is the\n * bounded probe. A successful readiness inspection closes the breaker, while\n * another unavailable result rearms it. A writer failure opens it permanently\n * because the writer remains disabled and its file stays absent for the life\n * of the process.\n */\nexport class SQLiteChangeLogReadRouter {\n readonly #shard: ShardID;\n readonly #readPercent: number;\n readonly #coldReadPercent: number;\n readonly #retentionMs: number;\n readonly #inspect: () => SQLiteChangeLogCoverage | undefined;\n readonly #failureCooldownMs: number;\n readonly #now: () => number;\n readonly #pins = new Map<string, ChangeLogReadRoute>();\n\n #breakerUntilMs: number | undefined;\n #permanentlyDisabled = false;\n\n constructor(opts: SQLiteChangeLogReadRouterOptions) {\n this.#shard = opts.shard;\n this.#readPercent = opts.readPercent;\n this.#coldReadPercent = opts.coldReadPercent ?? 0;\n this.#retentionMs = opts.retentionMs;\n this.#inspect = opts.inspect;\n this.#failureCooldownMs =\n opts.failureCooldownMs ?? DEFAULT_FAILURE_COOLDOWN_MS;\n this.#now = opts.now ?? Date.now;\n }\n\n /** Chooses and pins a source for a snapshot reservation. */\n pin(taskID: string): ChangeLogReadRoute {\n const route = this.#choose(taskID);\n this.#pins.set(taskID, route);\n return route;\n }\n\n /** Returns the source pinned by an active snapshot reservation. */\n peek(taskID: string): ChangeLogReadRoute | undefined {\n const route = this.#pins.get(taskID);\n return route && this.#asPinned(route);\n }\n\n /** Consumes a snapshot pin, or makes the stable choice for a direct retry. */\n consume(taskID: string): ChangeLogReadRoute {\n const route = this.#pins.get(taskID);\n if (route === undefined) {\n return this.#choose(taskID);\n }\n this.#pins.delete(taskID);\n return this.#asPinned(route);\n }\n\n /** Releases a pin whose snapshot reservation ended before subscribing. */\n release(taskID: string): void {\n this.#pins.delete(taskID);\n }\n\n /**\n * Replaces a pinned SQLite route with PG, for a reservation whose backup\n * the log cannot cover -- most often a log seeded after that backup.\n *\n * The pin moves, not just the reservation's advertised bounds. Confirming\n * with PG bounds while the task's `/changes` request still resolves to\n * SQLite is exactly the mismatch the reservation path exists to prevent,\n * so the two have to change together.\n *\n * A task with no pin is left alone: `consume` makes it a fresh choice.\n */\n demote(taskID: string): ChangeLogReadRoute {\n const route: ChangeLogReadRoute = {\n source: 'pg',\n reason: 'backup-uncovered',\n };\n if (this.#pins.has(taskID)) {\n this.#pins.set(taskID, route);\n }\n return route;\n }\n\n /** Opens the post-registration failure breaker. */\n trip(permanent = false): void {\n if (permanent) {\n this.#permanentlyDisabled = true;\n this.#breakerUntilMs = undefined;\n return;\n }\n if (!this.#permanentlyDisabled) {\n this.#breakerUntilMs = this.#now() + this.#failureCooldownMs;\n }\n }\n\n #choose(taskID: string): ChangeLogReadRoute {\n const now = this.#now();\n if (\n this.#permanentlyDisabled ||\n (this.#breakerUntilMs !== undefined && now < this.#breakerUntilMs)\n ) {\n return {source: 'pg', reason: 'breaker-open'};\n }\n\n // Inspect eligibility even at readPercent=0. Slice 11 starts its rollout\n // at zero specifically so warm/cold/unavailable rates are visible before\n // any subscriber is routed to SQLite.\n let coverage: SQLiteChangeLogCoverage | undefined;\n try {\n coverage = this.#inspect();\n } catch {\n coverage = undefined;\n }\n if (coverage === undefined) {\n // This was a breaker probe rather than ordinary startup unavailability.\n // Rearm it so a busy retry loop does not turn into an open/read loop.\n if (this.#breakerUntilMs !== undefined) {\n this.#breakerUntilMs = now + this.#failureCooldownMs;\n }\n return {source: 'pg', reason: 'log-unavailable'};\n }\n // A successful probe restores eligibility. This happens before the gates\n // below so that declining a task -- for a cold log or for the percentage\n // -- does not leave the cooldown armed against the next ordinary\n // unavailability.\n this.#breakerUntilMs = undefined;\n // A log seeded less than a retention window ago holds less history than\n // the purger promises to keep, so it is the case that most often cannot\n // cover a follower. It is also the only path that has no PG to fall back\n // to once PG is retired, which is the argument for exercising it now\n // rather than meeting it at the cutover: `coldReadPercent` is that dial.\n const cold = now - coverage.seededAtMs < this.#retentionMs;\n if (\n cold &&\n !isSampledForShard(this.#shard, taskID, this.#coldReadPercent)\n ) {\n return {source: 'pg', reason: 'cold-log', coverage};\n }\n\n // Keeping the percentage decision after readiness makes zero-percent\n // rollouts emit eligibility metrics without serving reads.\n if (!isSampledForShard(this.#shard, taskID, this.#readPercent)) {\n return {source: 'pg', reason: 'percentage', coverage};\n }\n return {\n source: 'sqlite',\n reason: cold ? 'selected-cold' : 'selected',\n coverage,\n };\n }\n\n #asPinned(route: ChangeLogReadRoute): ChangeLogReadRoute {\n return {...route, pinned: true};\n }\n}\n\n/**\n * Reads the coverage and validates the v2 schema and RMv2 identity in one\n * short-lived readonly connection. It never creates the log.\n */\nexport function inspectSQLiteChangeLog(\n lc: LogContext,\n changeLogFile: string,\n identity: ChangeLogIdentity,\n): SQLiteChangeLogCoverage | undefined {\n let db: Database | undefined;\n try {\n db = new Database(\n lc.withContext('component', 'sqlite-change-log-read-router'),\n changeLogFile,\n {readonly: true},\n );\n const meta = readChangeLogMeta(db);\n if (\n meta.schemaVersion !== CHANGE_LOG_DB_SCHEMA_VERSION ||\n meta.epoch !== identity.epoch ||\n meta.generation !== identity.generation ||\n meta.replicaID !== identity.replicaID\n ) {\n return undefined;\n }\n const bounds = readChangeLogBounds(db);\n if (bounds.minWatermark === null || bounds.headWatermark === null) {\n return undefined;\n }\n return {\n seededAtMs: meta.seededAtMs,\n seedWatermark: meta.seedWatermark,\n minWatermark: bounds.minWatermark,\n headWatermark: bounds.headWatermark,\n };\n } catch {\n return undefined;\n } finally {\n db?.close();\n }\n}\n"],"mappings":";;;;AAyDA,IAAM,8BAA8B;;;;;;;;;;;AAYpC,IAAa,4BAAb,MAAuC;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA,wBAAiB,IAAI,IAAgC;CAErD;CACA,uBAAuB;CAEvB,YAAY,MAAwC;EAClD,KAAKA,SAAS,KAAK;EACnB,KAAKC,eAAe,KAAK;EACzB,KAAKC,mBAAmB,KAAK,mBAAmB;EAChD,KAAKC,eAAe,KAAK;EACzB,KAAKC,WAAW,KAAK;EACrB,KAAKC,qBACH,KAAK,qBAAqB;EAC5B,KAAKC,OAAO,KAAK,OAAO,KAAK;CAC/B;;CAGA,IAAI,QAAoC;EACtC,MAAM,QAAQ,KAAKE,QAAQ,MAAM;EACjC,KAAKD,MAAM,IAAI,QAAQ,KAAK;EAC5B,OAAO;CACT;;CAGA,KAAK,QAAgD;EACnD,MAAM,QAAQ,KAAKA,MAAM,IAAI,MAAM;EACnC,OAAO,SAAS,KAAKE,UAAU,KAAK;CACtC;;CAGA,QAAQ,QAAoC;EAC1C,MAAM,QAAQ,KAAKF,MAAM,IAAI,MAAM;EACnC,IAAI,UAAU,KAAA,GACZ,OAAO,KAAKC,QAAQ,MAAM;EAE5B,KAAKD,MAAM,OAAO,MAAM;EACxB,OAAO,KAAKE,UAAU,KAAK;CAC7B;;CAGA,QAAQ,QAAsB;EAC5B,KAAKF,MAAM,OAAO,MAAM;CAC1B;;;;;;;;;;;;CAaA,OAAO,QAAoC;EACzC,MAAM,QAA4B;GAChC,QAAQ;GACR,QAAQ;EACV;EACA,IAAI,KAAKA,MAAM,IAAI,MAAM,GACvB,KAAKA,MAAM,IAAI,QAAQ,KAAK;EAE9B,OAAO;CACT;;CAGA,KAAK,YAAY,OAAa;EAC5B,IAAI,WAAW;GACb,KAAKG,uBAAuB;GAC5B,KAAKC,kBAAkB,KAAA;GACvB;EACF;EACA,IAAI,CAAC,KAAKD,sBACR,KAAKC,kBAAkB,KAAKL,KAAK,IAAI,KAAKD;CAE9C;CAEA,QAAQ,QAAoC;EAC1C,MAAM,MAAM,KAAKC,KAAK;EACtB,IACE,KAAKI,wBACJ,KAAKC,oBAAoB,KAAA,KAAa,MAAM,KAAKA,iBAElD,OAAO;GAAC,QAAQ;GAAM,QAAQ;EAAc;EAM9C,IAAI;EACJ,IAAI;GACF,WAAW,KAAKP,SAAS;EAC3B,QAAQ;GACN,WAAW,KAAA;EACb;EACA,IAAI,aAAa,KAAA,GAAW;GAG1B,IAAI,KAAKO,oBAAoB,KAAA,GAC3B,KAAKA,kBAAkB,MAAM,KAAKN;GAEpC,OAAO;IAAC,QAAQ;IAAM,QAAQ;GAAiB;EACjD;EAKA,KAAKM,kBAAkB,KAAA;EAMvB,MAAM,OAAO,MAAM,SAAS,aAAa,KAAKR;EAC9C,IACE,QACA,CAAC,kBAAkB,KAAKH,QAAQ,QAAQ,KAAKE,gBAAgB,GAE7D,OAAO;GAAC,QAAQ;GAAM,QAAQ;GAAY;EAAQ;EAKpD,IAAI,CAAC,kBAAkB,KAAKF,QAAQ,QAAQ,KAAKC,YAAY,GAC3D,OAAO;GAAC,QAAQ;GAAM,QAAQ;GAAc;EAAQ;EAEtD,OAAO;GACL,QAAQ;GACR,QAAQ,OAAO,kBAAkB;GACjC;EACF;CACF;CAEA,UAAU,OAA+C;EACvD,OAAO;GAAC,GAAG;GAAO,QAAQ;EAAI;CAChC;AACF;;;;;AAMA,SAAgB,uBACd,IACA,eACA,UACqC;CACrC,IAAI;CACJ,IAAI;EACF,KAAK,IAAI,SACP,GAAG,YAAY,aAAa,+BAA+B,GAC3D,eACA,EAAC,UAAU,KAAI,CACjB;EACA,MAAM,OAAO,kBAAkB,EAAE;EACjC,IACE,KAAK,kBAAA,KACL,KAAK,UAAU,SAAS,SACxB,KAAK,eAAe,SAAS,cAC7B,KAAK,cAAc,SAAS,WAE5B;EAEF,MAAM,SAAS,oBAAoB,EAAE;EACrC,IAAI,OAAO,iBAAiB,QAAQ,OAAO,kBAAkB,MAC3D;EAEF,OAAO;GACL,YAAY,KAAK;GACjB,eAAe,KAAK;GACpB,cAAc,OAAO;GACrB,eAAe,OAAO;EACxB;CACF,QAAQ;EACN;CACF,UAAU;EACR,IAAI,MAAM;CACZ;AACF"}
|
|
@@ -55,7 +55,7 @@ export declare class SQLiteChangeLogReader implements Disposable {
|
|
|
55
55
|
#private;
|
|
56
56
|
constructor(lc: LogContext, changeLogFile: string);
|
|
57
57
|
plan(fromWatermark: string): CatchupPlan;
|
|
58
|
-
read(fromWatermark: string, throughWatermark: string, batchSize: number, signal?: AbortSignal | undefined): AsyncIterable<readonly WatermarkedChange[]>;
|
|
58
|
+
read(fromWatermark: string, throughWatermark: string, batchSize: number, signal?: AbortSignal | undefined, maxRows?: number | undefined): AsyncIterable<readonly WatermarkedChange[]>;
|
|
59
59
|
close(): void;
|
|
60
60
|
[Symbol.dispose](): void;
|
|
61
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-change-log-reader.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAC,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GAKD;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,CAAC;AAiBjC;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,uNAQ1C,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,ucAOtC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,gCAAgC,wOAW5C,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,UAAU;;gBAK1C,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAQjD,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW;IAqBjC,IAAI,CACT,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-reader.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,sBAAsB,CAAC;AAE5D,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAC,GACxD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GAKD;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,CAAC;AAiBjC;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,uNAQ1C,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,ucAOtC,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,gCAAgC,wOAW5C,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,qBAAsB,YAAW,UAAU;;gBAK1C,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAQjD,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,WAAW;IAqBjC,IAAI,CACT,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,aAAa,CAAC,SAAS,iBAAiB,EAAE,CAAC;IA6E9C,KAAK,IAAI,IAAI;IAQb,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAuCzB"}
|
|
@@ -99,17 +99,21 @@ var SQLiteChangeLogReader = class {
|
|
|
99
99
|
headWatermark
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
async *read(fromWatermark, throughWatermark, batchSize, signal) {
|
|
102
|
+
async *read(fromWatermark, throughWatermark, batchSize, signal, maxRows) {
|
|
103
103
|
assert(Number.isSafeInteger(batchSize) && batchSize > 0, "SQLite change log batch size must be a positive safe integer");
|
|
104
|
+
assert(maxRows === void 0 || Number.isSafeInteger(maxRows) && maxRows > 0, "SQLite change log row limit must be a positive safe integer");
|
|
104
105
|
this.#assertOpen();
|
|
105
106
|
const statements = this.#prepare();
|
|
106
107
|
assert(statements !== void 0, "SQLite change log must be ready before it is read");
|
|
107
108
|
let lastWatermark = fromWatermark;
|
|
108
109
|
let lastPos = Number.MAX_SAFE_INTEGER;
|
|
109
110
|
let lastTag;
|
|
111
|
+
let rowsRead = 0;
|
|
110
112
|
while (true) {
|
|
111
113
|
this.#throwIfAborted(signal);
|
|
112
|
-
const
|
|
114
|
+
const remainingRows = maxRows === void 0 ? batchSize : maxRows - rowsRead;
|
|
115
|
+
if (remainingRows === 0) break;
|
|
116
|
+
const rows = statements.readBatch.all(lastWatermark, lastPos, throughWatermark, Math.min(batchSize, remainingRows));
|
|
113
117
|
this.#throwIfAborted(signal);
|
|
114
118
|
if (rows.length === 0) break;
|
|
115
119
|
const changes = rows.map(reconstructWatermarkedChange);
|
|
@@ -118,9 +122,10 @@ var SQLiteChangeLogReader = class {
|
|
|
118
122
|
lastWatermark = last.watermark;
|
|
119
123
|
lastPos = last.pos;
|
|
120
124
|
lastTag = last.tag;
|
|
125
|
+
rowsRead += rows.length;
|
|
121
126
|
yield changes;
|
|
122
127
|
}
|
|
123
|
-
if (fromWatermark !== throughWatermark) assert(lastWatermark === throughWatermark && lastTag === "commit", () => `SQLite change log did not end at pinned head ${throughWatermark}`);
|
|
128
|
+
if (fromWatermark !== throughWatermark && (maxRows === void 0 || rowsRead < maxRows)) assert(lastWatermark === throughWatermark && lastTag === "commit", () => `SQLite change log did not end at pinned head ${throughWatermark}`);
|
|
124
129
|
}
|
|
125
130
|
close() {
|
|
126
131
|
if (this.#closed) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-change-log-reader.js","names":["#db","#assertOpen","#prepare","#throwIfAborted","#closed","#statements","#streamTableExists"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport {CHANGE_LOG_STREAM_TABLE} from '../replicator/change-log-db.ts';\nimport {\n reconstructWatermarkedChange,\n type ChangeLogEntry,\n} from './change-log-codec.ts';\nimport type {WatermarkedChange} from './change-streamer.ts';\n\nexport type CatchupPlan =\n | {\n readonly kind: 'range';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n | {readonly kind: 'ahead'; readonly headWatermark: string}\n | {\n readonly kind: 'too-old';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n // The log exists as a file but has no content to serve: the replicator has\n // not created or reconciled it yet. Subscriber selection treats this as\n // \"decline and fall back to PG catchup\", which is why it is a plan rather\n // than an error.\n | {readonly kind: 'not-ready'};\n\ntype PlanRow = {\n readonly headWatermark: string | null;\n readonly minWatermark: string | null;\n readonly boundaryExists: number;\n};\n\ntype ChangeLogRow = ChangeLogEntry & {\n readonly pos: number;\n};\n\ntype PreparedStatements = {\n readonly plan: Statement;\n readonly readBatch: Statement;\n};\n\n/**\n * Seeks to the final position so validating a boundary does not scan every row\n * of a large transaction. Exported so the focused query-plan test covers the\n * production subquery.\n */\nexport const SQLITE_CHANGE_LOG_BOUNDARY_SQL = /*sql*/ `\n SELECT\n \"precommit\" = @fromWatermark AND\n \"tag\" = 'commit' AS \"boundaryExists\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @fromWatermark\n ORDER BY \"pos\" DESC\n LIMIT 1\n`;\n\n/**\n * The log describes its own bounds; nothing here reads the replica.\n * `max(\"watermark\")` is the head because a transaction's rows commit atomically,\n * so the log never ends mid-transaction.\n *\n * `min` and `max` are deliberately kept as *separate* scalar subqueries: SQLite\n * applies its single-aggregate index optimization only when the aggregate is\n * alone in its query, so `SELECT min(x), max(x) FROM t` degrades to a full scan\n * while two scalar subqueries are two index seeks. The query-plan test pins\n * this.\n *\n * Exported so that test can cover the production query.\n */\nexport const SQLITE_CHANGE_LOG_PLAN_SQL = /*sql*/ `\n SELECT\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\",\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n coalesce((${SQLITE_CHANGE_LOG_BOUNDARY_SQL}), 0) AS \"boundaryExists\"\n`;\n\n/** Exported so the focused query-plan test covers the production query. */\nexport const SQLITE_CHANGE_LOG_READ_BATCH_SQL = /*sql*/ `\n SELECT\n \"watermark\",\n \"pos\",\n \"tag\",\n \"change\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE (\"watermark\", \"pos\") > (?, ?)\n AND \"watermark\" <= ?\n ORDER BY \"watermark\", \"pos\"\n LIMIT ?\n`;\n\nconst TABLE_EXISTS_SQL = /*sql*/ `\n SELECT 1 FROM \"sqlite_master\" WHERE \"type\" = 'table' AND \"name\" = ?\n`;\n\n/**\n * Reads the change log without retaining a SQLite snapshot while a consumer\n * processes a batch.\n *\n * The log lives in its own database beside the replica\n * (`changeLogFileName(replicaFile)`), which this reader opens read-only; it\n * never opens the replica. The connection therefore keeps the journal mode\n * configured by the change-log writer. Callers pin a head with {@link plan}\n * before passing it to {@link read}.\n *\n * A change-streamer can start before the replicator has created the log, so the\n * stream table may not exist yet. Statements are prepared on first use rather\n * than in the constructor, and {@link plan} reports `not-ready` until they can\n * be; a reader constructed too early becomes usable once the writer catches up.\n */\nexport class SQLiteChangeLogReader implements Disposable {\n readonly #db: Database;\n #statements: PreparedStatements | undefined;\n #closed = false;\n\n constructor(lc: LogContext, changeLogFile: string) {\n this.#db = new Database(\n lc.withContext('component', 'sqlite-change-log-reader'),\n changeLogFile,\n {readonly: true},\n );\n }\n\n plan(fromWatermark: string): CatchupPlan {\n this.#assertOpen();\n const statements = this.#prepare();\n if (statements === undefined) {\n return {kind: 'not-ready'};\n }\n const row = statements.plan.get<PlanRow>({fromWatermark});\n if (row.headWatermark === null || row.minWatermark === null) {\n return {kind: 'not-ready'};\n }\n const {headWatermark, minWatermark} = row;\n\n if (fromWatermark > headWatermark) {\n return {kind: 'ahead', headWatermark};\n }\n if (fromWatermark < minWatermark || row.boundaryExists === 0) {\n return {kind: 'too-old', minWatermark, headWatermark};\n }\n return {kind: 'range', minWatermark, headWatermark};\n }\n\n async *read(\n fromWatermark: string,\n throughWatermark: string,\n batchSize: number,\n signal?: AbortSignal | undefined,\n ): AsyncIterable<readonly WatermarkedChange[]> {\n assert(\n Number.isSafeInteger(batchSize) && batchSize > 0,\n 'SQLite change log batch size must be a positive safe integer',\n );\n this.#assertOpen();\n const statements = this.#prepare();\n assert(\n statements !== undefined,\n 'SQLite change log must be ready before it is read',\n );\n\n let lastWatermark = fromWatermark;\n // The first query must be strictly after the requested transaction. Every\n // real stream position is far below this sentinel.\n let lastPos = Number.MAX_SAFE_INTEGER;\n let lastTag: string | undefined;\n\n while (true) {\n this.#throwIfAborted(signal);\n const rows = statements.readBatch.all<ChangeLogRow>(\n lastWatermark,\n lastPos,\n throughWatermark,\n batchSize,\n );\n this.#throwIfAborted(signal);\n\n if (rows.length === 0) {\n break;\n }\n\n const changes = rows.map(reconstructWatermarkedChange);\n const last = rows.at(-1);\n assert(last !== undefined, 'non-empty batch must have a final row');\n lastWatermark = last.watermark;\n lastPos = last.pos;\n lastTag = last.tag;\n\n // all() has exhausted the SELECT and closed its implicit read\n // transaction. This yield therefore cannot pin the WAL while subscriber\n // flow control is pending.\n yield changes;\n }\n\n // `throughWatermark` was pinned from `max(\"watermark\")` of this same table,\n // so reaching it no longer depends on the log agreeing with the replica's\n // `_zero.replicationState`, which is what this assertion used to guard.\n // What remains is a concurrent purge of the head transaction, which the\n // purge policy (never past the latest durable transaction) does not do.\n if (fromWatermark !== throughWatermark) {\n assert(\n lastWatermark === throughWatermark && lastTag === 'commit',\n () =>\n `SQLite change log did not end at pinned head ${throughWatermark}`,\n );\n }\n }\n\n close(): void {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#db.close();\n }\n\n [Symbol.dispose](): void {\n this.close();\n }\n\n /**\n * Prepares the statements once the stream table exists, memoizing them.\n * Returns `undefined` while the log has not been created, which is a normal\n * startup state rather than an error.\n */\n #prepare(): PreparedStatements | undefined {\n if (this.#statements === undefined && this.#streamTableExists()) {\n this.#statements = {\n plan: this.#db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),\n readBatch: this.#db.prepare(SQLITE_CHANGE_LOG_READ_BATCH_SQL),\n };\n }\n return this.#statements;\n }\n\n #streamTableExists(): boolean {\n return (\n this.#db\n .prepare(TABLE_EXISTS_SQL)\n .get<{1: number} | undefined>(CHANGE_LOG_STREAM_TABLE) !== undefined\n );\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new AbortError('SQLite change log reader is closed');\n }\n }\n\n #throwIfAborted(signal: AbortSignal | undefined): void {\n this.#assertOpen();\n if (signal?.aborted) {\n throw new AbortError('SQLite change log read aborted');\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAiDA,IAAa,iCAAyC;;;;UAI5C,wBAAwB;;;;;;;;;;;;;;;;;;AAmBlC,IAAa,6BAAqC;;qCAEb,wBAAwB;;qCAExB,wBAAwB;;gBAE7C,+BAA+B;;;AAI/C,IAAa,mCAA2C;;;;;;UAM9C,wBAAwB;;;;;;AAOlC,IAAM,mBAA2B;;;;;;;;;;;;;;;;;;AAmBjC,IAAa,wBAAb,MAAyD;CACvD;CACA;CACA,UAAU;CAEV,YAAY,IAAgB,eAAuB;EACjD,KAAKA,MAAM,IAAI,SACb,GAAG,YAAY,aAAa,0BAA0B,GACtD,eACA,EAAC,UAAU,KAAI,CACjB;CACF;CAEA,KAAK,eAAoC;EACvC,KAAKC,YAAY;EACjB,MAAM,aAAa,KAAKC,SAAS;EACjC,IAAI,eAAe,KAAA,GACjB,OAAO,EAAC,MAAM,YAAW;EAE3B,MAAM,MAAM,WAAW,KAAK,IAAa,EAAC,cAAa,CAAC;EACxD,IAAI,IAAI,kBAAkB,QAAQ,IAAI,iBAAiB,MACrD,OAAO,EAAC,MAAM,YAAW;EAE3B,MAAM,EAAC,eAAe,iBAAgB;EAEtC,IAAI,gBAAgB,eAClB,OAAO;GAAC,MAAM;GAAS;EAAa;EAEtC,IAAI,gBAAgB,gBAAgB,IAAI,mBAAmB,GACzD,OAAO;GAAC,MAAM;GAAW;GAAc;EAAa;EAEtD,OAAO;GAAC,MAAM;GAAS;GAAc;EAAa;CACpD;CAEA,OAAO,KACL,eACA,kBACA,WACA,QAC6C;EAC7C,OACE,OAAO,cAAc,SAAS,KAAK,YAAY,GAC/C,8DACF;EACA,KAAKD,YAAY;EACjB,MAAM,aAAa,KAAKC,SAAS;EACjC,OACE,eAAe,KAAA,GACf,mDACF;EAEA,IAAI,gBAAgB;EAGpB,IAAI,UAAU,OAAO;EACrB,IAAI;EAEJ,OAAO,MAAM;GACX,KAAKC,gBAAgB,MAAM;GAC3B,MAAM,OAAO,WAAW,UAAU,IAChC,eACA,SACA,kBACA,SACF;GACA,KAAKA,gBAAgB,MAAM;GAE3B,IAAI,KAAK,WAAW,GAClB;GAGF,MAAM,UAAU,KAAK,IAAI,4BAA4B;GACrD,MAAM,OAAO,KAAK,GAAG,EAAE;GACvB,OAAO,SAAS,KAAA,GAAW,uCAAuC;GAClE,gBAAgB,KAAK;GACrB,UAAU,KAAK;GACf,UAAU,KAAK;GAKf,MAAM;EACR;EAOA,IAAI,kBAAkB,kBACpB,OACE,kBAAkB,oBAAoB,YAAY,gBAEhD,gDAAgD,kBACpD;CAEJ;CAEA,QAAc;EACZ,IAAI,KAAKC,SACP;EAEF,KAAKA,UAAU;EACf,KAAKJ,IAAI,MAAM;CACjB;CAEA,CAAC,OAAO,WAAiB;EACvB,KAAK,MAAM;CACb;;;;;;CAOA,WAA2C;EACzC,IAAI,KAAKK,gBAAgB,KAAA,KAAa,KAAKC,mBAAmB,GAC5D,KAAKD,cAAc;GACjB,MAAM,KAAKL,IAAI,QAAQ,0BAA0B;GACjD,WAAW,KAAKA,IAAI,QAAQ,gCAAgC;EAC9D;EAEF,OAAO,KAAKK;CACd;CAEA,qBAA8B;EAC5B,OACE,KAAKL,IACF,QAAQ,gBAAgB,EACxB,IAA6B,uBAAuB,MAAM,KAAA;CAEjE;CAEA,cAAoB;EAClB,IAAI,KAAKI,SACP,MAAM,IAAI,WAAW,oCAAoC;CAE7D;CAEA,gBAAgB,QAAuC;EACrD,KAAKH,YAAY;EACjB,IAAI,QAAQ,SACV,MAAM,IAAI,WAAW,gCAAgC;CAEzD;AACF"}
|
|
1
|
+
{"version":3,"file":"sqlite-change-log-reader.js","names":["#db","#assertOpen","#prepare","#throwIfAborted","#closed","#statements","#streamTableExists"],"sources":["../../../../../../zero-cache/src/services/change-streamer/sqlite-change-log-reader.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport {CHANGE_LOG_STREAM_TABLE} from '../replicator/change-log-db.ts';\nimport {\n reconstructWatermarkedChange,\n type ChangeLogEntry,\n} from './change-log-codec.ts';\nimport type {WatermarkedChange} from './change-streamer.ts';\n\nexport type CatchupPlan =\n | {\n readonly kind: 'range';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n | {readonly kind: 'ahead'; readonly headWatermark: string}\n | {\n readonly kind: 'too-old';\n readonly minWatermark: string;\n readonly headWatermark: string;\n }\n // The log exists as a file but has no content to serve: the replicator has\n // not created or reconciled it yet. Subscriber selection treats this as\n // \"decline and fall back to PG catchup\", which is why it is a plan rather\n // than an error.\n | {readonly kind: 'not-ready'};\n\ntype PlanRow = {\n readonly headWatermark: string | null;\n readonly minWatermark: string | null;\n readonly boundaryExists: number;\n};\n\ntype ChangeLogRow = ChangeLogEntry & {\n readonly pos: number;\n};\n\ntype PreparedStatements = {\n readonly plan: Statement;\n readonly readBatch: Statement;\n};\n\n/**\n * Seeks to the final position so validating a boundary does not scan every row\n * of a large transaction. Exported so the focused query-plan test covers the\n * production subquery.\n */\nexport const SQLITE_CHANGE_LOG_BOUNDARY_SQL = /*sql*/ `\n SELECT\n \"precommit\" = @fromWatermark AND\n \"tag\" = 'commit' AS \"boundaryExists\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE \"watermark\" = @fromWatermark\n ORDER BY \"pos\" DESC\n LIMIT 1\n`;\n\n/**\n * The log describes its own bounds; nothing here reads the replica.\n * `max(\"watermark\")` is the head because a transaction's rows commit atomically,\n * so the log never ends mid-transaction.\n *\n * `min` and `max` are deliberately kept as *separate* scalar subqueries: SQLite\n * applies its single-aggregate index optimization only when the aggregate is\n * alone in its query, so `SELECT min(x), max(x) FROM t` degrades to a full scan\n * while two scalar subqueries are two index seeks. The query-plan test pins\n * this.\n *\n * Exported so that test can cover the production query.\n */\nexport const SQLITE_CHANGE_LOG_PLAN_SQL = /*sql*/ `\n SELECT\n (SELECT max(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"headWatermark\",\n (SELECT min(\"watermark\") FROM \"${CHANGE_LOG_STREAM_TABLE}\")\n AS \"minWatermark\",\n coalesce((${SQLITE_CHANGE_LOG_BOUNDARY_SQL}), 0) AS \"boundaryExists\"\n`;\n\n/** Exported so the focused query-plan test covers the production query. */\nexport const SQLITE_CHANGE_LOG_READ_BATCH_SQL = /*sql*/ `\n SELECT\n \"watermark\",\n \"pos\",\n \"tag\",\n \"change\"\n FROM \"${CHANGE_LOG_STREAM_TABLE}\"\n WHERE (\"watermark\", \"pos\") > (?, ?)\n AND \"watermark\" <= ?\n ORDER BY \"watermark\", \"pos\"\n LIMIT ?\n`;\n\nconst TABLE_EXISTS_SQL = /*sql*/ `\n SELECT 1 FROM \"sqlite_master\" WHERE \"type\" = 'table' AND \"name\" = ?\n`;\n\n/**\n * Reads the change log without retaining a SQLite snapshot while a consumer\n * processes a batch.\n *\n * The log lives in its own database beside the replica\n * (`changeLogFileName(replicaFile)`), which this reader opens read-only; it\n * never opens the replica. The connection therefore keeps the journal mode\n * configured by the change-log writer. Callers pin a head with {@link plan}\n * before passing it to {@link read}.\n *\n * A change-streamer can start before the replicator has created the log, so the\n * stream table may not exist yet. Statements are prepared on first use rather\n * than in the constructor, and {@link plan} reports `not-ready` until they can\n * be; a reader constructed too early becomes usable once the writer catches up.\n */\nexport class SQLiteChangeLogReader implements Disposable {\n readonly #db: Database;\n #statements: PreparedStatements | undefined;\n #closed = false;\n\n constructor(lc: LogContext, changeLogFile: string) {\n this.#db = new Database(\n lc.withContext('component', 'sqlite-change-log-reader'),\n changeLogFile,\n {readonly: true},\n );\n }\n\n plan(fromWatermark: string): CatchupPlan {\n this.#assertOpen();\n const statements = this.#prepare();\n if (statements === undefined) {\n return {kind: 'not-ready'};\n }\n const row = statements.plan.get<PlanRow>({fromWatermark});\n if (row.headWatermark === null || row.minWatermark === null) {\n return {kind: 'not-ready'};\n }\n const {headWatermark, minWatermark} = row;\n\n if (fromWatermark > headWatermark) {\n return {kind: 'ahead', headWatermark};\n }\n if (fromWatermark < minWatermark || row.boundaryExists === 0) {\n return {kind: 'too-old', minWatermark, headWatermark};\n }\n return {kind: 'range', minWatermark, headWatermark};\n }\n\n async *read(\n fromWatermark: string,\n throughWatermark: string,\n batchSize: number,\n signal?: AbortSignal | undefined,\n maxRows?: number | undefined,\n ): AsyncIterable<readonly WatermarkedChange[]> {\n assert(\n Number.isSafeInteger(batchSize) && batchSize > 0,\n 'SQLite change log batch size must be a positive safe integer',\n );\n assert(\n maxRows === undefined || (Number.isSafeInteger(maxRows) && maxRows > 0),\n 'SQLite change log row limit must be a positive safe integer',\n );\n this.#assertOpen();\n const statements = this.#prepare();\n assert(\n statements !== undefined,\n 'SQLite change log must be ready before it is read',\n );\n\n let lastWatermark = fromWatermark;\n // The first query must be strictly after the requested transaction. Every\n // real stream position is far below this sentinel.\n let lastPos = Number.MAX_SAFE_INTEGER;\n let lastTag: string | undefined;\n let rowsRead = 0;\n\n while (true) {\n this.#throwIfAborted(signal);\n const remainingRows =\n maxRows === undefined ? batchSize : maxRows - rowsRead;\n if (remainingRows === 0) {\n break;\n }\n const rows = statements.readBatch.all<ChangeLogRow>(\n lastWatermark,\n lastPos,\n throughWatermark,\n Math.min(batchSize, remainingRows),\n );\n this.#throwIfAborted(signal);\n\n if (rows.length === 0) {\n break;\n }\n\n const changes = rows.map(reconstructWatermarkedChange);\n const last = rows.at(-1);\n assert(last !== undefined, 'non-empty batch must have a final row');\n lastWatermark = last.watermark;\n lastPos = last.pos;\n lastTag = last.tag;\n rowsRead += rows.length;\n\n // all() has exhausted the SELECT and closed its implicit read\n // transaction. This yield therefore cannot pin the WAL while subscriber\n // flow control is pending.\n yield changes;\n }\n\n // `throughWatermark` was pinned from `max(\"watermark\")` of this same table,\n // so reaching it no longer depends on the log agreeing with the replica's\n // `_zero.replicationState`, which is what this assertion used to guard.\n // What remains is a concurrent purge of the head transaction, which the\n // purge policy (never past the latest durable transaction) does not do.\n // A read that stopped at `maxRows` cannot know whether more rows follow,\n // so completeness is only asserted for a read that ran out on its own.\n // Detecting a short bounded read is left to the caller, which set the\n // limit and is the only side that knows what a full range should hold.\n if (\n fromWatermark !== throughWatermark &&\n (maxRows === undefined || rowsRead < maxRows)\n ) {\n assert(\n lastWatermark === throughWatermark && lastTag === 'commit',\n () =>\n `SQLite change log did not end at pinned head ${throughWatermark}`,\n );\n }\n }\n\n close(): void {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#db.close();\n }\n\n [Symbol.dispose](): void {\n this.close();\n }\n\n /**\n * Prepares the statements once the stream table exists, memoizing them.\n * Returns `undefined` while the log has not been created, which is a normal\n * startup state rather than an error.\n */\n #prepare(): PreparedStatements | undefined {\n if (this.#statements === undefined && this.#streamTableExists()) {\n this.#statements = {\n plan: this.#db.prepare(SQLITE_CHANGE_LOG_PLAN_SQL),\n readBatch: this.#db.prepare(SQLITE_CHANGE_LOG_READ_BATCH_SQL),\n };\n }\n return this.#statements;\n }\n\n #streamTableExists(): boolean {\n return (\n this.#db\n .prepare(TABLE_EXISTS_SQL)\n .get<{1: number} | undefined>(CHANGE_LOG_STREAM_TABLE) !== undefined\n );\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new AbortError('SQLite change log reader is closed');\n }\n }\n\n #throwIfAborted(signal: AbortSignal | undefined): void {\n this.#assertOpen();\n if (signal?.aborted) {\n throw new AbortError('SQLite change log read aborted');\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import { type JSONObject } from '../../../../shared/src/bigint-json.ts';
|
|
2
3
|
import { TransactionPool } from '../../db/transaction-pool.ts';
|
|
3
4
|
import { type PostgresDB } from '../../types/pg.ts';
|
|
4
5
|
import { type ShardID } from '../../types/shards.ts';
|
|
@@ -8,7 +9,13 @@ import type { UpstreamStatusMessage } from '../change-source/protocol/current/st
|
|
|
8
9
|
import { type CookieSet } from '../replicator/change-log-cookies.ts';
|
|
9
10
|
import type { ReplicatorMode } from '../replicator/replicator.ts';
|
|
10
11
|
import type { Service } from '../service.ts';
|
|
12
|
+
import { type ChangeLogEntry } from './change-log-codec.ts';
|
|
11
13
|
import type { Subscriber } from './subscriber.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Factory for creating dynamic connection pools used by the Storer to
|
|
16
|
+
* isolate concurrent transactions into separate pg clients.
|
|
17
|
+
*/
|
|
18
|
+
export type PostgresDBProvider = (applicationName: string, maxConns: number) => PostgresDB;
|
|
12
19
|
export type TuningOptions = {
|
|
13
20
|
backPressureLimitHeapProportion: number;
|
|
14
21
|
statementTimeoutMs: number;
|
|
@@ -48,7 +55,7 @@ export type TuningOptions = {
|
|
|
48
55
|
export declare class Storer implements Service {
|
|
49
56
|
#private;
|
|
50
57
|
readonly id = "storer";
|
|
51
|
-
constructor(lc: LogContext, shard: ShardID, taskID: string, discoveryAddress: string, discoveryProtocol: string,
|
|
58
|
+
constructor(lc: LogContext, shard: ShardID, taskID: string, discoveryAddress: string, discoveryProtocol: string, dbProvider: PostgresDBProvider, replicaVersion: string, onCommitted: (c: Commit | UpstreamStatusMessage) => void, onFatal: (err: Error) => void, { backPressureLimitHeapProportion, statementTimeoutMs, changeLogBatchSize, drainTimeoutMs, }: TuningOptions);
|
|
52
59
|
assumeOwnership(purgeLock?: PurgeLock | null): Promise<void>;
|
|
53
60
|
/**
|
|
54
61
|
* The whole of what a stream connection starts from: where to resume, what to
|
|
@@ -73,6 +80,28 @@ export declare class Storer implements Service {
|
|
|
73
80
|
cookies: CookieSet;
|
|
74
81
|
}>;
|
|
75
82
|
getMinWatermarkForCatchup(): Promise<string | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Returns the retained Postgres catchup bounds.
|
|
85
|
+
* `minWatermark` is the oldest transaction, or `null` for a new replica.
|
|
86
|
+
* `lastWatermark` is the durable head.
|
|
87
|
+
*/
|
|
88
|
+
getCatchupBounds(): Promise<{
|
|
89
|
+
minWatermark: string | null;
|
|
90
|
+
lastWatermark: string;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Lists at most `limit` committed transaction watermarks in
|
|
94
|
+
* `(afterWatermark, throughWatermark]`, in ascending order.
|
|
95
|
+
*/
|
|
96
|
+
listCommitWatermarks(afterWatermark: string, throughWatermark: string, limit: number): Promise<string[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Reads `(afterWatermark, throughWatermark]` in stream order and bounded batches.
|
|
99
|
+
* This query matches the subscriber catchup query.
|
|
100
|
+
*
|
|
101
|
+
* Both queries fail when an escaped NUL reaches `change->'tag'`.
|
|
102
|
+
* Postgres cannot convert that value to text.
|
|
103
|
+
*/
|
|
104
|
+
readCatchupRange(afterWatermark: string, throughWatermark: string, batchRows?: number): AsyncIterable<ChangeLogEntry[]>;
|
|
76
105
|
purgeRecordsBefore(watermark: string): Promise<number>;
|
|
77
106
|
/**
|
|
78
107
|
* @returns The JSON stringified stream message to be sent downstream.
|
|
@@ -110,4 +139,37 @@ export declare class PurgeLocker {
|
|
|
110
139
|
constructor(lc: LogContext, shard: ShardID, db: PostgresDB);
|
|
111
140
|
acquire(): Promise<PurgeLock | null>;
|
|
112
141
|
}
|
|
142
|
+
type TaskKey = JSONObject;
|
|
143
|
+
type TaskDoneFn = () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Periodic monitor for detecting the pathological condition in which a db
|
|
146
|
+
* query hangs forever because of a connection problem, such as a half-closed
|
|
147
|
+
* socket.
|
|
148
|
+
*
|
|
149
|
+
* Use this for db calls that recur or accumulate progress within a single
|
|
150
|
+
* logical operation on the main storer loop (e.g. the queue-processing loop,
|
|
151
|
+
* or a multi-batch catchup read), where "no progress for N ms" is the
|
|
152
|
+
* meaningful signal. For a one-off db call -- including ones made before
|
|
153
|
+
* {@link Storer.run} starts this monitor's polling -- use a plain timeout
|
|
154
|
+
* (see `Storer.#withTimeout`) instead.
|
|
155
|
+
*
|
|
156
|
+
* The failure handler should stop the server. The ProgressMonitor will
|
|
157
|
+
* assume this and stop itself after calling it.
|
|
158
|
+
*
|
|
159
|
+
* Internal to the Storer, but exported for testing.
|
|
160
|
+
*/
|
|
161
|
+
export declare class ProgressMonitor {
|
|
162
|
+
#private;
|
|
163
|
+
constructor(lc: LogContext, failureThresholdMs: number, onFailure: (err: Error) => void);
|
|
164
|
+
start(): void;
|
|
165
|
+
checkTaskProgress(now: Date): void;
|
|
166
|
+
/**
|
|
167
|
+
* Starts a task to be tracked.
|
|
168
|
+
*
|
|
169
|
+
* @return a callback to invoke when the task is done.
|
|
170
|
+
*/
|
|
171
|
+
trackTask(task: TaskKey, now?: Date): TaskDoneFn;
|
|
172
|
+
stop(): void;
|
|
173
|
+
}
|
|
174
|
+
export {};
|
|
113
175
|
//# sourceMappingURL=storer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"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"}
|