orez 0.2.27 → 0.2.30
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/dist/cf-do/worker.d.ts +3 -0
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +37 -15
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -40
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- package/src/worker/zero-cache-embed.ts +0 -277
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* live restore stress test.
|
|
3
|
-
*
|
|
4
|
-
* keeps a frontend-like websocket connection active while a large restore runs,
|
|
5
|
-
* then triggers the same full reset path used by pg_restore (SIGUSR1) and
|
|
6
|
-
* verifies sync still works after restart.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import { tmpdir } from 'node:os'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
|
|
13
|
-
import { loadModule } from 'pgsql-parser'
|
|
14
|
-
import postgres from 'postgres'
|
|
15
|
-
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
|
16
|
-
import WebSocket from 'ws'
|
|
17
|
-
|
|
18
|
-
import { execDumpFile } from '../cli.js'
|
|
19
|
-
import { startZeroLite } from '../index.js'
|
|
20
|
-
import { installChangeTracking } from '../replication/change-tracker.js'
|
|
21
|
-
import {
|
|
22
|
-
ensureTablesInPublications,
|
|
23
|
-
hasNonNullPermissions,
|
|
24
|
-
installAllowAllPermissions,
|
|
25
|
-
} from './test-permissions.js'
|
|
26
|
-
|
|
27
|
-
import type { PGlite } from '@electric-sql/pglite'
|
|
28
|
-
|
|
29
|
-
const SYNC_PROTOCOL_VERSION = 49
|
|
30
|
-
const LIVE_CLIENT_SCHEMA = {
|
|
31
|
-
tables: {
|
|
32
|
-
restore_live_probe: {
|
|
33
|
-
columns: {
|
|
34
|
-
id: { type: 'string' },
|
|
35
|
-
value: { type: 'string' },
|
|
36
|
-
},
|
|
37
|
-
primaryKey: ['id'],
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function encodeSecProtocols(
|
|
43
|
-
initConnectionMessage: unknown,
|
|
44
|
-
authToken: string | undefined
|
|
45
|
-
): string {
|
|
46
|
-
const payload = JSON.stringify({ initConnectionMessage, authToken })
|
|
47
|
-
return encodeURIComponent(Buffer.from(payload, 'utf-8').toString('base64'))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
class Queue<T> {
|
|
51
|
-
private items: T[] = []
|
|
52
|
-
private waiters: Array<{
|
|
53
|
-
resolve: (v: T) => void
|
|
54
|
-
timer?: ReturnType<typeof setTimeout>
|
|
55
|
-
}> = []
|
|
56
|
-
|
|
57
|
-
enqueue(item: T) {
|
|
58
|
-
const waiter = this.waiters.shift()
|
|
59
|
-
if (waiter) {
|
|
60
|
-
if (waiter.timer) clearTimeout(waiter.timer)
|
|
61
|
-
waiter.resolve(item)
|
|
62
|
-
} else {
|
|
63
|
-
this.items.push(item)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
dequeue(fallback?: T, timeoutMs = 10_000): Promise<T> {
|
|
68
|
-
if (this.items.length > 0) {
|
|
69
|
-
return Promise.resolve(this.items.shift()!)
|
|
70
|
-
}
|
|
71
|
-
return new Promise<T>((resolve) => {
|
|
72
|
-
const waiter: { resolve: (v: T) => void; timer?: ReturnType<typeof setTimeout> } = {
|
|
73
|
-
resolve,
|
|
74
|
-
}
|
|
75
|
-
if (fallback !== undefined) {
|
|
76
|
-
waiter.timer = setTimeout(() => {
|
|
77
|
-
const idx = this.waiters.indexOf(waiter)
|
|
78
|
-
if (idx >= 0) this.waiters.splice(idx, 1)
|
|
79
|
-
resolve(fallback)
|
|
80
|
-
}, timeoutMs)
|
|
81
|
-
}
|
|
82
|
-
this.waiters.push(waiter)
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function envInt(name: string, fallback: number): number {
|
|
88
|
-
const raw = process.env[name]
|
|
89
|
-
if (!raw) return fallback
|
|
90
|
-
const n = Number(raw)
|
|
91
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function escapeCopy(val: string): string {
|
|
95
|
-
return val
|
|
96
|
-
.replace(/\\/g, '\\\\')
|
|
97
|
-
.replace(/\t/g, '\\t')
|
|
98
|
-
.replace(/\n/g, '\\n')
|
|
99
|
-
.replace(/\r/g, '\\r')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function generateStressDump(opts: {
|
|
103
|
-
tables: number
|
|
104
|
-
rowsPerTable: number
|
|
105
|
-
columnsPerTable: number
|
|
106
|
-
payloadBytes: number
|
|
107
|
-
}): string {
|
|
108
|
-
const lines: string[] = []
|
|
109
|
-
lines.push('SET statement_timeout = 0;')
|
|
110
|
-
lines.push("SET client_encoding = 'UTF8';")
|
|
111
|
-
lines.push('SET standard_conforming_strings = on;')
|
|
112
|
-
lines.push('')
|
|
113
|
-
|
|
114
|
-
for (let t = 0; t < opts.tables; t++) {
|
|
115
|
-
const table = `stress_restore_${t}`
|
|
116
|
-
const cols = Array.from({ length: opts.columnsPerTable }, (_, i) => `c_${i} TEXT`)
|
|
117
|
-
lines.push(
|
|
118
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id BIGINT PRIMARY KEY, ${cols.join(', ')});`
|
|
119
|
-
)
|
|
120
|
-
lines.push(
|
|
121
|
-
`COPY ${table} (id, ${Array.from({ length: opts.columnsPerTable }, (_, i) => `c_${i}`).join(', ')}) FROM stdin;`
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
for (let r = 0; r < opts.rowsPerTable; r++) {
|
|
125
|
-
const id = t * 1_000_000 + r + 1
|
|
126
|
-
const row = Array.from({ length: opts.columnsPerTable }, (_, c) => {
|
|
127
|
-
if (r % 97 === 0 && c === 0) return '\\N'
|
|
128
|
-
const base = `t${t}_r${r}_c${c}_`
|
|
129
|
-
return escapeCopy(base + 'x'.repeat(Math.max(1, opts.payloadBytes - base.length)))
|
|
130
|
-
})
|
|
131
|
-
lines.push(`${id}\t${row.join('\t')}`)
|
|
132
|
-
}
|
|
133
|
-
lines.push('\\.')
|
|
134
|
-
lines.push('')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return lines.join('\n')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function connectAndSubscribe(
|
|
141
|
-
port: number,
|
|
142
|
-
downstream: Queue<unknown>,
|
|
143
|
-
query: Record<string, unknown>
|
|
144
|
-
): Promise<WebSocket> {
|
|
145
|
-
return new Promise((resolve, reject) => {
|
|
146
|
-
const initConnectionMessage: [string, Record<string, unknown>] = [
|
|
147
|
-
'initConnection',
|
|
148
|
-
{
|
|
149
|
-
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
150
|
-
clientSchema: LIVE_CLIENT_SCHEMA,
|
|
151
|
-
},
|
|
152
|
-
]
|
|
153
|
-
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
154
|
-
const ws = new WebSocket(
|
|
155
|
-
`ws://127.0.0.1:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
156
|
-
`?clientGroupID=restore-live-cg-${Date.now()}` +
|
|
157
|
-
`&clientID=restore-live-client` +
|
|
158
|
-
`&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
159
|
-
secProtocol
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
let settled = false
|
|
163
|
-
const failTimer = setTimeout(() => {
|
|
164
|
-
if (settled) return
|
|
165
|
-
settled = true
|
|
166
|
-
try {
|
|
167
|
-
ws.close()
|
|
168
|
-
} catch {}
|
|
169
|
-
reject(new Error('websocket connected but no downstream messages'))
|
|
170
|
-
}, 7000)
|
|
171
|
-
|
|
172
|
-
ws.on('message', (data) => {
|
|
173
|
-
const msg = JSON.parse(data.toString())
|
|
174
|
-
downstream.enqueue(msg)
|
|
175
|
-
if (!settled) {
|
|
176
|
-
settled = true
|
|
177
|
-
clearTimeout(failTimer)
|
|
178
|
-
resolve(ws)
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
ws.once('error', (err) => {
|
|
182
|
-
if (settled) return
|
|
183
|
-
settled = true
|
|
184
|
-
clearTimeout(failTimer)
|
|
185
|
-
reject(err)
|
|
186
|
-
})
|
|
187
|
-
ws.once('close', () => {
|
|
188
|
-
if (settled) return
|
|
189
|
-
settled = true
|
|
190
|
-
clearTimeout(failTimer)
|
|
191
|
-
reject(new Error('websocket closed before initial downstream message'))
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function connectAndSubscribeWithRetry(
|
|
197
|
-
port: number,
|
|
198
|
-
downstream: Queue<unknown>,
|
|
199
|
-
query: Record<string, unknown>,
|
|
200
|
-
timeoutMs = 30_000
|
|
201
|
-
): Promise<WebSocket> {
|
|
202
|
-
const deadline = Date.now() + timeoutMs
|
|
203
|
-
let lastErr: unknown
|
|
204
|
-
while (Date.now() < deadline) {
|
|
205
|
-
try {
|
|
206
|
-
return await connectAndSubscribe(port, downstream, query)
|
|
207
|
-
} catch (err) {
|
|
208
|
-
lastErr = err
|
|
209
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
throw new Error(
|
|
213
|
-
`timed out connecting websocket after reset: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function drainInitialPokes(downstream: Queue<unknown>) {
|
|
218
|
-
const deadline = Date.now() + 30_000
|
|
219
|
-
while (Date.now() < deadline) {
|
|
220
|
-
const msg = (await downstream.dequeue('timeout' as any, 3000)) as any
|
|
221
|
-
if (msg === 'timeout') return
|
|
222
|
-
if (Array.isArray(msg) && msg[0] === 'pokeEnd') return
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function waitForPokeWithValue(
|
|
227
|
-
downstream: Queue<unknown>,
|
|
228
|
-
expectedValue: string,
|
|
229
|
-
timeoutMs = 20_000
|
|
230
|
-
): Promise<void> {
|
|
231
|
-
const deadline = Date.now() + timeoutMs
|
|
232
|
-
const seen: unknown[] = []
|
|
233
|
-
while (Date.now() < deadline) {
|
|
234
|
-
const remaining = Math.max(1000, deadline - Date.now())
|
|
235
|
-
const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
|
|
236
|
-
if (msg === 'timeout') {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`timed out waiting for pokePart; recent messages: ${JSON.stringify(seen.slice(-8))}`
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
seen.push(msg)
|
|
242
|
-
if (!Array.isArray(msg) || msg[0] !== 'pokePart' || !msg[1]?.rowsPatch) continue
|
|
243
|
-
const rowsPatch = msg[1].rowsPatch as Array<Record<string, any>>
|
|
244
|
-
if (
|
|
245
|
-
rowsPatch.some(
|
|
246
|
-
(patch) =>
|
|
247
|
-
patch.op === 'put' &&
|
|
248
|
-
patch.tableName === 'restore_live_probe' &&
|
|
249
|
-
patch.value?.value === expectedValue
|
|
250
|
-
)
|
|
251
|
-
) {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
throw new Error(
|
|
256
|
-
`timed out waiting for restore_live_probe value "${expectedValue}"; recent messages: ${JSON.stringify(seen.slice(-8))}`
|
|
257
|
-
)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function waitForZero(port: number, timeoutMs = 60_000) {
|
|
261
|
-
const { Socket } = await import('node:net')
|
|
262
|
-
const deadline = Date.now() + timeoutMs
|
|
263
|
-
while (Date.now() < deadline) {
|
|
264
|
-
const ok = await new Promise<boolean>((resolve) => {
|
|
265
|
-
const sock = new Socket()
|
|
266
|
-
const done = (value: boolean) => {
|
|
267
|
-
sock.removeAllListeners()
|
|
268
|
-
try {
|
|
269
|
-
sock.destroy()
|
|
270
|
-
} catch {}
|
|
271
|
-
resolve(value)
|
|
272
|
-
}
|
|
273
|
-
sock.setTimeout(1000)
|
|
274
|
-
sock.once('connect', () => done(true))
|
|
275
|
-
sock.once('timeout', () => done(false))
|
|
276
|
-
sock.once('error', () => done(false))
|
|
277
|
-
sock.connect(port, '127.0.0.1')
|
|
278
|
-
})
|
|
279
|
-
if (ok) return
|
|
280
|
-
await new Promise((r) => setTimeout(r, 500))
|
|
281
|
-
}
|
|
282
|
-
throw new Error(`zero-cache not ready on port ${port} after ${timeoutMs}ms`)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
describe('live restore stress with connected frontend', { timeout: 360_000 }, () => {
|
|
286
|
-
let db: PGlite
|
|
287
|
-
let pgPort: number
|
|
288
|
-
let zeroPort: number
|
|
289
|
-
let shutdown: () => Promise<void>
|
|
290
|
-
let restartZero: (() => Promise<void>) | undefined
|
|
291
|
-
let resetZeroFull: (() => Promise<void>) | undefined
|
|
292
|
-
let dataDir: string
|
|
293
|
-
let dumpFile: string
|
|
294
|
-
|
|
295
|
-
beforeAll(async () => {
|
|
296
|
-
await loadModule()
|
|
297
|
-
|
|
298
|
-
const tables = envInt('OREZ_STRESS_TABLES', 6)
|
|
299
|
-
const rowsPerTable = envInt('OREZ_STRESS_ROWS', 1800)
|
|
300
|
-
const columnsPerTable = envInt('OREZ_STRESS_COLS', 8)
|
|
301
|
-
const payloadBytes = envInt('OREZ_STRESS_PAYLOAD', 96)
|
|
302
|
-
|
|
303
|
-
dumpFile = join(tmpdir(), `orez-live-stress-${Date.now()}.sql`)
|
|
304
|
-
writeFileSync(
|
|
305
|
-
dumpFile,
|
|
306
|
-
generateStressDump({ tables, rowsPerTable, columnsPerTable, payloadBytes })
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
dataDir = `.orez-live-stress-test-${Date.now()}`
|
|
310
|
-
const started = await startZeroLite({
|
|
311
|
-
pgPort: 29000 + Math.floor(Math.random() * 1000),
|
|
312
|
-
zeroPort: 30000 + Math.floor(Math.random() * 1000),
|
|
313
|
-
dataDir,
|
|
314
|
-
logLevel: 'warn',
|
|
315
|
-
skipZeroCache: false,
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
db = started.db
|
|
319
|
-
pgPort = started.pgPort
|
|
320
|
-
zeroPort = started.zeroPort
|
|
321
|
-
shutdown = started.stop
|
|
322
|
-
restartZero = started.restartZero
|
|
323
|
-
resetZeroFull = started.resetZeroFull
|
|
324
|
-
await waitForZero(zeroPort, 90_000)
|
|
325
|
-
}, 180_000)
|
|
326
|
-
|
|
327
|
-
afterAll(async () => {
|
|
328
|
-
if (shutdown) await shutdown()
|
|
329
|
-
try {
|
|
330
|
-
unlinkSync(dumpFile)
|
|
331
|
-
} catch {}
|
|
332
|
-
if (dataDir) {
|
|
333
|
-
try {
|
|
334
|
-
rmSync(dataDir, { recursive: true, force: true })
|
|
335
|
-
} catch {}
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
test('frontend stays connected through restore lifecycle and syncs after reset', async () => {
|
|
340
|
-
await db.exec(`
|
|
341
|
-
CREATE TABLE IF NOT EXISTS restore_live_probe (
|
|
342
|
-
id TEXT PRIMARY KEY,
|
|
343
|
-
value TEXT NOT NULL
|
|
344
|
-
)
|
|
345
|
-
`)
|
|
346
|
-
await ensureTablesInPublications(db, ['restore_live_probe'])
|
|
347
|
-
await installAllowAllPermissions(db, ['restore_live_probe'])
|
|
348
|
-
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
349
|
-
if (resetZeroFull) {
|
|
350
|
-
await resetZeroFull()
|
|
351
|
-
await waitForZero(zeroPort, 90_000)
|
|
352
|
-
} else if (restartZero) {
|
|
353
|
-
await restartZero()
|
|
354
|
-
await waitForZero(zeroPort, 60_000)
|
|
355
|
-
}
|
|
356
|
-
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
357
|
-
if (pubName) {
|
|
358
|
-
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
359
|
-
await db
|
|
360
|
-
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."restore_live_probe"`)
|
|
361
|
-
.catch(() => {})
|
|
362
|
-
await installChangeTracking(db)
|
|
363
|
-
}
|
|
364
|
-
await db.query(`INSERT INTO restore_live_probe (id, value) VALUES ($1, $2)`, [
|
|
365
|
-
'before-restore',
|
|
366
|
-
'before',
|
|
367
|
-
])
|
|
368
|
-
|
|
369
|
-
const downstream = new Queue<unknown>()
|
|
370
|
-
let ws = await connectAndSubscribeWithRetry(zeroPort, downstream, {
|
|
371
|
-
table: 'restore_live_probe',
|
|
372
|
-
orderBy: [['id', 'asc']],
|
|
373
|
-
})
|
|
374
|
-
await drainInitialPokes(downstream)
|
|
375
|
-
|
|
376
|
-
// restore while websocket is connected (frontend simulation)
|
|
377
|
-
const sql = postgres({
|
|
378
|
-
host: '127.0.0.1',
|
|
379
|
-
port: pgPort,
|
|
380
|
-
user: 'user',
|
|
381
|
-
password: 'password',
|
|
382
|
-
database: 'postgres',
|
|
383
|
-
max: 1,
|
|
384
|
-
onnotice: () => {},
|
|
385
|
-
})
|
|
386
|
-
try {
|
|
387
|
-
const wireDb = { exec: (query: string) => sql.unsafe(query) as Promise<unknown> }
|
|
388
|
-
await execDumpFile(wireDb, dumpFile)
|
|
389
|
-
} finally {
|
|
390
|
-
await sql.end({ timeout: 1 }).catch(() => {})
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const pid = Number(readFileSync(join(dataDir, 'orez.pid'), 'utf-8').trim())
|
|
394
|
-
expect(pid).toBeGreaterThan(0)
|
|
395
|
-
process.kill(pid, 'SIGUSR1')
|
|
396
|
-
await waitForZero(zeroPort, 90_000)
|
|
397
|
-
if (pubName) {
|
|
398
|
-
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
399
|
-
await db
|
|
400
|
-
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."restore_live_probe"`)
|
|
401
|
-
.catch(() => {})
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
ws.close()
|
|
406
|
-
} catch {}
|
|
407
|
-
const downstreamAfterReset = new Queue<unknown>()
|
|
408
|
-
ws = await connectAndSubscribeWithRetry(zeroPort, downstreamAfterReset, {
|
|
409
|
-
table: 'restore_live_probe',
|
|
410
|
-
orderBy: [['id', 'asc']],
|
|
411
|
-
})
|
|
412
|
-
await drainInitialPokes(downstreamAfterReset)
|
|
413
|
-
|
|
414
|
-
// verify write is captured in change tracking after reset
|
|
415
|
-
const marker = `after-${Date.now()}`
|
|
416
|
-
await db.query(`INSERT INTO restore_live_probe (id, value) VALUES ($1, $2)`, [
|
|
417
|
-
`post-restore-${Date.now()}`,
|
|
418
|
-
marker,
|
|
419
|
-
])
|
|
420
|
-
const tracked = await db.query<{ count: string }>(
|
|
421
|
-
`SELECT count(*)::text as count
|
|
422
|
-
FROM _orez._zero_changes
|
|
423
|
-
WHERE table_name = 'public.restore_live_probe'`
|
|
424
|
-
)
|
|
425
|
-
if (Number(tracked.rows[0]?.count || '0') === 0) {
|
|
426
|
-
throw new Error('post-reset write was not captured in _orez._zero_changes')
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
await waitForPokeWithValue(downstreamAfterReset, marker, 30_000)
|
|
430
|
-
|
|
431
|
-
ws.close()
|
|
432
|
-
})
|
|
433
|
-
})
|