Skip processLocalRelationshipUpdates when no relationships to write#617
Merged
ZihanLi58 merged 1 commit intoMay 6, 2026
Merged
Conversation
handleRelationshipIngestion is called once per aspect during create/update. For aspects with no RELATIONSHIP-typed fields, localRelationshipUpdates ends up empty after the .filter(entry -> !entry.getValue().isEmpty()) step. The existing code still calls _localRelationshipWriterDAO.processLocalRelationshipUpdates with that empty list — its body is a no-op for-loop, but the @transactional annotation on the method opens + commits a transaction per call regardless. On a slow MySQL host this manifests as ~150ms per aspect of pure transaction overhead (BEGIN + COMMIT round-trips) for no useful work. With N aspects per create, total wasted time scales linearly: e.g. 5-aspect MlModelInstance create observed at 867ms wall on prod-lor1, of which the inserted SQL is 1-2ms (per dao.benchmark.<entity>.create.aspects_5.latency). Fix: short-circuit when localRelationshipUpdates is empty, avoiding the no-op @transactional invocation entirely.
rakhiagr
approved these changes
May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
handleRelationshipIngestionis called once per aspect during create/update. For aspects with noRELATIONSHIP-typed fields,
localRelationshipUpdatesends up empty after the existing.filter(entry -> !entry.getValue().isEmpty())step. The existing code still calls_localRelationshipWriterDAO.processLocalRelationshipUpdateswith that empty list — its body is a no-opfor-loop, but the
@Transactionalannotation on the method opens + commits a transaction per callregardless.
On a slow MySQL host this manifests as ~150 ms per aspect of pure transaction overhead (BEGIN + COMMIT
round-trips) for no useful work. With N aspects per create, total wasted time scales linearly.
Evidence
A 5-aspect MlModelInstance/create on prod-lor1 (a LinkedIn fabric) was observed at 867 ms wall-clock, of
which the actual SQL INSERT is 1-2 ms (per
dao.benchmark.<entity>.create.aspects_5.latency). A wall-clockasync-profiler flame graph showed the rest of the time spent in:
BaseLocalDAO.create
→ createAspectsWithCallbacks
→ EbeanLocalDAO.createNewAssetWithAspects
→ handleRelationshipIngestion (called once per aspect)
→ EbeanLocalRelationshipWriterDAO.processLocalRelationshipUpdates [@transactional]
→ JdbcTransaction.commit → setAutoCommit → MySQL round-trip → __poll
Linear scaling confirmed by varying aspect count on the slow fabric:
Per-aspect cost ≈ 162 ms regardless of whether the aspect actually has any relationships to write. None of
the test payloads contained any populated relationship fields, yet the cost still appeared linearly with N
aspects.
Fix
One-line short-circuit: skip the call to
processLocalRelationshipUpdateswhen the list is empty, avoidingthe no-op
@Transactionalinvocation entirely. Behavior is identical for the non-empty case (the existingcode path is unchanged for aspects that do have relationships to write).
Testing Done
./gradlew :dao-impl:ebean-dao:compileJava)EbeanLocalDAOTestcover the non-empty path; my changepreserves identical behavior there. Local MariaDB4j-based test suite couldn't be run in my environment (test
infra issue unrelated to the change), but the change is observably equivalent for the non-empty case and
only avoids a no-op call in the empty case.
ms for the same payload (matching prod-ltx1 baseline).
Checklist