This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 235cb9be53 TIKA-5007: make JDBCEmitter thread-safe (#3444)
235cb9be53 is described below

commit 235cb9be532cd61093fbb862da2f10713c9e6b99
Author: Tim Allison <[email protected]>
AuthorDate: Wed Oct 28 09:03:46 2026 -0400

    TIKA-5007: make JDBCEmitter thread-safe (#3444)
---
 CHANGES.txt                                        |  4 ++
 .../tika/pipes/emitter/jdbc/JDBCEmitter.java       | 10 ++--
 .../tika/pipes/emitter/jdbc/JDBCEmitterTest.java   | 62 ++++++++++++++++++++++
 3 files changed, 72 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 6b8df9399d..7969560e69 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -22,6 +22,10 @@ Release 4.3.0 - ???
      for EKS IAM roles for service accounts (IRSA); the plugin now bundles the 
AWS STS
      module it needs (TIKA-5006).
 
+   * The JDBC emitter is thread-safe: concurrent emits (shared-server mode, 
several
+     async emitter threads) shared one connection and prepared statement 
unguarded,
+     mixing values across rows and committing each other's batches (TIKA-5007).
+
   OTHER CHANGES
 
    * PEM-armored CMS/PKCS7 (-----BEGIN PKCS7----- or -----BEGIN CMS-----) is 
parsed like
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitter.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitter.java
index 9b4ab0f411..1541755e04 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitter.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/main/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitter.java
@@ -75,8 +75,8 @@ import org.apache.tika.utils.TikaDates;
  * </pre>
  * <p>
  * This is only an initial, basic implementation of an emitter for JDBC.
- * It is currently NOT thread safe because of the shared prepared statement,
- * and depending on the jdbc implementation because of the shared connection.
+ * It is thread safe: emits share one connection and prepared statement, so 
they are
+ * serialized, one transaction at a time.
  * </p>
  */
 public class JDBCEmitter extends AbstractEmitter implements Closeable {
@@ -185,7 +185,9 @@ public class JDBCEmitter extends AbstractEmitter implements 
Closeable {
         }
     }
 
-    private void emitWithRetry(String emitKey, List<Metadata> metadataList) 
throws IOException {
+    // one lock for bind, batch, commit and reconnect: they share the 
connection and statement
+    private synchronized void emitWithRetry(String emitKey, List<Metadata> 
metadataList)
+            throws IOException {
         int tries = 0;
         Exception ex = null;
         while (tries++ <= config.maxRetries()) {
@@ -409,7 +411,7 @@ public class JDBCEmitter extends AbstractEmitter implements 
Closeable {
     }
 
     @Override
-    public void close() throws IOException {
+    public synchronized void close() throws IOException {
         try {
             if (insertStatement != null) {
                 insertStatement.close();
diff --git 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/test/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterTest.java
 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/test/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterTest.java
index 7dfcfaa452..1c9760f382 100644
--- 
a/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/test/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterTest.java
+++ 
b/tika-pipes/tika-pipes-plugins/tika-pipes-jdbc/src/test/java/org/apache/tika/pipes/emitter/jdbc/JDBCEmitterTest.java
@@ -37,6 +37,11 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TimeZone;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -397,6 +402,63 @@ public class JDBCEmitterTest {
         assertEquals(3, rows);
     }
 
+    /**
+     * Shared-server mode hands one emitter to every thread (TIKA-5007): 
concurrent emits must
+     * not mix one document's values into another's row or commit each other's 
batches.
+     */
+    @Test
+    public void testConcurrentEmits(@TempDir Path tmpDir) throws Exception {
+        String connectionString = "jdbc:h2:file:" + 
tmpDir.resolve("h2").toAbsolutePath();
+        LinkedHashMap<String, String> keys = new LinkedHashMap<>();
+        keys.put("k1", "string");
+        keys.put("k2", "int");
+        ObjectNode configNode = createConfigNode(connectionString,
+                "insert into test (path, k1, k2) values (?,?,?)",
+                "create table test (path varchar(64) primary key, k1 
varchar(64), k2 integer)",
+                null, "first_only", keys);
+        JDBCEmitter emitter = JDBCEmitter.build(new 
ExtensionConfig("test-jdbc", "jdbc-emitter",
+                MAPPER.writeValueAsString(configNode)));
+
+        int threads = 8;
+        int perThread = 200;
+        ExecutorService executor = Executors.newFixedThreadPool(threads);
+        CountDownLatch start = new CountDownLatch(1);
+        List<Future<?>> futures = new ArrayList<>();
+        try {
+            for (int t = 0; t < threads; t++) {
+                int base = t * perThread;
+                futures.add(executor.submit(() -> {
+                    start.await();
+                    for (int i = base; i < base + perThread; i++) {
+                        emitter.emit("id" + i, List.of(m("k1", "v" + i, "k2", 
Integer.toString(i))),
+                                new ParseContext());
+                    }
+                    return null;
+                }));
+            }
+            start.countDown();
+            for (Future<?> f : futures) {
+                f.get(2, TimeUnit.MINUTES);
+            }
+        } finally {
+            executor.shutdownNow();
+            emitter.close();
+        }
+
+        try (Connection connection = 
DriverManager.getConnection(connectionString);
+                Statement st = connection.createStatement();
+                ResultSet rs = st.executeQuery("select path, k1, k2 from 
test")) {
+            int rows = 0;
+            while (rs.next()) {
+                int k2 = rs.getInt(3);
+                assertEquals("id" + k2, rs.getString(1));
+                assertEquals("v" + k2, rs.getString(2));
+                rows++;
+            }
+            assertEquals(threads * perThread, rows);
+        }
+    }
+
     private Metadata m(String... strings) {
         Metadata metadata = new Metadata();
         for (int i = 0; i < strings.length; i++) {

Reply via email to