Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/chdb-node-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ jobs:
# Provider conformance for the S3 backend. Separate from the job above so a
# flaky container cannot redden the engine-free suite, and because this is the
# only automated check that conditional create and compare-and-swap work
# against a real object store rather than a filesystem. MinIO needs no
# credentials, so it runs on every PR; AWS S3 and R2 stay manual.
# against a real object store rather than a filesystem. The local server
# needs no external credentials, so it runs on every PR; AWS S3 and R2 stay manual.
durable-s3:
runs-on: ubuntu-latest
steps:
Expand All @@ -68,13 +68,15 @@ jobs:
node-version: 22.x
cache: npm
- run: npm install --ignore-scripts
- name: Start MinIO
- name: Start MinIO-compatible Silo
# Not a `services:` container: GitHub offers no way to pass a command to
# one, and this image needs `server /data` to start at all.
# Official MinIO images are no longer anonymously pullable from Quay or
# Docker Hub. Pin a tested MinIO-compatible Silo release and its digest.
run: |
docker run -d --name minio -p 9000:9000 \
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
quay.io/minio/minio:latest server /data
docker.io/pgsty/silo:RELEASE.2026-09-16T00-00-00Z@sha256:635197cb9f36d01bee221d34d1c7d7960f6a95c48b0b6c01d99cd13bdae51a46 server /data
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:9000/minio/health/live >/dev/null; then
echo "MinIO ready after ${i}s"; exit 0
Expand Down
17 changes: 14 additions & 3 deletions lib/chdb_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,18 +214,26 @@ static void hard_close_all() {
g_reg.boundKey.clear();
}

// Node's environment cleanup closes connections, but the engine may still own
// background threads when C++ static destructors run. On macOS with core
// v26.9.0, a CLI that queried once could hang indefinitely inside exit().
// Run this process-wide backstop before the engine's own exit handlers.
static void shutdown_on_process_exit() {
hard_close_all();
(void)chdb_shutdown();
}

static void ensure_atexit() {
static bool done = false;
if (!done) {
std::atexit(hard_close_all);
std::atexit(shutdown_on_process_exit);
done = true;
}
}

// Default connection for standalone query()/queryBind() (lazy, in-memory). A
// single shared default per process; reused across standalone calls.
static chdb_connection *get_default_conn(char **error_message) {
ensure_atexit();
std::lock_guard<std::mutex> lk(g_reg_mu);
if (g_reg.defaultConn) return g_reg.defaultConn;
// No default up. If a session holds a real data directory, "" cannot bind a
Expand All @@ -245,6 +253,9 @@ static chdb_connection *get_default_conn(char **error_message) {
g_reg.defaultConn = c;
g_reg.boundKey = "";
g_reg.conns.insert(c);
// Register only after chdb_connect initialized the engine: atexit handlers
// run in reverse registration order, so shutdown must precede its teardown.
ensure_atexit();
return c;
}

Expand All @@ -253,7 +264,6 @@ static chdb_connection *get_default_conn(char **error_message) {
static chdb_connection *acquire_session_conn(const std::string &path,
const std::vector<std::string> &extraArgs,
char **error_message) {
ensure_atexit();
std::lock_guard<std::mutex> lk(g_reg_mu);
if (!g_reg.conns.empty()) {
if (g_reg.boundKey.empty()) {
Expand Down Expand Up @@ -300,6 +310,7 @@ static chdb_connection *acquire_session_conn(const std::string &path,
}
g_reg.boundKey = path;
g_reg.conns.insert(c);
ensure_atexit();
return c;
}

Expand Down
24 changes: 24 additions & 0 deletions test/v3/engine-exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { spawnSync } from 'node:child_process'
import { join } from 'node:path'

describe('engine process teardown (chdb-node#97)', () => {
it('exits after an async query on the published engine', () => {
const entry = join(process.cwd(), 'index.js')
const script = `
const { queryBindAsync } = require(${JSON.stringify(entry)});
queryBindAsync('SELECT 42', {}, { format: 'CSV' }).then(
result => { process.stdout.write(result.text()); process.exit(0); },
error => { console.error(error); process.exit(1); }
);
`
const child = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
timeout: 15_000,
})
expect(child.error).toBeUndefined()
expect(child.status).toBe(0)
expect(child.stdout.trim()).toBe('42')
expect(child.stderr).toBe('')
}, 20_000)
})
3 changes: 2 additions & 1 deletion test/v3/layer3/codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ describe('CLI — end-to-end', () => {
const r = spawnSync(
process.execPath,
['dist/layer3/codegen/cli.js', '--from-file', csv, '--format', 'CSVWithNames', '--label', 'events'],
{ cwd: process.cwd(), encoding: 'utf-8' },
{ cwd: process.cwd(), encoding: 'utf-8', timeout: 15_000 },
)
expect(r.error).toBeUndefined()
expect(r.status).toBe(0)
expect(r.stdout).toMatch(/export interface Database \{/)
expect(r.stdout).toMatch(/events: \{[\s\S]*'Nullable\(Int64\)'/)
Expand Down
Loading