Skip to content

Disable LMDB value-cache eviction in new repo configs - #210

Open
tkuhn wants to merge 1 commit into
mainfrom
fix-lmdb-value-eviction
Open

Disable LMDB value-cache eviction in new repo configs#210
tkuhn wants to merge 1 commit into
mainfrom
fix-lmdb-value-eviction

Conversation

@tkuhn

@tkuhn tkuhn commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What

Set lmdb:valueEvictionInterval 0 in both repository-config templates in TripleStore.java, and plumb NANOPUB_QUERY_FETCHING_SOCKET_TIMEOUT through docker-compose.yml.

Why

With eviction enabled (rdf4j's 60s default), unused value IDs are freed and then reused; a stale reference afterwards dereferences to a different value. That is the value-ID-remap corruption behind eight events on the fleet between 2026-08-20 and 2026-08-31 — unrelated nanopubs sharing identifiers, nanopubs absorbed into others, literals surfacing in the wrong slots.

Setting the interval to 0 disables the pass. This is the workaround recommended upstream on 2026-08-31 while the underlying race is investigated.

The eviction pass carries the comment "this is also run after opening the database" in ValueStore.java. That explains something we had recorded repeatedly across those eight events without ever having a mechanism for it: every one of them followed a restart of the affected store. The trigger was the startup pass freeing and reusing IDs, not an unclean shutdown.

Cost is disk, not memory. Dead values are never reclaimed or reused, but gcIds() returns early when eviction is disabled, so nothing accumulates on the heap. Our stores are append-only apart from last30d pruning, so the disk cost is small.

Scope — this covers new shards only

Repositories that already exist keep the config.ttl they were created with. Each host needs a one-off sweep of its ~2,800 configs with rdf4j stopped (it re-serialises config.ttl on shutdown and would overwrite the edits). The sweep is independent of this release and does not depend on it. Script: kpxl-admin/scripts/disable-lmdb-value-eviction.sh.

Because a restart with eviction still enabled is itself the exposure, the sweep should run before this release is deployed, not after.

Second change: NANOPUB_QUERY_FETCHING_SOCKET_TIMEOUT

Utils.getHttpRequestConfig has always read this variable, but the query service declares an explicit environment: list and no env_file, so setting it in .env silently did nothing.

The 10s default is aggressive for a store that is slow rather than broken: the read raises QueryInterruptedException, the loader's while(!success) retry loops treat that as transient, and the retries add load, which makes the next read slower still. That loop produced ~17% HTTP 503 on federated /api on the OVH node on 2026-08-31 while status was READY (root cause there was a too-small heap; raising it cleared the 503s, but the amplification path remains).

The default is unchanged at 10000 — this only makes it reachable.

Testing

517 tests pass. The config-template change is covered by existing repo-creation tests; the compose change validates with docker compose config.

Follow-up

Revisit once the upstream race is fixed — eviction exists for a reason and we are trading disk for safety.

🤖 Generated with Claude Code

With eviction enabled (rdf4j's 60s default), unused value IDs are freed and
then REUSED; a stale reference afterwards dereferences to a different value.
That is the value-ID-remap corruption behind eight events on the fleet between
2026-08-20 and 2026-08-31 (eclipse-rdf4j/rdf4j#5970), where unrelated records
shared identifiers, records were absorbed into others, and literals appeared in
the wrong slots.

Setting valueEvictionInterval to 0 disables the pass and is the workaround
recommended upstream on 2026-08-31. It costs disk — dead values are never
reclaimed — but not memory: gcIds() returns early when eviction is off, so
nothing accumulates on the heap. Our stores are append-only apart from last30d
pruning, so the disk cost is small.

The eviction pass "is also run after opening the database" (ValueStore.java),
which finally explains why every one of those eight events followed a restart
of the affected store — something we had recorded repeatedly without a
mechanism.

This covers newly created shards only. Repositories that already exist keep the
config.ttl they were created with and need a one-off sweep with rdf4j stopped.

Also plumb NANOPUB_QUERY_FETCHING_SOCKET_TIMEOUT through docker-compose.yml.
Utils.getHttpRequestConfig has always read it, but the query service declares an
explicit environment list and no env_file, so setting it in .env did nothing.
The 10s default is aggressive for a store that is slow rather than broken: the
read raises QueryInterruptedException, the loader's retry loops treat that as
transient, and the retries add load. The default is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant