-
Notifications
You must be signed in to change notification settings - Fork 56
fix(db): detect WSL2 and adjust pgserve startup timeout #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b4f64ed
9535b2b
74f967e
89b3fa2
59bbcf5
de1da2c
ad9772b
da47fbf
f493115
fc0de38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { join } from 'node:path'; | |
| import type postgres from 'postgres'; | ||
| import { runMigrations } from './db-migrations.js'; | ||
| import { needsSeed, runSeed } from './pg-seed.js'; | ||
| import { isWSL2 } from './wsl2-detect.js'; | ||
|
|
||
| /** | ||
| * Re-export Sql type for callers that need to annotate sql connection parameters. | ||
|
|
@@ -327,10 +328,16 @@ function findPgserveBin(): string { | |
| * Avoids the self-referencing proxy deadlock that occurs when the | ||
| * MultiTenantRouter Bun TCP proxy runs in the same event loop as | ||
| * the daemon that also connects to it. | ||
| * | ||
| * On WSL2, uses an extended timeout (30s) due to slower I/O performance. | ||
| */ | ||
| async function startPgserveOnPort(port: number): Promise<number> { | ||
| mkdirSync(DATA_DIR, { recursive: true }); | ||
|
|
||
| const isWsl = isWSL2(); | ||
| const timeoutMs = isWsl ? 30000 : 15000; | ||
| const timeoutSec = timeoutMs / 1000; | ||
|
|
||
|
Comment on lines
+337
to
+340
|
||
| const child = spawn( | ||
| findPgserveBin(), | ||
| [ | ||
|
|
@@ -352,7 +359,13 @@ async function startPgserveOnPort(port: number): Promise<number> { | |
| child.unref(); | ||
| pgserveChild = child; | ||
|
|
||
| const deadline = Date.now() + 15000; | ||
| const initialBootstrapMs = isWsl ? 2000 : 100; | ||
| await new Promise((r) => setTimeout(r, initialBootstrapMs)); | ||
|
|
||
|
Comment on lines
+362
to
+364
|
||
| const deadline = Date.now() + timeoutMs; | ||
| let retryDelayMs = 100; | ||
|
Comment on lines
+362
to
+366
|
||
| const maxRetryDelayMs = 1000; | ||
|
|
||
| while (Date.now() < deadline) { | ||
| if (await isPostgresHealthy(port)) { | ||
| activePort = port; | ||
|
|
@@ -361,15 +374,19 @@ async function startPgserveOnPort(port: number): Promise<number> { | |
| writeLockfile(port); | ||
| return port; | ||
| } | ||
| await new Promise((r) => setTimeout(r, 500)); | ||
|
|
||
| await new Promise((r) => setTimeout(r, retryDelayMs)); | ||
| retryDelayMs = Math.min(Math.floor(retryDelayMs * 1.5), maxRetryDelayMs); | ||
| } | ||
|
|
||
| try { | ||
| child.kill('SIGTERM'); | ||
| } catch { | ||
| /* dead */ | ||
| } | ||
| throw new Error(`pgserve failed to start on port ${port} (timeout after 15s)`); | ||
|
|
||
| process.env.GENIE_PG_AVAILABLE = 'false'; | ||
| throw new Error(`pgserve failed to start on port ${port} (timeout after ${timeoutSec}s)`); | ||
| } | ||
|
|
||
| /** Register process exit handler to clean up lockfile (once). */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,13 +1,30 @@ | ||||||
| /** | ||||||
| * Mailbox — Durable message store with unread/read semantics. | ||||||
| * | ||||||
| * ## Migration note (commit 44a153a3) | ||||||
| * | ||||||
| * Previously messages were written to `.genie/mailbox/<worker>.json` files and | ||||||
| * delivered by a polling loop that woke up every few seconds to check for new | ||||||
| * entries. This introduced latency proportional to the poll interval and made | ||||||
| * cross-process coordination fragile. | ||||||
| * | ||||||
| * The current implementation replaced file-based polling with PostgreSQL: | ||||||
| * - Messages are persisted to the `mailbox` table (durable, queryable). | ||||||
| * - A `AFTER INSERT` trigger fires `pg_notify('genie_mailbox_delivery', …)` | ||||||
|
||||||
| * - A `AFTER INSERT` trigger fires `pg_notify('genie_mailbox_delivery', …)` | |
| * - An `AFTER INSERT` trigger fires `pg_notify('genie_mailbox_delivery', …)` |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header comment claims “A 30-second fallback poll catches any notifications missed…”, but this module doesn’t implement a fallback poll (subscribeDelivery only registers LISTEN/NOTIFY). Either remove that statement or move/clarify it to wherever the fallback polling is actually implemented so the docs match the code.
| * - A 30-second fallback poll catches any notifications missed during | |
| * reconnects or daemon restarts. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /** | ||
| * WSL2 environment detection. | ||
| * | ||
| * Genie on WSL2 experiences slower pgserve startup due to I/O characteristics. | ||
| * This utility detects WSL2 and allows callers to adjust timeouts accordingly. | ||
| */ | ||
|
|
||
| import { readFileSync } from 'node:fs'; | ||
|
|
||
| let memoized: boolean | null = null; | ||
|
|
||
| /** | ||
| * Detect if running on WSL2 by checking /proc/version for "microsoft" or "wsl". | ||
| * Result is memoized for performance. | ||
| * | ||
| * @returns true if running on WSL2, false otherwise | ||
| */ | ||
| export function isWSL2(): boolean { | ||
| // Return memoized result if already computed | ||
| if (memoized !== null) return memoized; | ||
|
|
||
| try { | ||
| const procVersion = readFileSync('/proc/version', 'utf-8'); | ||
| memoized = procVersion.toLowerCase().includes('microsoft') || procVersion.toLowerCase().includes('wsl'); | ||
| return memoized; | ||
|
Comment on lines
+12
to
+25
|
||
| } catch { | ||
| // /proc/version not readable (non-Linux or permission issue) — assume not WSL2 | ||
| memoized = false; | ||
| return false; | ||
| } | ||
|
Comment on lines
+22
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For improved readability and slightly better performance, you can refactor this block. Calling toLowerCase() once is more efficient, and the catch block can be made more concise. try {
const procVersion = readFileSync('/proc/version', 'utf-8').toLowerCase();
memoized = procVersion.includes('microsoft') || procVersion.includes('wsl');
return memoized;
} catch {
// /proc/version not readable (non-Linux or permission issue) — assume not WSL2
return (memoized = false);
} |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Increasing daemon-side startup timeout to 30s on WSL2 here creates a mismatch with the CLI path in
_ensurePgserve()that still gives up after 16s (const deadline = Date.now() + 16000). On slower WSL2 setups, the daemon can now succeed between 16–30s, but the initiating CLI command will still throw a timeout error even though pgserve comes up shortly after, which makes normal command execution fail spuriously in the target environment.Useful? React with 👍 / 👎.