From 3718834d1d9f01de899d9742ac3a1139e459c0b4 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 18:34:00 -0700 Subject: [PATCH 1/8] test: cover concurrent first-use of a fresh Connection (issue #24) Adds a regression test that fires two queries concurrently as the very first operation on a fresh in-memory Database + Connection, plus a concurrent init() test. Before the lazy-init race fix this crashes the process with SIGSEGV (exit 139); reproduced 2/6 mocha runs locally. --- test/test_concurrency.js | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/test_concurrency.js b/test/test_concurrency.js index 8b70776..3952ecc 100644 --- a/test/test_concurrency.js +++ b/test/test_concurrency.js @@ -74,3 +74,55 @@ describe("Concurrent query execution across multiple connections", function () { assert.isFalse(results[4][0]["a.isStudent"]); }); }); + +describe("Concurrent initialization on a fresh connection (issue #24)", function () { + it("should handle concurrent queries as the very first use without explicit init", async function () { + // Regression test for https://github.com/LadybugDB/ladybug-nodejs/issues/24. + // Each iteration uses a fresh in-memory database + connection and fires + // two queries concurrently as the first operation. Before the fix, the + // lazy-init race in Connection.init() crashed the process (SIGSEGV). + // bufferPoolSize 64 MB, maxDBSize 1 GB (the default maxDBSize asks for + // an 8 TB mmap, which is too large for some environments). + for (let i = 0; i < 50; i++) { + const memDb = new lbug.Database( + ":memory:", + 64 * 1024 * 1024, + true, + false, + 1024 * 1024 * 1024 + ); + const freshConn = new lbug.Connection(memDb); + const queryResults = await Promise.all([ + freshConn.query("RETURN 1 AS x"), + freshConn.query("RETURN 2 AS x"), + ]); + const rows = await Promise.all( + queryResults.map((queryResult) => queryResult.getAll()) + ); + assert.deepEqual(rows[0], [{ x: 1 }]); + assert.deepEqual(rows[1], [{ x: 2 }]); + for (const queryResult of queryResults) { + queryResult.close(); + } + await freshConn.close(); + await memDb.close(); + } + }); + + it("should handle concurrent init() calls on a fresh connection", async function () { + const memDb = new lbug.Database( + ":memory:", + 64 * 1024 * 1024, + true, + false, + 1024 * 1024 * 1024 + ); + const freshConn = new lbug.Connection(memDb); + await Promise.all([freshConn.init(), freshConn.init()]); + const queryResult = await freshConn.query("RETURN 1 AS x"); + assert.deepEqual(await queryResult.getAll(), [{ x: 1 }]); + queryResult.close(); + await freshConn.close(); + await memDb.close(); + }); +}); From f107aad9da4c6854fe5fce053911a439730a7c36 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 18:54:18 -0700 Subject: [PATCH 2/8] ci: install OpenSSL on Windows runners CMake requires OpenSSL 3 (CMakeLists.txt:519) but windows-latest has no dev files, so configure fails with Could NOT find OpenSSL and npm test then fails with Cannot find module '../build/'. Install Shining Light OpenSSL via Chocolatey and point CMake at it. --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e62f39..1c8265d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,13 @@ jobs: restore-keys: | ${{ runner.os }}-ccache-node- + - name: Install OpenSSL (Windows) + if: ${{ matrix.platform == 'win32' }} + shell: cmd + run: | + choco install openssl.light -y --no-progress + echo OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64>> %GITHUB_ENV% + - name: Build Node.js native module (Windows) if: ${{ matrix.platform == 'win32' }} shell: cmd From 58e346cd49f82f517c66ccb58d0fda433a63ff62 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 18:56:55 -0700 Subject: [PATCH 3/8] fix: share a single lazy init across concurrent Connection uses (issue #24) Connection.init() awaited _database._getDatabase() before assigning _initPromise, so concurrent first-uses each passed the null guard and started their own native initAsync on the same NodeConnection, crashing the process (SIGSEGV, exit 139). Cache the init promise synchronously via an async IIFE covering db fetch + native alloc + initAsync, and clear it after settle only if no newer init replaced it (preserves the _initPromise=null post-init contract and allows retry after failure). --- src_js/connection.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src_js/connection.js b/src_js/connection.js index 61f6ef4..8bd6be5 100644 --- a/src_js/connection.js +++ b/src_js/connection.js @@ -41,18 +41,23 @@ class Connection { if (this._isClosed) { throw new Error("Connection is closed."); } - if (!this._isInitialized) { - if (!this._initPromise) { + if (this._isInitialized) { + return; + } + if (!this._initPromise) { + // Cache the promise synchronously (no await before assignment) so + // concurrent init() calls share a single initialization instead of + // each starting their own (which crashed the process, see #24). + this._initPromise = (async () => { + const database = await this._database._getDatabase(); if (!this._connection) { - const database = await this._database._getDatabase(); this._connection = new LbugNative.NodeConnection(database); } - this._initPromise = new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { this._connection.initAsync((err) => { if (err) { reject(err); } else { - this._isInitialized = true; if (this._numThreads) { this._connection.setMaxNumThreadForExec(this._numThreads); } @@ -63,8 +68,21 @@ class Connection { } }); }); + this._isInitialized = true; + })(); + } + const initPromise = this._initPromise; + try { + await initPromise; + } catch (err) { + // Allow retry after a failed initialization (clear only if no + // newer init has started in the meantime). + if (this._initPromise === initPromise) { + this._initPromise = null; } - await this._initPromise; + throw err; + } + if (this._initPromise === initPromise) { this._initPromise = null; } } From 2c18435e9deb1d1e3c0fbbddaf1ad4413e6e8dd3 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 19:06:14 -0700 Subject: [PATCH 4/8] ci: fix OPENSSL_ROOT_DIR for openssl.light on Windows The Chocolatey openssl.light package installs to C:\Program Files\OpenSSL (per its install log), not C:\Program Files\OpenSSL-Win64, so CMake's FindOpenSSL still failed. Point OPENSSL_ROOT_DIR at the real location. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c8265d..6278fbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,7 @@ jobs: shell: cmd run: | choco install openssl.light -y --no-progress - echo OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64>> %GITHUB_ENV% + echo OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL>> %GITHUB_ENV% - name: Build Node.js native module (Windows) if: ${{ matrix.platform == 'win32' }} From 13583ef5c4ae2505546c8a57415bf64ad6a8ed05 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 19:08:46 -0700 Subject: [PATCH 5/8] ci: pass explicit OpenSSL lib paths on Windows FindOpenSSL detects the headers (3.6.4) under OPENSSL_ROOT_DIR but still misses OPENSSL_CRYPTO_LIBRARY in the Shining Light layout. Probe the install tree for libcrypto/libssl import libs and export them explicitly; also list all .lib files for diagnostics. --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6278fbc..26f7b6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,10 +108,17 @@ jobs: - name: Install OpenSSL (Windows) if: ${{ matrix.platform == 'win32' }} - shell: cmd + shell: pwsh run: | choco install openssl.light -y --no-progress - echo OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL>> %GITHUB_ENV% + "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV + # Shining Light layouts differ by version; locate the import libs + # explicitly so CMake's FindOpenSSL finds them. + $crypto = Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter 'libcrypto*.lib' | Select-Object -First 1 + $ssl = Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter 'libssl*.lib' | Select-Object -First 1 + if ($crypto) { "OPENSSL_CRYPTO_LIBRARY=$($crypto.FullName)" >> $env:GITHUB_ENV } + if ($ssl) { "OPENSSL_SSL_LIBRARY=$($ssl.FullName)" >> $env:GITHUB_ENV } + Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter '*.lib' | ForEach-Object { $_.FullName } - name: Build Node.js native module (Windows) if: ${{ matrix.platform == 'win32' }} From f256bd31dcaebf9e38c79cab4f7aa3340b3831b2 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 19:12:46 -0700 Subject: [PATCH 6/8] ci: stage Shining Light OpenSSL into a standard layout on Windows FindOpenSSL does not search lib/VC/x64/MD and ignores the OPENSSL_*_LIBRARY env vars, so copy the x64/MD import libs plus headers to D:\openssl (lib/ + include/) and point OPENSSL_ROOT_DIR there. Also put the OpenSSL bin dir on PATH for the test step's runtime DLLs. --- .github/workflows/ci.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26f7b6f..d79b4cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,14 +111,18 @@ jobs: shell: pwsh run: | choco install openssl.light -y --no-progress - "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> $env:GITHUB_ENV - # Shining Light layouts differ by version; locate the import libs - # explicitly so CMake's FindOpenSSL finds them. - $crypto = Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter 'libcrypto*.lib' | Select-Object -First 1 - $ssl = Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter 'libssl*.lib' | Select-Object -First 1 - if ($crypto) { "OPENSSL_CRYPTO_LIBRARY=$($crypto.FullName)" >> $env:GITHUB_ENV } - if ($ssl) { "OPENSSL_SSL_LIBRARY=$($ssl.FullName)" >> $env:GITHUB_ENV } - Get-ChildItem -Path 'C:\Program Files\OpenSSL' -Recurse -Filter '*.lib' | ForEach-Object { $_.FullName } + # Shining Light keeps the x64/MD import libs under lib/VC/x64/MD, + # which CMake's FindOpenSSL does not search (and it ignores the + # OPENSSL_*_LIBRARY env vars), so stage a standard layout in a + # space-free dir and point OPENSSL_ROOT_DIR at it. + New-Item -ItemType Directory -Force -Path D:\openssl\lib | Out-Null + Copy-Item -Recurse 'C:\Program Files\OpenSSL\include' D:\openssl\ + Copy-Item 'C:\Program Files\OpenSSL\lib\VC\x64\MD\libcrypto.lib' D:\openssl\lib\ + Copy-Item 'C:\Program Files\OpenSSL\lib\VC\x64\MD\libssl.lib' D:\openssl\lib\ + "OPENSSL_ROOT_DIR=D:\openssl" >> $env:GITHUB_ENV + # Runtime DLLs (libcrypto/libssl-3-x64.dll) for the test step. + 'C:\Program Files\OpenSSL\bin' >> $env:GITHUB_PATH + Get-ChildItem D:\openssl -Recurse | ForEach-Object { $_.FullName } - name: Build Node.js native module (Windows) if: ${{ matrix.platform == 'win32' }} From 0b1ff5871afd20a5ea56db74fe47f878fec538ec Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 20:12:34 -0700 Subject: [PATCH 7/8] ci: force MSVC compilers for the Windows build windows-2025 runners ship mingw64 in PATH, so CMake's Ninja compiler search picked GNU 15.2.0 over MSVC even after vcvars64. That broke the link two ways: GNU ar choked in BundleStaticLibrary.cmake, and MinGW ld missed ws2_32 (MSVC-only pragma comment lib). Pin CC/CXX=cl to match the main repo's VS-based Windows builds. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d79b4cf..9919ff1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,10 @@ jobs: shell: cmd env: EXTRA_CMAKE_FLAGS: -DLBUG_NODEJS_ENABLE_TEST_EXPORTS=ON + # windows-2025 images ship mingw64 in PATH, which CMake would + # otherwise prefer over MSVC (breaking ws2_32 linking and ar). + CC: cl + CXX: cl run: | powershell.exe -Command "Add-MpPreference -ExclusionPath '${{ github.workspace }}'" call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" From e680acdbf01ad31056a0ea2ae34e6ee313ed364c Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Fri, 25 Sep 2026 20:21:20 -0700 Subject: [PATCH 8/8] ci: use msvc-dev-cmd and fail-fast Windows build step The hardcoded VS2022 vcvars64.bat path does not exist on the windows-2025-vs2026 image, so cl never reached PATH and CMake failed with 'Could not find the compiler specified in CXX: cl'. Use ilammy/msvc-dev-cmd (as the main repo's ci-workflow does) to locate VS via vswhere. Also chain the build as 'make ... && ccache -s' so a make failure fails the step; previously the trailing 'ccache -s' (exit 0) masked build failures and the step stayed green while tests failed downstream with MODULE_NOT_FOUND. --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9919ff1..f43e7f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,12 @@ jobs: 'C:\Program Files\OpenSSL\bin' >> $env:GITHUB_PATH Get-ChildItem D:\openssl -Recurse | ForEach-Object { $_.FullName } + - name: Set up MSVC environment (Windows) + if: ${{ matrix.platform == 'win32' }} + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Build Node.js native module (Windows) if: ${{ matrix.platform == 'win32' }} shell: cmd @@ -131,14 +137,13 @@ jobs: EXTRA_CMAKE_FLAGS: -DLBUG_NODEJS_ENABLE_TEST_EXPORTS=ON # windows-2025 images ship mingw64 in PATH, which CMake would # otherwise prefer over MSVC (breaking ws2_32 linking and ar). + # (cl itself comes from msvc-dev-cmd above via vswhere.) CC: cl CXX: cl run: | powershell.exe -Command "Add-MpPreference -ExclusionPath '${{ github.workspace }}'" - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" - ccache -s - make GEN=Ninja nodejs ccache -s + make GEN=Ninja nodejs && ccache -s - name: Build Node.js native module (non-Windows) if: ${{ matrix.platform != 'win32' }}