Skip to content

fix(ENGKNOW-3657): harden DictionaryEntries read accessors against clear() races#130

Open
gmagnu wants to merge 28 commits into
mainfrom
ENGKNOW-3657-gor-table-service-memory-usage
Open

fix(ENGKNOW-3657): harden DictionaryEntries read accessors against clear() races#130
gmagnu wants to merge 28 commits into
mainfrom
ENGKNOW-3657-gor-table-service-memory-usage

Conversation

@gmagnu

@gmagnu gmagnu commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes 3 concurrency holes in DictionaryEntries — the shared dictionary the table service caches across many concurrent selects.

# Hole Fix
1 getEntries() did two separate volatile reads (dataLoaded, then rawLines) — a racing clear() could make it return null (torn read). clear() looks safe because it's synchronized, but the unlocked fast-path reads are the trap. New private loadedEntries() reads rawLines into one local so the load check and the returned reference are the same read.
2 updateTagMap() dereferenced rawLines.size() with no !dataLoaded guard (unlike updateContentMap()), so getEntries(String...) could NPE if clear() slipped in after its getEntries() call. Added the same if (!dataLoaded) loadLinesAndUpdateIndices() guard.
3 getEntries() returned the backing ArrayList and getAllActiveTags() returned a live Multiset.elementSet() — callers could structurally modify shared internal state (elementSet() blocks add but allows remove/clear). Both now return unmodifiable views. Internal mutators (insert/delete) and internal reads switched to loadedEntries() so the wrap doesn't break .add/.remove.

Class javadoc updated to document the unmodifiable-view contract.

Tests

New UTestDictionaryEntriesViews (deterministic):

  • getEntries_returnsUnmodifiableList — red before fix, green after
  • getAllActiveTags_returnsUnmodifiableSet — asserts clear() on the returned set throws (the real hole)
  • insertAndDelete_stillMutateBackingList — regression for the loadedEntries() refactor
  • getEntriesByTag_reloadsAfterClearMinor fixes to README #2 functional guard

Existing UTestDictionaryEntriesConcurrency (16-thread stress) still passes. model dictionary + table suites green; gortools compiles.

🤖 Generated with Claude Code

gmagnu and others added 28 commits July 8, 2026 12:44
Diagnosis-first spec: local load harness + profiler to pinpoint the
dominant memory consumer behind table-service OOMs under load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the same exclusive-lock write path as the real Table Service so
concurrent writes serialize instead of racing on a shared temp file and
silently losing inserts. Log caught op errors; shutdownNow on await timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cache table instances so read-side DictionaryEntries stay retained for the
run (models a session/table cache), and measure post-GC retained-heap delta
over a baseline. Replaces the peak-churn metric that failed to scale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…driver

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
contentHashToLines, activeTags and deletedEntriesCount are only needed by
the mutation/stats paths, not by select/filter reads. Building them lazily
shrinks the long-lived cached dictionary copy for select-heavy workloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the memory-diagnosis design spec and implementation plan as local-only files; not part of the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the memory findings and profiling runbook as local-only files; not part of the PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getEntries()/isLoaded() read dataLoaded+rawLines without synchronization while a synchronized loader publishes them, so a concurrent reader could see dataLoaded==true with a stale rawLines (transient NPE/torn read). This is hit by concurrent selects sharing one cached GorDictionaryTable. Mark the lazily-published DictionaryEntries fields (and TableInfoBase.id) volatile so unsynchronized reads observe fully-constructed state; correct the class contract in the javadoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getTableAccessOptimizer() built the optimizer via an unsynchronized null-check on a non-volatile field, so concurrent callers on a shared table could build/publish multiple instances unsafely. Use double-checked locking on a volatile field so concurrent readers share one safely-published optimizer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
inferShouldBucketizeFromContent() lazily cached the content type in a non-volatile field with an unsynchronized null-check. Make the field volatile and compute via a local; the computation is idempotent so no lock is needed, and reads now observe a safely-published value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dening

Cover the getEntries()/getAllActiveTags() unmodifiable-view guarantees,
the updateTagMap() reload-after-clear guard, and that insert/delete still
mutate the backing list after switching to the internal loadedEntries()
accessor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Junit Tests - Summary

4 736 tests  +4   4 558 ✅ +5   19m 45s ⏱️ +51s
  488 suites +1     178 💤  - 1 
  488 files   +1       0 ❌ ±0 

Results for commit c1811ef. ± Comparison against base commit 38e0cbd.

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