Skip to content

JDBC backend: the importer shares one JDBC connection across every import thread #891

Description

@vharseko

Importer is thread-safe by contract, and JDBCStorage.ImporterImpl is not: it borrows one java.sql.Connection in its constructor and every import thread writes through that single connection and the single transaction wrapping it.

Surfaced while reviewing #884; it does not belong to that PR.

The contract

// opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Importer.java:26-32
/**
 * Allows to run an import. For performance reasons, imports are run without transactions.
 * <p>
 * Since import is multi threaded, implementations must be thread-safe.
 */
@ThreadSafe
public interface Importer extends Closeable

What is shared

// opendj-server-legacy/src/main/java/org/opends/server/backends/jdbc/JDBCStorage.java:1482-1520
private final class ImporterImpl implements Importer {
    final Connection con;
    final ReadableTransactionImpl txr;
    final WriteableTransactionTransactionImpl txw;
    ...
    public ImporterImpl() {
        ...
        con = getConnection();
        txr = new ReadableTransactionImpl(con);
        txw = new WriteableTransactionTransactionImpl(con);
    }

One connection, one ReadableTransactionImpl, one WriteableTransactionTransactionImpl - and with it one StampSession (:1051), which opens a second connection lazily (:407) with no synchronization of its own.

Who calls it, and from how many threads

Phase two is unconditionally parallel. invokeParallel runs one thread per chunk:

// opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java:1522-1526
private static <V> List<V> invokeParallel(String threadNameTemplate, Collection<Callable<V>> tasks)
    throws InterruptedException, ExecutionException
{
  final ExecutorService executor = Executors.newCachedThreadPool(newThreadFactory(null, threadNameTemplate, true));

called at :1286 with the phase-two tasks, each of which writes through the importer - ChunkCopierTask.call() (:2513-2519) copies a whole chunk with Importer.put, and DN2IDImporterTask and VLVIndexImporterTask do the same for their trees. Every tree of the backend is copied at once, through one connection.

Phase one clears the trees through it too. AbstractTwoPhaseImportStrategy.beforePhaseOne (:1346-1350) runs entryContainer.delete(asWriteableTransaction(importer)), and it is called from processEntry on the PHASE1-IMPORTER-* threads (:1248) - once per entry container, so concurrently when the backend holds more than one base DN.

Symptoms

  • Statements interleave on one connection. The drivers differ in how they take that - some synchronize, some do not - but none of them makes two threads sharing one connection into two independent writers.
  • One thread commits another thread's work. clearTree (JDBCStorage.java:1160-1163), deleteTree (:1170) and openTree all end in con.commit(), so a tree being cleared in phase one commits whatever else is in flight on that connection. ImporterImpl.close() is written as though its commit() were the one that decides whether the import is durable, and it is not.
  • The stamp connection can be opened twice. StampSession.connection() (:407-412) is a plain lazy if (con==null), so two threads reaching it together open two connections and one of them is never closed - it is not the field that close() will see.

How the others do it

PDBStorage.ImporterImpl keeps its per-thread state in a ThreadLocal, which is exactly what the contract asks for:

// opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java:271-280
private final class ImporterImpl implements Importer
{
  private final ThreadLocal<Map<TreeName, Exchange>> exchanges = new ThreadLocal<Map<TreeName, Exchange>>()

CASStorage.ImporterImpl shares one TransactionImpl, but the Cassandra CqlSession under it is thread-safe by design, so that one holds.

Suggested fix

Give each thread its own connection, borrowed on first use and returned in close() - a ThreadLocal of the transaction pair over a connection of the pool, the shape PDBStorage uses. The pool is bounded since #878, so the number of connections an import can take is bounded with it, and the phase-two thread count is what the bound has to accommodate.

The commit points want revisiting at the same time: with a connection per thread, close() has to commit each of them, not just its own.

Environment

master (5.2.x), all four JDBC dialects, import-ldif and rebuild-index. The line numbers are of master as it stands.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions