Skip to content

[#708] Fix OutOfMemory during replication initialize with JDBC backend#711

Open
vharseko wants to merge 3 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/jdbc-cursor-708
Open

[#708] Fix OutOfMemory during replication initialize with JDBC backend#711
vharseko wants to merge 3 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/jdbc-cursor-708

Conversation

@vharseko

@vharseko vharseko commented Jul 5, 2026

Copy link
Copy Markdown
Member

Fixes #708

Problem

Initializing a large replica (~40M entries) with the JDBC/PostgreSQL backend fails with OutOfMemoryError during the index initialization phase, with all indexes stuck at 0% complete and PostgreSQL transferring huge amounts of data.

Root cause: Storage.CursorImpl opened every tree cursor as a scrollable ResultSet over the whole table (select h,k,v from ... order by k). The postgres driver streams rows only for TYPE_FORWARD_ONLY statements with a fetch size — scrollable result sets are always materialized entirely in client memory. Import phase two opens such a cursor over id2entry (MostlyOrderedChunk.flip()ImporterToChunkAdapter.flip()), so the driver tried to buffer all 40M entries in the heap. The giant query also monopolized the single connection shared by all phase-two index writers, which is why every index reported 0% complete. The same materialization happened for any cursor in normal operation (e.g. unindexed searches), and no configuration workaround exists (defaultRowFetchSize is ignored for scrollable result sets).

Fix

  • CursorImpl now iterates with keyset-paginated forward-only batches (where k>? order by k + offset ? rows fetch next ? rows only, limit ?,? for mysql), fully draining and closing each batch statement. Cursor memory is O(batch) instead of O(tree). Batch size is configurable via org.openidentityplatform.opendj.jdbc.fetchsize (default 1000).
  • Cursor positioning is resolved server-side: positionToKey is a point lookup on (h,k), positionToKeyOrNextwhere k>=?, positionToLastKeyorder by k desc, positionToIndexoffset. Semantics mirror the JE backend cursor (failed positioning leaves the cursor undefined).
  • delete() issues a regular delete where h=? and k=? instead of CONCUR_UPDATABLE/deleteRow().
  • For postgres, openTree creates create index if not exists k_<hash> on <table> (k): the (h,k) primary key cannot serve order by k, so without it every batch would be a full-table top-N sort. Existing deployments get the index on next backend open.
  • For mysql, the old k tinyblob dialect cannot serve the keyset batches at all: the optimizer refuses prefix indexes (k(255)) for order by k and even for the range predicate — the plan stays full scan + filesort (verified on mysql:9.2). The dialect now uses k varbinary(255) (same 255-byte cap) with primary key(h,k), and openTree creates the full k_<hash> index through a metadata check, since mysql has no create index if not exists. The batch query then runs as an index range scan without filesort, same as postgres. Tables of the previous tinyblob dialect are not migrated and must be re-created.

Tests

PgSqlTestCase 33/33, MySqlTestCase 33/33, EncryptedTestCase (jdbc) 32/32 — including testExportLDIFAndImportLDIF and testRebuildAllIndex/testRebuildDegradedIndex, which exercise the exact import phase-two and ID2EntrySource cursor paths that previously OOMed, plus the new testCursorCrossesFetchSizeBatches: a cursor with fetchsize=2 over 7 keys checks iteration order across batch boundaries, positionToKey/positionToKeyOrNext/positionToIndex/positionToLastKey, delete() through the cursor with continued iteration, and the read-only guard. Verified in postgres and mysql that k_* indexes are created on all trees and serve the keyset batches with index scans.

Bundled test-infra fixes so the classes also pass in one TestNG fork (98/98) and over a dirty database: test_issue_496 accepts attributeOrValueExists when another backend test class already added the schema definitions to the shared in-JVM server, and the jdbc TestCase drops stale opendj_% tables before setUp — PgSql and Encrypted map the same tree names to the same tables, and leftovers encrypted with a lost cipher key broke the next run's cleanup.

…IdentityPlatform#708)

The JDBC storage cursor fetched the whole tree with a scrollable
ResultSet ("select h,k,v ... order by k"): the postgres/mysql drivers
materialize such result sets entirely in client memory, so opening a
cursor over id2entry during import phase two pulled all entries into
the heap and stalled every index writer sharing the connection.

Replace the scrollable ResultSet with keyset-paginated batches
("where k>? order by k" + offset/fetch, configurable via
org.openidentityplatform.opendj.jdbc.fetchsize), resolve cursor
positioning with server-side queries, and create a supporting index
on (k) for postgres so ordered batches use an index scan instead of
a full sort.
vharseko added 2 commits July 6, 2026 10:00
MySQL cannot use a prefix index over "k tinyblob" for the cursor's
"where k>? order by k" batches (the plan stays full scan + filesort),
so the dialect now uses k varbinary(255) (same 255-byte cap) with
primary key(h,k), and openTree creates a full k_<hash> index via a
metadata check, since MySQL has no "create index if not exists".

Also cover cursor positioning across fetchsize batches with a test on
all JDBC dialects.
…e database

- test_issue_496 accepts attributeOrValueExists: the shared in-JVM
  server already has the schema definitions when another backend test
  class ran first
- jdbc TestCase drops stale opendj_% tables before setUp: PgSql and
  Encrypted map the same tree names to the same tables, and leftovers
  encrypted with a lost cipher key broke the next run's cleanup
@vharseko vharseko changed the title Fix OutOfMemory during replication initialize with JDBC backend [#708] Fix OutOfMemory during replication initialize with JDBC backend Jul 6, 2026
return "h char(128),k raw(2000),v blob,primary key(h,k)";
}else if (((CachedConnection) con).parent.getClass().getName().contains("mysql")) {
return "h char(128),k tinyblob,v longblob,primary key(h(128),k(255))";
return "h char(128),k varbinary(255),v longblob,primary key(h,k)";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the migration from tinyblob to varbinary(255) fo MySQL could potentially cause data loss

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OutOfMemory error initializing replication using jdbc postgres backend

2 participants