From 7f0614ac2617469fdca0d8db08dc979a6a44e73c Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 3 Mar 2026 14:42:53 +0000 Subject: [PATCH 1/3] Initial multi connections support --- README.md | 13 +- .../src/main/resources/application.properties | 2 +- .../pglite4j/jdbc/PgLiteDriver.java | 156 ++++++++++++++++-- .../pglite4j/jdbc/PgLiteDriverTest.java | 65 ++++++++ 4 files changed, 219 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 23c535d..467450c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ quarkus.datasource.jdbc.driver=io.roastedroot.pglite4j.jdbc.PgLiteDriver quarkus.datasource.username=postgres quarkus.datasource.password=password quarkus.datasource.jdbc.min-size=1 -quarkus.datasource.jdbc.max-size=1 +quarkus.datasource.jdbc.max-size=5 quarkus.devservices.enabled=false quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false @@ -82,18 +82,18 @@ spring.datasource.url=jdbc:pglite:memory:// spring.datasource.driver-class-name=io.roastedroot.pglite4j.jdbc.PgLiteDriver spring.datasource.username=postgres spring.datasource.password=password -spring.datasource.hikari.maximum-pool-size=1 +spring.datasource.hikari.maximum-pool-size=5 spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false ``` -### HikariCP - NOT TESTED +### HikariCP ```java HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:pglite:memory://"); -config.setMaximumPoolSize(1); +config.setMaximumPoolSize(5); DataSource ds = new HikariDataSource(config); ``` @@ -109,8 +109,9 @@ pglite4j/ ## Status and known limitations -- [ ] **Only `memory://` is supported** — no persistent / file-backed databases yet -- [ ] **Single connection only** — PGlite is single-threaded; connection pool max size must be 1 +- [x] ~~**Only `memory://` is supported**~~ — persistent / file-backed databases are not planned; the WASM backend uses an in-memory virtual filesystem (ZeroFS) with no disk I/O, which is fundamental to the architecture +- [x] ~~**Single connection only**~~ — multiple JDBC connections are now supported per database instance; requests are serialized through a single PGLite backend via a lock, so connection pools with `max-size > 1` work correctly (queries execute one at a time, not in parallel) +- [ ] **No connection isolation** — PostgreSQL runs in single-user mode with one session; all connections share the same session state (transactions, prepared statements, session variables). Queries are serialized, so there is no data corruption, but concurrent transactions are not isolated from each other. This is fine for connection pools that use connections sequentially (borrow, use, return). - [ ] **Limited extensions** — only `plpgsql` and `dict_snowball` are bundled; adding more requires rebuilding the WASM binary - [ ] **Startup time** — first connection has some overhead it can be optimized more - [ ] **Binary size** — the WASM binary + pgdata resources add several MBs to the classpath diff --git a/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties b/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties index b86e4c8..6b8c939 100644 --- a/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties +++ b/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties @@ -11,4 +11,4 @@ quarkus.hibernate-orm.database.generation=drop-and-create quarkus.devservices.enabled=false quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect -quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false +quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false \ No newline at end of file diff --git a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java index b950508..9b7d269 100644 --- a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java +++ b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; @@ -14,9 +13,13 @@ import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; public final class PgLiteDriver implements Driver { @@ -69,6 +72,7 @@ public Connection connect(String url, Properties info) throws SQLException { props.putIfAbsent("password", "password"); props.setProperty("sslmode", "disable"); props.setProperty("gssEncMode", "disable"); + props.putIfAbsent("connectTimeout", "60"); String pgUrl = "jdbc:postgresql://127.0.0.1:" + instance.getPort() + "/template1"; return new org.postgresql.Driver().connect(pgUrl, props); @@ -108,11 +112,15 @@ static final class ManagedInstance { private PGLite pgLite; private ServerSocket serverSocket; private volatile boolean running; + private final Object pgLock = new Object(); + private final AtomicInteger connectionCounter = new AtomicInteger(); + private final Set activeSockets = ConcurrentHashMap.newKeySet(); + private volatile List cachedStartupResponses; void boot() { pgLite = PGLite.builder().build(); try { - serverSocket = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); + serverSocket = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { pgLite.close(); throw new RuntimeException("Failed to create ServerSocket", e); @@ -131,40 +139,99 @@ private void acceptLoop() { while (running) { try { Socket socket = serverSocket.accept(); - handleConnection(socket); + Thread handler = + new Thread( + () -> handleConnection(socket), + "pglite-conn-" + connectionCounter.getAndIncrement()); + handler.setDaemon(true); + handler.start(); } catch (IOException e) { if (running) { - throw new UncheckedIOException( - "PgLiteDriver: accept error: " + e.getMessage(), e); + // log but don't crash the accept loop } } } } private void handleConnection(Socket socket) { + activeSockets.add(socket); try { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); byte[] buf = new byte[65536]; + String connName = Thread.currentThread().getName(); + System.err.println("[pglite4j] " + connName + " starting startup"); + handleStartup(in, out, buf); + System.err.println("[pglite4j] " + connName + " startup complete"); + + int msgCount = 0; while (running) { int n = in.read(buf); if (n <= 0) { + System.err.println( + "[pglite4j] " + + connName + + " read returned " + + n + + " (EOF), closing"); break; } byte[] message = Arrays.copyOf(buf, n); - byte[] response = pgLite.execProtocolRaw(message); + msgCount++; + // Try to extract SQL from message + String debugMsg = ""; + if (message.length > 5 && message[0] == 'Q') { + // Simple Query: Q + len(4) + query\0 + debugMsg = new String(message, 5, Math.min(message.length - 6, 200)); + } else if (message.length > 5 && message[0] == 'P') { + // Parse: P + len(4) + stmtName\0 + query\0 + ... + int nameEnd = 5; + while (nameEnd < message.length && message[nameEnd] != 0) { + nameEnd++; + } + if (nameEnd + 1 < message.length) { + int qStart = nameEnd + 1; + int qEnd = qStart; + while (qEnd < message.length && message[qEnd] != 0) { + qEnd++; + } + debugMsg = + "PARSE: " + + new String( + message, qStart, Math.min(qEnd - qStart, 200)); + } + } + System.err.println( + "[pglite4j] " + + connName + + " msg#" + + msgCount + + " len=" + + n + + " type=" + + (char) message[0] + + " " + + debugMsg); + byte[] response; + synchronized (pgLock) { + response = pgLite.execProtocolRaw(message); + } if (response.length > 0) { out.write(response); out.flush(); } } } catch (IOException e) { - if (running) { - throw new UncheckedIOException( - "PgLiteDriver: connection error: " + e.getMessage(), e); - } + // one connection failure must not crash other connections + System.err.println("[pglite4j] IOException in handleConnection: " + e); + e.printStackTrace(System.err); + } catch (RuntimeException e) { + // protect other connections from PGLite errors + System.err.println("[pglite4j] RuntimeException in handleConnection: " + e); + e.printStackTrace(System.err); } finally { + activeSockets.remove(socket); try { socket.close(); } catch (IOException e) { @@ -173,6 +240,68 @@ private void handleConnection(Socket socket) { } } + private void handleStartup(InputStream in, OutputStream out, byte[] buf) + throws IOException { + List cached = cachedStartupResponses; + if (cached != null) { + replayStartup(cached, in, out, buf); + return; + } + synchronized (pgLock) { + cached = cachedStartupResponses; + if (cached != null) { + replayStartup(cached, in, out, buf); + return; + } + List responses = new ArrayList<>(); + while (running) { + int n = in.read(buf); + if (n <= 0) { + throw new IOException("Connection closed during startup"); + } + byte[] message = Arrays.copyOf(buf, n); + byte[] response = pgLite.execProtocolRaw(message); + responses.add(response); + if (response.length > 0) { + out.write(response); + out.flush(); + } + if (endsWithReadyForQuery(response)) { + break; + } + } + cachedStartupResponses = responses; + } + } + + private static void replayStartup( + List cached, InputStream in, OutputStream out, byte[] buf) + throws IOException { + for (byte[] cachedResp : cached) { + int n = in.read(buf); + if (n <= 0) { + throw new IOException("Connection closed during startup replay"); + } + if (cachedResp.length > 0) { + out.write(cachedResp); + out.flush(); + } + } + } + + private static boolean endsWithReadyForQuery(byte[] response) { + // ReadyForQuery: type='Z' (0x5A), length=5 (00 00 00 05), status byte + if (response.length < 6) { + return false; + } + int off = response.length - 6; + return response[off] == 'Z' + && response[off + 1] == 0 + && response[off + 2] == 0 + && response[off + 3] == 0 + && response[off + 4] == 5; + } + void close() { running = false; try { @@ -180,6 +309,13 @@ void close() { } catch (IOException e) { // cleanup } + for (Socket s : activeSockets) { + try { + s.close(); + } catch (IOException e) { + // cleanup + } + } pgLite.close(); } } diff --git a/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java b/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java index 04a8264..c5006b3 100644 --- a/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java +++ b/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java @@ -158,4 +158,69 @@ void driverAcceptsUrl() throws SQLException { assertFalse(driver.acceptsURL("jdbc:postgresql://localhost/test")); assertFalse(driver.acceptsURL("jdbc:mysql://localhost/test")); } + + @Test + @Order(10) + void namedDatabasesAreIndependent() throws SQLException { + try (Connection db1 = DriverManager.getConnection("jdbc:pglite:memory:db1"); + Connection db2 = DriverManager.getConnection("jdbc:pglite:memory:db2")) { + try (Statement stmt = db1.createStatement()) { + stmt.execute("CREATE TABLE only_in_db1 (id INT)"); + } + // Verify db2 does not have the table created on db1 + try (Statement stmt = db2.createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT EXISTS (" + + "SELECT 1 FROM pg_class" + + " WHERE relname = 'only_in_db1'" + + " AND relkind = 'r')")) { + assertTrue(rs.next()); + assertFalse(rs.getBoolean(1)); + } + } + } + + @Test + @Order(11) + void multipleConnectionsSameDatabase() throws SQLException { + String url = "jdbc:pglite:memory:multiconn"; + try (Connection conn1 = DriverManager.getConnection(url); + Connection conn2 = DriverManager.getConnection(url)) { + try (Statement stmt = conn1.createStatement()) { + stmt.execute("CREATE TABLE shared_table (id INT, val TEXT)"); + stmt.executeUpdate("INSERT INTO shared_table VALUES (1, 'hello')"); + } + try (Statement stmt = conn2.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT val FROM shared_table WHERE id = 1")) { + assertTrue(rs.next()); + assertEquals("hello", rs.getString("val")); + } + } + } + + @Test + @Order(12) + void connectionCloseDoesNotAffectOther() throws SQLException { + String url = "jdbc:pglite:memory:closetest"; + Connection conn1 = DriverManager.getConnection(url); + Connection conn2 = DriverManager.getConnection(url); + try { + try (Statement stmt = conn1.createStatement()) { + stmt.execute("CREATE TABLE survive_close (id INT)"); + stmt.executeUpdate("INSERT INTO survive_close VALUES (42)"); + } + conn1.close(); + + try (Statement stmt = conn2.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id FROM survive_close")) { + assertTrue(rs.next()); + assertEquals(42, rs.getInt(1)); + } + } finally { + if (!conn2.isClosed()) { + conn2.close(); + } + } + } } From 0c2c1bc6d9b854d68b922f00cf4de893803baa87 Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 4 Mar 2026 12:58:28 +0000 Subject: [PATCH 2/3] Support multiple concurrent JDBC connections --- ERROR_RECOVERY_PLAN.md | 74 +++++++++++ README.md | 7 +- core/pom.xml | 6 +- .../io/roastedroot/pglite4j/core/PGLite.java | 18 ++- .../roastedroot/pglite4j/core/PGLiteTest.java | 67 ++++++++++ .../pglite4j/core/PgWireCodec.java | 122 ++++++++++++++++++ .../pglite4j/jdbc/PgLiteDriver.java | 3 + .../pglite-wasm/interactive_one.c.diff | 14 ++ .../src-backend-utils-error-elog.c.diff | 91 +++++-------- 9 files changed, 338 insertions(+), 64 deletions(-) create mode 100644 ERROR_RECOVERY_PLAN.md create mode 100644 wasm-build/patches/pglite-wasm/interactive_one.c.diff diff --git a/ERROR_RECOVERY_PLAN.md b/ERROR_RECOVERY_PLAN.md new file mode 100644 index 0000000..e0badda --- /dev/null +++ b/ERROR_RECOVERY_PLAN.md @@ -0,0 +1,74 @@ +# PGLite4j Error Recovery Plan + +## The Issue + +Any SQL error (e.g. `SELECT * FROM nonexistent_table`) kills the WASM instance permanently. + +**Root cause chain:** +1. PostgreSQL hits an error → `ereport(ERROR)` → `errfinish()` → `PG_RE_THROW()` +2. In normal PG, `PG_RE_THROW()` does `siglongjmp()` back to the error handler +3. In WASI build, `siglongjmp` is not available, so the elog.c patch replaces it with `abort()` +4. `abort()` → `proc_exit(134)` → `__builtin_unreachable()` → WASM TrapException +5. The Chicory WASM instance is dead after a trap — no further queries can run + +**Immediate symptom:** Flyway sends `SELECT rolname FROM pg_roles WHERE rolname ILIKE 'rds_superuser'` (Amazon RDS detection). The `pg_roles` view doesn't exist in pglite's catalog, causing an ERROR that traps the instance. All subsequent JDBC connections fail with "This connection has been closed". + +## The Fix — Imported Error Handler + Error Flag Check + +Instead of `abort()` → `proc_exit()` → trap, we: + +1. **C side (elog.c patch):** Replace `abort()` with a call to a custom WASM-imported function `pgl_on_error()` followed by `__builtin_unreachable()`: + - `pgl_on_error()` is provided by Java at instance creation (same pattern as sqlite4j callbacks) + - The imported function sets an error flag on the Java side and returns + - `__builtin_unreachable()` still causes a trap, but `proc_exit` is never called + +2. **Java side (PGLite.java):** + - Provide the `pgl_on_error` import function (following the sqlite4j `WasmDBImports` pattern) + - In `execProtocolRaw()`, catch RuntimeException around `interactiveOne()` + - When caught AND errorFlag is set: + - Call `exports.clearError()` — this does `EmitErrorReport()`, `AbortCurrentTransaction()`, `FlushErrorState()`, and sets `send_ready_for_query = true` + - Call `exports.interactiveWrite(-1)` — signals error state via `cma_rsize < 0` + - Call `exports.interactiveOne()` — enters `resume_on_error` → `wire_flush` path, sends `ReadyForQuery`, flushes output to CMA + - Collect response (ErrorResponse + ReadyForQuery) + - Reset error flag + - The instance survives and is reusable for subsequent queries + +### Key C code reference + +`clear_error()` in `interactive_one.c:228-280` does the full PostgreSQL error cleanup. +`resume_on_error:` at `interactive_one.c:649` does wire flush + ReadyForQuery. + +## Steps + +### Step 1 — Write a failing test + +Write a Java test in the pglite4j `core` module that: +- Creates a PGLite instance +- Sends a valid query (e.g. `SELECT 1`) — should succeed +- Sends an invalid query (e.g. `SELECT * FROM pg_roles`) — currently traps/crashes +- Sends another valid query (e.g. `SELECT 2`) — should succeed if recovery works +- Asserts the instance is still usable after the error + +This test captures the exact failure mode and will pass once the fix is in place. + +### Step 2 — C side: replace `abort()` with imported error handler + +In `wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff`: +- Declare `extern void pgl_on_error(void);` (WASM import) +- Replace `abort();` with `pgl_on_error(); __builtin_unreachable();` +- Rebuild the WASM binary + +### Step 3 — Java side: provide the import and recover from errors + +In `PGLite.java`: +- Add `volatile boolean errorFlag` field +- Add `pgl_on_error` as a HostFunction import (sets errorFlag = true, returns) +- Follow the sqlite4j pattern: build imports with both `wasi.toHostFunctions()` and the custom import +- In `execProtocolRaw()`: catch trap, check flag, call `clearError()` + `interactiveWrite(-1)` + `interactiveOne()`, collect response + +### Step 4 — Verify Flyway quickstarts + +Re-run the 3 blocked Quarkus quickstart tests: +- quartz-quickstart +- hibernate-orm-multi-tenancy-schema-quickstart +- hibernate-orm-multi-tenancy-database-quickstart diff --git a/README.md b/README.md index 467450c..07e4e5d 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,12 @@ pglite4j/ - [x] ~~**Only `memory://` is supported**~~ — persistent / file-backed databases are not planned; the WASM backend uses an in-memory virtual filesystem (ZeroFS) with no disk I/O, which is fundamental to the architecture - [x] ~~**Single connection only**~~ — multiple JDBC connections are now supported per database instance; requests are serialized through a single PGLite backend via a lock, so connection pools with `max-size > 1` work correctly (queries execute one at a time, not in parallel) -- [ ] **No connection isolation** — PostgreSQL runs in single-user mode with one session; all connections share the same session state (transactions, prepared statements, session variables). Queries are serialized, so there is no data corruption, but concurrent transactions are not isolated from each other. This is fine for connection pools that use connections sequentially (borrow, use, return). +- [x] ~~**Error recovery**~~ — both simple and extended query protocol errors are handled correctly; PostgreSQL errors trap the WASM instance and are caught by the Java side, which resets the backend state and drains stale protocol buffers so subsequent queries work cleanly +- [ ] **No connection isolation** — PostgreSQL runs in single-user mode with one session; all connections share the same session state (transactions, session variables). Queries are serialized, so there is no data corruption, but concurrent transactions are not isolated from each other. This is fine for connection pools that use connections sequentially (borrow, use, return). +- [ ] **Server-side prepared statements disabled** — because all connections share a single PostgreSQL backend, named prepared statements (`S_1`, `S_2`, …) would collide across connections. The driver sets `prepareThreshold=0` so pgjdbc always uses the unnamed prepared statement. This has no functional impact but means PostgreSQL cannot cache query plans across executions. - [ ] **Limited extensions** — only `plpgsql` and `dict_snowball` are bundled; adding more requires rebuilding the WASM binary -- [ ] **Startup time** — first connection has some overhead it can be optimized more +- [ ] **Startup time** — first connection has some overhead that can be optimized further - [ ] **Binary size** — the WASM binary + pgdata resources add several MBs to the classpath -- [ ] **Error recovery** — `clear_error()` integration for automatic transaction recovery is not yet wired up ### CMA (Contiguous Memory Allocator) diff --git a/core/pom.xml b/core/pom.xml index 31264c3..26ea990 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -71,8 +71,8 @@ io.roastedroot.pglite4j.core.PGLiteModule ../wasm-build/output/pglite.wasi - - + WARN + diff --git a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java index b813d4f..e05f676 100644 --- a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java +++ b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java @@ -115,7 +115,23 @@ public byte[] execProtocolRaw(byte[] message) { for (int tick = 0; tick < 256; tick++) { boolean producedBefore = collectReply(replies); - exports.interactiveOne(); + try { + exports.interactiveOne(); + } catch (RuntimeException e) { + if (exports.pglCheckError() != 0) { + // PostgreSQL hit an ERROR (e.g. relation not found). + // pgl_on_error() set the WASM-side flag and the + // instance trapped via __builtin_unreachable(). + // Recover: clean up PG error state and flush the + // ErrorResponse + ReadyForQuery back through the wire. + exports.clearError(); + exports.interactiveWrite(-1); + exports.interactiveOne(); + collectReply(replies); + break; + } + throw e; + } boolean producedAfter = collectReply(replies); if (!producedBefore && !producedAfter) { break; diff --git a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java index 233d942..bc13307 100644 --- a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java +++ b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java @@ -68,6 +68,37 @@ public void createTableAndInsert() { } } + @Test + public void errorRecovery() { + try (PGLite pg = PGLite.builder().build()) { + doHandshake(pg); + + // 1. Valid query should succeed + byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;")); + assertNotNull(r1); + String data1 = PgWireCodec.parseDataRows(r1); + System.out.println("Before error: SELECT 1 => " + data1); + assertTrue(data1.contains("1")); + + // 2. Invalid query — references a non-existent table. + // Without error recovery this traps the WASM instance permanently. + byte[] r2 = + pg.execProtocolRaw( + PgWireCodec.queryMessage("SELECT * FROM nonexistent_table_xyz;")); + assertNotNull(r2); + System.out.println("Error response length: " + r2.length + " bytes"); + // The response should contain an ErrorResponse (tag 'E') + assertTrue(r2.length > 0, "Expected non-empty error response"); + + // 3. Valid query should still work after the error + byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;")); + assertNotNull(r3); + String data3 = PgWireCodec.parseDataRows(r3); + System.out.println("After error: SELECT 2 => " + data3); + assertTrue(data3.contains("2"), "Instance should be reusable after SQL error"); + } + } + @Test public void cmaBufferOverflow() { try (PGLite pg = PGLite.builder().build()) { @@ -102,6 +133,42 @@ public void cmaBufferOverflow() { } } + @Test + public void extendedProtocolErrorRecovery() { + try (PGLite pg = PGLite.builder().build()) { + doHandshake(pg); + + // 1. Valid simple query first + byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;")); + assertNotNull(r1); + String data1 = PgWireCodec.parseDataRows(r1); + System.out.println("Extended proto test: SELECT 1 => " + data1); + assertTrue(data1.contains("1")); + + // 2. Extended protocol batch (Parse+Bind+Describe+Execute+Sync) + // for a query that will fail (nonexistent table). + // This reproduces the Flyway hang: pgjdbc uses extended protocol + // and the error recovery must send ReadyForQuery. + byte[] batch = PgWireCodec.extendedQueryBatch("SELECT * FROM nonexistent_table_xyz"); + byte[] r2 = pg.execProtocolRaw(batch); + assertNotNull(r2); + System.out.println("Extended proto error response: " + r2.length + " bytes"); + assertTrue(r2.length > 0, "Expected non-empty error response"); + assertTrue( + PgWireCodec.hasReadyForQuery(r2), + "Expected ReadyForQuery after extended protocol error"); + + // 3. Valid query should still work (verifies no buffer corruption) + byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;")); + assertNotNull(r3); + String data3 = PgWireCodec.parseDataRows(r3); + System.out.println("After extended proto error: SELECT 2 => " + data3); + assertTrue( + data3.contains("2"), + "Instance should be reusable after extended protocol error"); + } + } + static void doHandshake(PGLite pg) { byte[] startup = PgWireCodec.startupMessage("postgres", "template1"); byte[] resp1 = pg.execProtocolRaw(startup); diff --git a/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java b/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java index c64378d..60410f2 100644 --- a/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java +++ b/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java @@ -185,6 +185,128 @@ static String parseDataRows(byte[] data) { return sb.toString(); } + // === Extended Query Protocol === + + /** Build a Parse ('P') message. */ + static byte[] parseMessage(String stmtName, String sql) { + byte[] nameBytes = (stmtName + "\0").getBytes(StandardCharsets.UTF_8); + byte[] sqlBytes = (sql + "\0").getBytes(StandardCharsets.UTF_8); + int bodyLen = nameBytes.length + sqlBytes.length + 2; // 2 for numParamTypes + byte[] msg = new byte[1 + 4 + bodyLen]; + msg[0] = 'P'; + int len = 4 + bodyLen; + msg[1] = (byte) (len >> 24); + msg[2] = (byte) (len >> 16); + msg[3] = (byte) (len >> 8); + msg[4] = (byte) len; + int pos = 5; + System.arraycopy(nameBytes, 0, msg, pos, nameBytes.length); + pos += nameBytes.length; + System.arraycopy(sqlBytes, 0, msg, pos, sqlBytes.length); + pos += sqlBytes.length; + msg[pos] = 0; + msg[pos + 1] = 0; // 0 parameter types + return msg; + } + + /** Build a Bind ('B') message with no parameters. */ + static byte[] bindMessage(String portal, String stmtName) { + byte[] portalBytes = (portal + "\0").getBytes(StandardCharsets.UTF_8); + byte[] nameBytes = (stmtName + "\0").getBytes(StandardCharsets.UTF_8); + int bodyLen = portalBytes.length + nameBytes.length + 2 + 2 + 2; + byte[] msg = new byte[1 + 4 + bodyLen]; + msg[0] = 'B'; + int len = 4 + bodyLen; + msg[1] = (byte) (len >> 24); + msg[2] = (byte) (len >> 16); + msg[3] = (byte) (len >> 8); + msg[4] = (byte) len; + int pos = 5; + System.arraycopy(portalBytes, 0, msg, pos, portalBytes.length); + pos += portalBytes.length; + System.arraycopy(nameBytes, 0, msg, pos, nameBytes.length); + pos += nameBytes.length; + // 0 format codes, 0 parameters, 0 result format codes + msg[pos] = 0; + msg[pos + 1] = 0; + pos += 2; + msg[pos] = 0; + msg[pos + 1] = 0; + pos += 2; + msg[pos] = 0; + msg[pos + 1] = 0; + return msg; + } + + /** Build a Describe ('D') message. */ + static byte[] describeMessage(char type, String name) { + byte[] nameBytes = (name + "\0").getBytes(StandardCharsets.UTF_8); + int bodyLen = 1 + nameBytes.length; + byte[] msg = new byte[1 + 4 + bodyLen]; + msg[0] = 'D'; + int len = 4 + bodyLen; + msg[1] = (byte) (len >> 24); + msg[2] = (byte) (len >> 16); + msg[3] = (byte) (len >> 8); + msg[4] = (byte) len; + msg[5] = (byte) type; + System.arraycopy(nameBytes, 0, msg, 6, nameBytes.length); + return msg; + } + + /** Build an Execute ('E') message. */ + static byte[] executeMessage(String portal, int maxRows) { + byte[] portalBytes = (portal + "\0").getBytes(StandardCharsets.UTF_8); + int bodyLen = portalBytes.length + 4; + byte[] msg = new byte[1 + 4 + bodyLen]; + msg[0] = 'E'; + int len = 4 + bodyLen; + msg[1] = (byte) (len >> 24); + msg[2] = (byte) (len >> 16); + msg[3] = (byte) (len >> 8); + msg[4] = (byte) len; + int pos = 5; + System.arraycopy(portalBytes, 0, msg, pos, portalBytes.length); + pos += portalBytes.length; + msg[pos] = (byte) (maxRows >> 24); + msg[pos + 1] = (byte) (maxRows >> 16); + msg[pos + 2] = (byte) (maxRows >> 8); + msg[pos + 3] = (byte) maxRows; + return msg; + } + + /** Build a Sync ('S') message. */ + static byte[] syncMessage() { + return new byte[] {'S', 0, 0, 0, 4}; + } + + /** Build a complete extended query batch: Parse + Bind + Describe + Execute + Sync. */ + static byte[] extendedQueryBatch(String sql) { + byte[] parse = parseMessage("", sql); + byte[] bind = bindMessage("", ""); + byte[] describe = describeMessage('S', ""); + byte[] execute = executeMessage("", 0); + byte[] sync = syncMessage(); + byte[] batch = + new byte + [parse.length + + bind.length + + describe.length + + execute.length + + sync.length]; + int pos = 0; + System.arraycopy(parse, 0, batch, pos, parse.length); + pos += parse.length; + System.arraycopy(bind, 0, batch, pos, bind.length); + pos += bind.length; + System.arraycopy(describe, 0, batch, pos, describe.length); + pos += describe.length; + System.arraycopy(execute, 0, batch, pos, execute.length); + pos += execute.length; + System.arraycopy(sync, 0, batch, pos, sync.length); + return batch; + } + private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { diff --git a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java index 9b7d269..57c9627 100644 --- a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java +++ b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java @@ -72,6 +72,9 @@ public Connection connect(String url, Properties info) throws SQLException { props.putIfAbsent("password", "password"); props.setProperty("sslmode", "disable"); props.setProperty("gssEncMode", "disable"); + // All connections share one PG backend — named prepared statements + // would collide (S_1, S_2, ...). Force unnamed statements only. + props.setProperty("prepareThreshold", "0"); props.putIfAbsent("connectTimeout", "60"); String pgUrl = "jdbc:postgresql://127.0.0.1:" + instance.getPort() + "/template1"; diff --git a/wasm-build/patches/pglite-wasm/interactive_one.c.diff b/wasm-build/patches/pglite-wasm/interactive_one.c.diff new file mode 100644 index 0000000..8501eaa --- /dev/null +++ b/wasm-build/patches/pglite-wasm/interactive_one.c.diff @@ -0,0 +1,14 @@ +--- a/interactive_one.c ++++ b/interactive_one.c +@@ -646,6 +646,11 @@ + } + } + resume_on_error: ++ ignore_till_sync = false; ++ send_ready_for_query = true; ++ /* pglite: drain stale receive buffer left from the errored batch ++ * so the next pq_startmsgread() resets cleanly to new CMA data */ ++ while (pq_buffer_remaining_data() > 0) pq_getbyte(); + if (!is_repl) { + wire_flush: + if (!ClientAuthInProgress) { diff --git a/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff b/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff index 36bdaad..fc98382 100644 --- a/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff +++ b/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff @@ -1,37 +1,36 @@ ---- REL_17_5_WASM/src/backend/utils/error/elog.c -+++ REL_17_5_WASM-pglite/src/backend/utils/error/elog.c -@@ -348,12 +348,16 @@ - ErrorData *edata; - bool output_to_server; - bool output_to_client = false; -+#if defined(__EMSCRIPTEN__) || defined(__wasi__) -+# warning "FIXME: error levels" -+#else - int i; - - /* - * Check some cases in which we want to promote an error into a more - * severe error. None of this logic applies for non-error messages. - */ +--- a/src/backend/utils/error/elog.c ++++ b/src/backend/utils/error/elog.c +@@ -85,6 +85,25 @@ + #include "utils/ps_status.h" + #include "utils/varlena.h" + ++#if defined(__wasi__) ++static volatile int pgl_error_flag = 0; ++ ++/* Called from errfinish() instead of abort() on PG ERROR. ++ Sets flag so the host runtime can detect and recover. */ ++static void pgl_on_error(void) { ++ pgl_error_flag = 1; ++} + - if (elevel >= ERROR) - { - /* -@@ -394,7 +398,7 @@ - for (i = 0; i <= errordata_stack_depth; i++) - elevel = Max(elevel, errordata[i].elevel); - } -- ++/* Exported: returns 1 if a PG error trapped, resets the flag. ++ Called by Java after catching a TrapException. */ ++__attribute__((export_name("pgl_check_error"))) ++int pgl_check_error(void) { ++ int val = pgl_error_flag; ++ pgl_error_flag = 0; ++ return val; ++} +#endif - /* - * Now decide whether we need to process this report at all; if it's - * warning or less and not enabled for logging, just return false without -@@ -539,7 +543,12 @@ - */ - ++ + /* In this module, access gettext() via err_gettext() */ + #undef _ + #define _(x) err_gettext(x) +@@ -546,7 +565,16 @@ recursion_depth--; -+#if defined(__wasi__) -+ fprintf(stderr, "# 547: PG_RE_THROW(ERROR : %d) ignored\r\n", recursion_depth); + #if defined(__wasi__) + fprintf(stderr, "# 547: PG_RE_THROW(ERROR : %d) ignored\r\n", recursion_depth); +- abort(); + if (errordata_stack_depth >= 0) { + ErrorData *__edata = &errordata[errordata_stack_depth]; + fprintf(stderr, "# PG ERROR [%s:%d] %s: %s\r\n", @@ -40,30 +39,8 @@ + __edata->funcname ? __edata->funcname : "?", + __edata->message ? __edata->message : "(no message)"); + } -+ abort(); -+#else ++ pgl_on_error(); ++ __builtin_unreachable(); + #else PG_RE_THROW(); -+#endif - } - - /* Emit the message to the right places */ -@@ -587,7 +596,11 @@ - * FATAL termination. The postmaster may or may not consider this - * worthy of panic, depending on which subprocess returns it. - */ -+#if defined(__EMSCRIPTEN__) || defined(__wasi__) -+ puts("# 600: proc_exit(FATAL) ignored\r\n"); -+#else - proc_exit(1); -+#endif - } - - if (elevel >= PANIC) -@@ -697,6 +710,7 @@ - */ - if (edata->elevel >= ERROR) - { -+puts("#712"); - errfinish(filename, lineno, funcname); - pg_unreachable(); - } + #endif From fd559a88ffbb4b4dee6afeedfa9a7d5cc5cfcc19 Mon Sep 17 00:00:00 2001 From: andreatp Date: Wed, 4 Mar 2026 13:43:24 +0000 Subject: [PATCH 3/3] initial review --- ERROR_RECOVERY_PLAN.md | 74 ------------------- .../io/roastedroot/pglite4j/core/PGLite.java | 1 - .../roastedroot/pglite4j/core/PGLiteTest.java | 41 +--------- .../pglite4j/jdbc/PgLiteDriver.java | 53 +------------ 4 files changed, 4 insertions(+), 165 deletions(-) delete mode 100644 ERROR_RECOVERY_PLAN.md diff --git a/ERROR_RECOVERY_PLAN.md b/ERROR_RECOVERY_PLAN.md deleted file mode 100644 index e0badda..0000000 --- a/ERROR_RECOVERY_PLAN.md +++ /dev/null @@ -1,74 +0,0 @@ -# PGLite4j Error Recovery Plan - -## The Issue - -Any SQL error (e.g. `SELECT * FROM nonexistent_table`) kills the WASM instance permanently. - -**Root cause chain:** -1. PostgreSQL hits an error → `ereport(ERROR)` → `errfinish()` → `PG_RE_THROW()` -2. In normal PG, `PG_RE_THROW()` does `siglongjmp()` back to the error handler -3. In WASI build, `siglongjmp` is not available, so the elog.c patch replaces it with `abort()` -4. `abort()` → `proc_exit(134)` → `__builtin_unreachable()` → WASM TrapException -5. The Chicory WASM instance is dead after a trap — no further queries can run - -**Immediate symptom:** Flyway sends `SELECT rolname FROM pg_roles WHERE rolname ILIKE 'rds_superuser'` (Amazon RDS detection). The `pg_roles` view doesn't exist in pglite's catalog, causing an ERROR that traps the instance. All subsequent JDBC connections fail with "This connection has been closed". - -## The Fix — Imported Error Handler + Error Flag Check - -Instead of `abort()` → `proc_exit()` → trap, we: - -1. **C side (elog.c patch):** Replace `abort()` with a call to a custom WASM-imported function `pgl_on_error()` followed by `__builtin_unreachable()`: - - `pgl_on_error()` is provided by Java at instance creation (same pattern as sqlite4j callbacks) - - The imported function sets an error flag on the Java side and returns - - `__builtin_unreachable()` still causes a trap, but `proc_exit` is never called - -2. **Java side (PGLite.java):** - - Provide the `pgl_on_error` import function (following the sqlite4j `WasmDBImports` pattern) - - In `execProtocolRaw()`, catch RuntimeException around `interactiveOne()` - - When caught AND errorFlag is set: - - Call `exports.clearError()` — this does `EmitErrorReport()`, `AbortCurrentTransaction()`, `FlushErrorState()`, and sets `send_ready_for_query = true` - - Call `exports.interactiveWrite(-1)` — signals error state via `cma_rsize < 0` - - Call `exports.interactiveOne()` — enters `resume_on_error` → `wire_flush` path, sends `ReadyForQuery`, flushes output to CMA - - Collect response (ErrorResponse + ReadyForQuery) - - Reset error flag - - The instance survives and is reusable for subsequent queries - -### Key C code reference - -`clear_error()` in `interactive_one.c:228-280` does the full PostgreSQL error cleanup. -`resume_on_error:` at `interactive_one.c:649` does wire flush + ReadyForQuery. - -## Steps - -### Step 1 — Write a failing test - -Write a Java test in the pglite4j `core` module that: -- Creates a PGLite instance -- Sends a valid query (e.g. `SELECT 1`) — should succeed -- Sends an invalid query (e.g. `SELECT * FROM pg_roles`) — currently traps/crashes -- Sends another valid query (e.g. `SELECT 2`) — should succeed if recovery works -- Asserts the instance is still usable after the error - -This test captures the exact failure mode and will pass once the fix is in place. - -### Step 2 — C side: replace `abort()` with imported error handler - -In `wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff`: -- Declare `extern void pgl_on_error(void);` (WASM import) -- Replace `abort();` with `pgl_on_error(); __builtin_unreachable();` -- Rebuild the WASM binary - -### Step 3 — Java side: provide the import and recover from errors - -In `PGLite.java`: -- Add `volatile boolean errorFlag` field -- Add `pgl_on_error` as a HostFunction import (sets errorFlag = true, returns) -- Follow the sqlite4j pattern: build imports with both `wasi.toHostFunctions()` and the custom import -- In `execProtocolRaw()`: catch trap, check flag, call `clearError()` + `interactiveWrite(-1)` + `interactiveOne()`, collect response - -### Step 4 — Verify Flyway quickstarts - -Re-run the 3 blocked Quarkus quickstart tests: -- quartz-quickstart -- hibernate-orm-multi-tenancy-schema-quickstart -- hibernate-orm-multi-tenancy-database-quickstart diff --git a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java index e05f676..103dd3b 100644 --- a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java +++ b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java @@ -95,7 +95,6 @@ private PGLite() { int channel = exports.getChannel(); this.bufferAddr = exports.getBufferAddr(channel); - // System.err.println("PGLite: channel=" + channel + " bufferAddr=" + bufferAddr); } catch (IOException e) { throw new RuntimeException("Failed to initialize PGLite", e); } diff --git a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java index bc13307..93c2727 100644 --- a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java +++ b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java @@ -17,7 +17,6 @@ public void selectOne() { assertTrue(result.length > 0); String data = PgWireCodec.parseDataRows(result); - System.out.println("SELECT 1 => " + data); assertTrue(data.contains("1")); } } @@ -27,11 +26,9 @@ public void handshake() { try (PGLite pg = PGLite.builder().build()) { doHandshake(pg); - // After handshake, queries should work byte[] result = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 42 AS answer;")); assertNotNull(result); String data = PgWireCodec.parseDataRows(result); - System.out.println("After handshake: SELECT 42 => " + data); assertTrue(data.contains("42")); } } @@ -41,29 +38,24 @@ public void createTableAndInsert() { try (PGLite pg = PGLite.builder().build()) { doHandshake(pg); - // DDL via simple query protocol byte[] r1 = pg.execProtocolRaw( PgWireCodec.queryMessage("CREATE TABLE test (id INTEGER, name TEXT);")); - System.out.println("CREATE TABLE: " + r1.length + " bytes"); + assertNotNull(r1); - // SERIAL column byte[] r2 = pg.execProtocolRaw( PgWireCodec.queryMessage( "CREATE TABLE test_serial (id SERIAL PRIMARY KEY, val TEXT);")); - System.out.println("CREATE TABLE SERIAL: " + r2.length + " bytes"); + assertNotNull(r2); - // INSERT byte[] r3 = pg.execProtocolRaw( PgWireCodec.queryMessage("INSERT INTO test VALUES (1, 'hello');")); - System.out.println("INSERT: " + r3.length + " bytes"); + assertNotNull(r3); - // SELECT byte[] r4 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT * FROM test;")); String data = PgWireCodec.parseDataRows(r4); - System.out.println("SELECT: " + data); assertTrue(data.contains("hello")); } } @@ -73,28 +65,20 @@ public void errorRecovery() { try (PGLite pg = PGLite.builder().build()) { doHandshake(pg); - // 1. Valid query should succeed byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;")); assertNotNull(r1); String data1 = PgWireCodec.parseDataRows(r1); - System.out.println("Before error: SELECT 1 => " + data1); assertTrue(data1.contains("1")); - // 2. Invalid query — references a non-existent table. - // Without error recovery this traps the WASM instance permanently. byte[] r2 = pg.execProtocolRaw( PgWireCodec.queryMessage("SELECT * FROM nonexistent_table_xyz;")); assertNotNull(r2); - System.out.println("Error response length: " + r2.length + " bytes"); - // The response should contain an ErrorResponse (tag 'E') assertTrue(r2.length > 0, "Expected non-empty error response"); - // 3. Valid query should still work after the error byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;")); assertNotNull(r3); String data3 = PgWireCodec.parseDataRows(r3); - System.out.println("After error: SELECT 2 => " + data3); assertTrue(data3.contains("2"), "Instance should be reusable after SQL error"); } } @@ -105,19 +89,11 @@ public void cmaBufferOverflow() { doHandshake(pg); int bufSize = pg.getBufferSize(); - System.out.println( - "CMA buffer size: " + bufSize + " bytes (" + (bufSize / 1024) + " KB)"); - - // Generate a wire protocol response that exceeds the CMA buffer. - // repeat('x', N) returns an N-byte string in the DataRow message. int repeatLen = bufSize + 1000; String sql = "SELECT repeat('x', " + repeatLen + ");"; - System.out.println("Query: SELECT repeat('x', " + repeatLen + ")"); byte[] result = pg.execProtocolRaw(PgWireCodec.queryMessage(sql)); - System.out.println("Response length: " + result.length + " bytes"); assertNotNull(result); - // The response must contain the full string + wire protocol overhead assertTrue( result.length > repeatLen, "Expected response > " + repeatLen + " but got " + result.length); @@ -125,10 +101,8 @@ public void cmaBufferOverflow() { PgWireCodec.hasReadyForQuery(result), "Expected ReadyForQuery in overflow response"); - // Verify a normal query still works after the overflow byte[] r2 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 42;")); String data = PgWireCodec.parseDataRows(r2); - System.out.println("Post-overflow query: SELECT 42 => " + data); assertTrue(data.contains("42"), "Normal query should work after CMA overflow"); } } @@ -138,31 +112,22 @@ public void extendedProtocolErrorRecovery() { try (PGLite pg = PGLite.builder().build()) { doHandshake(pg); - // 1. Valid simple query first byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;")); assertNotNull(r1); String data1 = PgWireCodec.parseDataRows(r1); - System.out.println("Extended proto test: SELECT 1 => " + data1); assertTrue(data1.contains("1")); - // 2. Extended protocol batch (Parse+Bind+Describe+Execute+Sync) - // for a query that will fail (nonexistent table). - // This reproduces the Flyway hang: pgjdbc uses extended protocol - // and the error recovery must send ReadyForQuery. byte[] batch = PgWireCodec.extendedQueryBatch("SELECT * FROM nonexistent_table_xyz"); byte[] r2 = pg.execProtocolRaw(batch); assertNotNull(r2); - System.out.println("Extended proto error response: " + r2.length + " bytes"); assertTrue(r2.length > 0, "Expected non-empty error response"); assertTrue( PgWireCodec.hasReadyForQuery(r2), "Expected ReadyForQuery after extended protocol error"); - // 3. Valid query should still work (verifies no buffer corruption) byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;")); assertNotNull(r3); String data3 = PgWireCodec.parseDataRows(r3); - System.out.println("After extended proto error: SELECT 2 => " + data3); assertTrue( data3.contains("2"), "Instance should be reusable after extended protocol error"); diff --git a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java index 57c9627..6401a56 100644 --- a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java +++ b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java @@ -163,59 +163,14 @@ private void handleConnection(Socket socket) { OutputStream out = socket.getOutputStream(); byte[] buf = new byte[65536]; - String connName = Thread.currentThread().getName(); - System.err.println("[pglite4j] " + connName + " starting startup"); handleStartup(in, out, buf); - System.err.println("[pglite4j] " + connName + " startup complete"); - int msgCount = 0; while (running) { int n = in.read(buf); if (n <= 0) { - System.err.println( - "[pglite4j] " - + connName - + " read returned " - + n - + " (EOF), closing"); break; } byte[] message = Arrays.copyOf(buf, n); - msgCount++; - // Try to extract SQL from message - String debugMsg = ""; - if (message.length > 5 && message[0] == 'Q') { - // Simple Query: Q + len(4) + query\0 - debugMsg = new String(message, 5, Math.min(message.length - 6, 200)); - } else if (message.length > 5 && message[0] == 'P') { - // Parse: P + len(4) + stmtName\0 + query\0 + ... - int nameEnd = 5; - while (nameEnd < message.length && message[nameEnd] != 0) { - nameEnd++; - } - if (nameEnd + 1 < message.length) { - int qStart = nameEnd + 1; - int qEnd = qStart; - while (qEnd < message.length && message[qEnd] != 0) { - qEnd++; - } - debugMsg = - "PARSE: " - + new String( - message, qStart, Math.min(qEnd - qStart, 200)); - } - } - System.err.println( - "[pglite4j] " - + connName - + " msg#" - + msgCount - + " len=" - + n - + " type=" - + (char) message[0] - + " " - + debugMsg); byte[] response; synchronized (pgLock) { response = pgLite.execProtocolRaw(message); @@ -225,14 +180,8 @@ private void handleConnection(Socket socket) { out.flush(); } } - } catch (IOException e) { + } catch (IOException | RuntimeException e) { // one connection failure must not crash other connections - System.err.println("[pglite4j] IOException in handleConnection: " + e); - e.printStackTrace(System.err); - } catch (RuntimeException e) { - // protect other connections from PGLite errors - System.err.println("[pglite4j] RuntimeException in handleConnection: " + e); - e.printStackTrace(System.err); } finally { activeSockets.remove(socket); try {