Skip to content

ENGKNOW-3657: Reduce Table Service dictionary memory (tunable TableCache + lazy content map)#129

Merged
gmagnu merged 25 commits into
mainfrom
ENGKNOW-3657-gor-table-service-memory-usage
Jul 9, 2026
Merged

ENGKNOW-3657: Reduce Table Service dictionary memory (tunable TableCache + lazy content map)#129
gmagnu merged 25 commits into
mainfrom
ENGKNOW-3657-gor-table-service-memory-usage

Conversation

@gmagnu

@gmagnu gmagnu commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

ENGKNOW-3657: Reduce Table Service dictionary memory

GOR-library side of the Table Service select memory fix.

Problem

Under concurrent select load, Table Service heap grows ~2GB→10GB. Root cause: select re-parses a ~50MB / 300K-entry dictionary per call into its own DictionaryEntries (all entry objects + indices), so N concurrent selects = N full copies.

Changes in this PR

  • TableCache tunable + observable: max size / TTL from system properties gor.dictionary.cache.maxsize (default 500) and gor.dictionary.cache.ttl.hours (default 12); recordStats(); stats() / estimatedSize() / cleanUp() accessors. Caffeine promoted implementationapi in model so the public CacheStats return type resolves for subclasses.
  • Lazy content map in DictionaryEntries: contentHashToLines, activeTags, deletedEntriesCount are now built lazily (only when the mutation/stats paths need them), not eagerly at load. The select/filter read path uses only rawLines + tagHashToLines, so a select-only dictionary keeps a smaller footprint. updateContentMap() is idempotent and resets deletedEntriesCount to stay correct across the incremental mutation path.
  • Pre-existing commits on the branch: memory measurement harness (MemorySampler, DictionaryFixture, TableServiceLoadDriver, load-test entrypoints) and the design/plan docs for Step-0 profiling.

Tests

  • New UTestTableCache (tunable size + recorded stats) and UTestDictionaryEntriesLazy (read path does not build the content map; consumers do).
  • Existing org.gorpipe.gor.table.* suite: 169 pass, no regression. :gortools + :model test sources compile with the caffeine scope change.

Merge order (cross-repo)

This PR must merge and be released before the companion gdb-gor-services PR (which routes select through this cache and bumps GOR_VERSION).

🤖 Generated with Claude Code

gmagnu and others added 17 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>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Junit Tests - Summary

4 720 tests  +15   4 542 ✅ +14   18m 12s ⏱️ -15s
  483 suites +10     178 💤 + 1 
  483 files   +10       0 ❌ ± 0 

Results for commit 1e10cab. ± Comparison against base commit 41b0e4d.

♻️ This comment has been updated with latest results.

Comment thread test/src/main/java/org/gorpipe/test/memory/MemoryLoadConfig.java Dismissed
gmagnu and others added 8 commits July 8, 2026 20:21
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>
@gmagnu gmagnu merged commit 2701ddd into main Jul 9, 2026
14 checks passed
@gmagnu gmagnu deleted the ENGKNOW-3657-gor-table-service-memory-usage branch July 9, 2026 16:27
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.

2 participants