orez 0.0.52 → 0.0.53

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/src/index.ts CHANGED
@@ -101,6 +101,10 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
101
101
 
102
102
  mkdirSync(config.dataDir, { recursive: true })
103
103
 
104
+ // write pid file for IPC (pg_restore uses this to signal restart)
105
+ const pidFile = resolve(config.dataDir, 'orez.pid')
106
+ writeFileSync(pidFile, String(process.pid))
107
+
104
108
  // start pglite (separate instances for postgres, zero_cvr, zero_cdb)
105
109
  const instances = await createPGliteInstances(config)
106
110
  const db = instances.postgres
@@ -166,6 +170,15 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
166
170
  })
167
171
  }
168
172
 
173
+ // handle SIGUSR1 to restart zero-cache (sent by pg_restore after clearing replica)
174
+ if (!config.skipZeroCache) {
175
+ process.on('SIGUSR1', async () => {
176
+ log.orez('received SIGUSR1, restarting zero-cache...')
177
+ await restartZeroCache(false) // replica already cleared by pg_restore
178
+ log.zero(`ready ${port(config.zeroPort, 'magenta')}`)
179
+ })
180
+ }
181
+
169
182
  const killZeroCache = async () => {
170
183
  if (zeroCacheProcess && !zeroCacheProcess.killed) {
171
184
  zeroCacheProcess.kill('SIGTERM')
@@ -202,6 +215,9 @@ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
202
215
  instances.cvr.close(),
203
216
  instances.cdb.close(),
204
217
  ])
218
+ try {
219
+ unlinkSync(pidFile)
220
+ } catch {}
205
221
  log.debug.orez('stopped')
206
222
  }
207
223
 
@@ -353,9 +369,13 @@ async function startZeroCache(
353
369
  const cdbUrl = getConnectionString(config, 'zero_cdb')
354
370
 
355
371
  // defaults that can be overridden by user env
372
+ // when admin is enabled and user hasn't set ZERO_LOG_LEVEL, capture debug
373
+ // logs for the admin UI while still respecting --log-level for console output
374
+ const zeroLogLevel =
375
+ config.adminPort > 0 && !process.env.ZERO_LOG_LEVEL ? 'debug' : config.logLevel
356
376
  const defaults: Record<string, string> = {
357
377
  NODE_ENV: 'development',
358
- ZERO_LOG_LEVEL: config.logLevel,
378
+ ZERO_LOG_LEVEL: zeroLogLevel,
359
379
  ZERO_NUM_SYNC_WORKERS: '1',
360
380
  // disable query planner — it relies on scanStatus which causes infinite
361
381
  // loops with wasm sqlite and has caused freezes with native too.
@@ -402,11 +422,30 @@ async function startZeroCache(
402
422
  stdio: ['ignore', 'pipe', 'pipe'],
403
423
  })
404
424
 
425
+ // detect log level from zero-cache output
426
+ const detectLevel = (line: string, fallback: string): string => {
427
+ const lower = line.toLowerCase()
428
+ if (
429
+ lower.includes('"level":"error"') ||
430
+ lower.includes(' error ') ||
431
+ lower.includes('error:')
432
+ )
433
+ return 'error'
434
+ if (
435
+ lower.includes('"level":"warn"') ||
436
+ lower.includes(' warn ') ||
437
+ lower.includes('warning:')
438
+ )
439
+ return 'warn'
440
+ if (lower.includes('"level":"debug"') || lower.includes(' debug ')) return 'debug'
441
+ return fallback
442
+ }
443
+
405
444
  child.stdout?.on('data', (data: Buffer) => {
406
445
  const lines = data.toString().trim().split('\n')
407
446
  for (const line of lines) {
408
447
  log.debug.zero(line)
409
- logStore?.push('zero', 'info', line)
448
+ logStore?.push('zero', detectLevel(line, 'info'), line)
410
449
  }
411
450
  })
412
451
 
@@ -414,7 +453,7 @@ async function startZeroCache(
414
453
  const lines = data.toString().trim().split('\n')
415
454
  for (const line of lines) {
416
455
  log.debug.zero(line)
417
- logStore?.push('zero', 'error', line)
456
+ logStore?.push('zero', detectLevel(line, 'error'), line)
418
457
  }
419
458
  })
420
459
 
@@ -692,8 +692,8 @@ describe('tcp replication', () => {
692
692
  }, 15_000)
693
693
 
694
694
  it('handles multiple tables in same stream', async () => {
695
- await db.exec(`CREATE TABLE public.other (id SERIAL PRIMARY KEY, label TEXT)`)
696
- await db.exec(`CREATE PUBLICATION zero_multi FOR ALL TABLES`)
695
+ // create a second table and re-install tracking for both
696
+ await db.exec(`CREATE TABLE public.products (id SERIAL PRIMARY KEY, label TEXT)`)
697
697
  await installChangeTracking(db)
698
698
 
699
699
  const replClient = new TestPgClient(port)
@@ -707,28 +707,37 @@ describe('tcp replication', () => {
707
707
  'CREATE_REPLICATION_SLOT "multi_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
708
708
  )
709
709
  await replClient.startReplication(
710
- "START_REPLICATION SLOT \"multi_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_multi')"
710
+ "START_REPLICATION SLOT \"multi_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
711
711
  )
712
712
 
713
713
  await replClient.collectStream(200)
714
714
 
715
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('t1', 1)`)
716
- await db.exec(`INSERT INTO public.other (label) VALUES ('t2')`)
715
+ // insert into both tables
716
+ await db.exec(`INSERT INTO public.items (name, value) VALUES ('multi1', 1)`)
717
+ await db.exec(`INSERT INTO public.products (label) VALUES ('multi2')`)
717
718
 
718
- const stream = await replClient.collectStream(2500)
719
-
720
- const decoded: PgOutputMessage[] = []
721
- for (const msg of stream) {
722
- if (msg.type === 0x64) {
723
- const result = decodeCopyData(new Uint8Array(msg.data))
724
- if (result) decoded.push(result)
719
+ // collect until we see both relations (with timeout)
720
+ const allDecoded: PgOutputMessage[] = []
721
+ const deadline = Date.now() + 8000
722
+ while (Date.now() < deadline) {
723
+ const stream = await replClient.collectStream(500)
724
+ for (const msg of stream) {
725
+ if (msg.type === 0x64) {
726
+ const result = decodeCopyData(new Uint8Array(msg.data))
727
+ if (result) allDecoded.push(result)
728
+ }
725
729
  }
730
+ const relations = allDecoded.filter(
731
+ (m) => m.type === 'Relation'
732
+ ) as RelationMessage[]
733
+ const tableNames = relations.map((r) => r.tableName)
734
+ if (tableNames.includes('items') && tableNames.includes('products')) break
726
735
  }
727
736
 
728
- const relations = decoded.filter((m) => m.type === 'Relation') as RelationMessage[]
737
+ const relations = allDecoded.filter((m) => m.type === 'Relation') as RelationMessage[]
729
738
  const tableNames = relations.map((r) => r.tableName)
730
739
  expect(tableNames).toContain('items')
731
- expect(tableNames).toContain('other')
740
+ expect(tableNames).toContain('products')
732
741
 
733
742
  replClient.close()
734
743
  }, 15_000)