perf(ebean-dao): implement config-controlled multi-aspect batch read - #642
Conversation
24cff49 to
e2c2fcd
Compare
91b8360 to
5910b08
Compare
e2c2fcd to
d726dfb
Compare
5910b08 to
16b49ac
Compare
d726dfb to
f74070f
Compare
16b49ac to
73c2356
Compare
f74070f to
becabe0
Compare
| @Nonnull List<AspectKey<URN, ? extends RecordTemplate>> keys, int keysCount, int position, | ||
| boolean includeSoftDeleted, boolean isTestMode) { | ||
| throw new UnsupportedOperationException("batchGetUnionMultiAspect is not implemented yet"); | ||
| final List<EbeanMetadataAspect> result = |
There was a problem hiding this comment.
Just curious, which of the decorator we are using now? we have instrumental one and the usage tracking ones..
There was a problem hiding this comment.
It's host-configured and the two compose in EbeanLocalDAO — base EbeanLocalAccess, optionally wrapped by InstrumentedEbeanLocalAccess (setBenchmarkMetrics) and/or UsageTrackingEbeanLocalAccess (setUsageEmitter, one-shot, skipped for NoOpDaoUsageEmitter), in either order. So at runtime it can be none / either / both.
The important guarantee is that the new read behaves identically under any of those configs. I verified batchGetUnionMultiAspect has exact parity with batchGetUnion in both decorators:
- UsageTracking: delegates +
emitRead("batchGetUnionMultiAspect", ...)with the sameemissionEnabled() && !isTestMode && !DaoReadContext.isInternalRead()gating. - Instrumented:
instrument("batchGetUnionMultiAspect", ..., bucketCount(keys.size()), ...)— same pattern.
So usage events still fire and benchmark timings are still recorded for the multi-aspect path regardless of which decorator(s) are active — no coverage gap. No code change needed.
| final Class<ASPECT> aspectClass = (Class<ASPECT>) aspectKeys.get(index).getAspectClass(); | ||
| final String tableName = isTestMode ? getTestTableName(entityUrn) : getTableName(entityUrn); | ||
| final String columnName = getAspectColumnName(entityUrn.getEntityType(), aspectClass); | ||
| if (!validator.columnExists(tableName, columnName)) { |
There was a problem hiding this comment.
Is this check needed? or we can do it by check the sqlRow directly?
There was a problem hiding this comment.
Cleaned up — the previous version called validator.columnExists(...) twice (once when grouping keys, once again in the mapping pass). The rewrite computes it once during key collection; the mapping pass is now purely row-driven and reads columns straight off the SqlRow (skipping null cells via sqlRow.get(columnName) == null), so the redundant second check is gone. (cb39303)
| if (!validator.columnExists(tableName, columnName)) { | ||
| continue; | ||
| } | ||
| final SqlRow sqlRow = urnToRow.get(entityUrn.toString()); |
There was a problem hiding this comment.
sqlRow.getString("urn") might not match entityUrn.toString(), as we have canical urn stuff, you might want to make it case insensitive.
There was a problem hiding this comment.
check with @mridul111998 for more context on this
There was a problem hiding this comment.
Good catch — fixed. The old code did an exact-string urnToRow.get(entityUrn.toString()) lookup, which would silently drop an aspect if the stored urn differed from the requested urn by canonical/case. I reworked the mapping to be row-driven (like the per-aspect path): it now iterates the returned rows and matches each against the requested (urn, aspect) pairs via a case-insensitive key (matchKey lower-cases the urn). The returned aspect's urn is derived from the row itself, so it matches per-aspect output exactly. (cb39303)
There was a problem hiding this comment.
the fix looks clean.
not blocking: might worth noting that it relies on the urn column being case-insensitively collated so WHERE urn IN (...) returns case-variant rows for matchKey - mysql's default utf8mb4 collation is _ci, which is fine, if we ever need to use this with TiDB (default utf8mb4_bin, case sensitive), might need to explicitly pin ci collation
| public <ASPECT extends RecordTemplate> List<EbeanMetadataAspect> batchGetUnionMultiAspect( | ||
| @Nonnull List<AspectKey<URN, ? extends RecordTemplate>> aspectKeys, int keysCount, int position, | ||
| boolean includeSoftDeleted, boolean isTestMode) { | ||
| throw new UnsupportedOperationException("batchGetUnionMultiAspect is not implemented yet"); |
There was a problem hiding this comment.
Small edge-case: If a direct IEbeanLocalAccess caller passes duplicate (urn, aspect) keys, the multi-aspect path returns duplicates while the per-aspect path returns one result because it groups into Set. Not an issue for EbeanLocalDAO.get(...) if all callers go through the Set API, but worth either matching the per-aspect behavior or documenting/testing the expected direct-access behavior.
There was a problem hiding this comment.
Fixed as part of the same row-driven rewrite. Requested (urn, aspect) pairs now go into a requestedPairs Set, and the mapping loop uses requestedPairs.remove(...) — which both restricts output to requested pairs and collapses duplicate keys to a single result, matching the per-aspect path's Set<Urn> grouping. Added testBatchGetUnionMultiAspectDeduplicatesRepeatedKeys asserting parity when the same key is passed twice. (cb39303)
73c2356 to
9e8df27
Compare
becabe0 to
8b65bc6
Compare
9e8df27 to
9a983cd
Compare
c1cdb88 to
cb39303
Compare
9a983cd to
b01a751
Compare
cb39303 to
69f1d8a
Compare
b01a751 to
ae85ade
Compare
69f1d8a to
3dab7c8
Compare
… [META-24100] Add MultiAspectReadBenchmarkTest measuring the current per-aspect read (one SELECT per aspect) against the bundled multi-aspect read from #642 (a single SELECT per entity table) on identical seeded data, and report the query-count reduction and p50 latency speedup. Local in-process MariaDB result (73-aspect entity, 30 urns, 200 iters): current 73 SELECTs p50 27.431ms; multi-aspect 1 SELECT p50 1.193ms => 73x fewer round-trips, p50 latency 95.7% lower (~23x faster). Gated behind -Dgma.benchmark=true (skipped in CI). URN batch loaded from benchmark-urns.txt so it is editable without recompiling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ae85ade to
5ea7cfa
Compare
3dab7c8 to
5549f68
Compare
… [META-24100] Add MultiAspectReadBenchmarkTest measuring the current per-aspect read (one SELECT per aspect) against the bundled multi-aspect read from #642 (a single SELECT per entity table) on identical seeded data, and report the query-count reduction plus p50/p90/p99/max latency speedup. Local in-process MariaDB result (73-aspect entity, 30 urns, 200 iters): current 73 SELECTs p50 26.985ms p99 34.415ms; multi-aspect 1 SELECT p50 1.139ms p99 1.375ms => 73x fewer round-trips, p50 ~23.7x faster, p99 ~25x. Reuses the benchmark harness (urn resource, gma.benchmark gate) added by the baseline #640; only adds the incremental gma.benchmark.urnFile hook. Gated behind -Dgma.benchmark=true (skipped in CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fill in the multi-aspect read behavior on top of the API surface + routing: - SQLStatementUtils.createMultiAspectReadSql: build one bundled SELECT per entity table (row-level deleted_ts filter only) - EbeanLocalAccess.batchGetUnionMultiAspect: group keys by table, one read per table, map each (urn, aspect) key preserving per-aspect semantics - Instrumented/UsageTracking decorators: implement the stubbed override to delegate the new read (instrument + usage emission) - Update the stubbed surface tests (previously asserting UnsupportedOperation) to assert the real SQL builder output (using real production dataset URNs from the PR linkedin#640 read benchmark) and multi-aspect read behavior The AspectReadStrategy routing seam and stubbed surface already exist in the base PR; this PR only makes the MULTI_ASPECT/DUAL paths functional. Default strategy is PER_ASPECT, so behavior is unchanged unless opted in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5549f68 to
f544ad2
Compare
… [META-24100] Add MultiAspectReadBenchmarkTest measuring the current per-aspect read (one SELECT per aspect) against the bundled multi-aspect read from linkedin#642 (a single SELECT per entity table) on identical seeded data, and report the query-count reduction plus p50/p90/p99/max latency speedup. Local in-process MariaDB result (73-aspect entity, 30 urns, 200 iters): current 73 SELECTs p50 26.985ms p99 34.415ms; multi-aspect 1 SELECT p50 1.139ms p99 1.375ms => 73x fewer round-trips, p50 ~23.7x faster, p99 ~25x. Reuses the benchmark harness (urn resource, gma.benchmark gate) added by the baseline linkedin#640; only adds the incremental gma.benchmark.urnFile hook. Gated behind -Dgma.benchmark=true (skipped in CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… [META-24100] Add MultiAspectReadBenchmarkTest measuring the current per-aspect read (one SELECT per aspect) against the bundled multi-aspect read from linkedin#642 (a single SELECT per entity table) on identical seeded data, and report the query-count reduction plus p50/p90/p99/max latency speedup. Local in-process MariaDB result (73-aspect entity, 30 urns, 200 iters): current 73 SELECTs p50 26.985ms p99 34.415ms; multi-aspect 1 SELECT p50 1.139ms p99 1.375ms => 73x fewer round-trips, p50 ~23.7x faster, p99 ~25x. Reuses the benchmark harness (urn resource, gma.benchmark gate) added by the baseline linkedin#640; only adds the incremental gma.benchmark.urnFile hook. Gated behind -Dgma.benchmark=true (skipped in CI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Implements the config-controlled multi-aspect batch read on top of the API surface + routing introduced in the base PR (#641). This collapses N per-aspect
SELECTs into a single bundledSELECTof all requested aspect columns per entity table — the read-latency optimization for META-24100.Stacked on #641 (base branch =
dbhayani/multi-aspect-read-primitive). It only makes theMULTI_ASPECT/DUALpaths functional; the default strategy is stillPER_ASPECT, so behavior is unchanged unless explicitly opted in.What's included
SQLStatementUtils.javacreateMultiAspectReadSql(...)— build one bundledSELECTof all aspect columns per table; urns escaped viaescapeReservedCharInUrn; row-leveldeleted_ts IS NULLfilter; also selectsdeleted_tswhenincludeSoftDeletedEbeanLocalAccess.javabatchGetUnionMultiAspect(...)— group keys by entity table, run one query per table, then row-driven mapping back to aspects (see below)InstrumentedEbeanLocalAccess.java,UsageTrackingEbeanLocalAccess.javaMapping semantics (row-driven, matches the per-aspect path)
Rather than reverse-looking-up rows by the requested urn string, the mapping iterates the returned rows (like the per-aspect path) and emits one aspect per requested
(urn, aspect)pair:matchKey), so an aspect is not silently dropped when the stored urn differs from the requested urn by canonical/case. The returned aspect's urn is derived from the row itself, matching per-aspect output.Set(requestedPairs);requestedPairs.remove(...)both restricts output to requested pairs and deduplicates repeated(urn, aspect)keys, matching the per-aspectSet<Urn>grouping.validator.columnExistscheck during key collection), no row / null cell → skip.Soft-delete semantics
A bundled query can't
ANDper-column JSON soft-delete checks without dropping the whole row when any single aspect is soft-deleted. So soft-delete is handled in two layers:deleted_ts IS NULL, dropped whenincludeSoftDeleted), andEBeanDAOUtils.isSoftDeletedAspect.This preserves parity with the per-aspect path.
Validation in prod without member impact (
DUAL)The
DUALstrategy (in #641'snewSchemaBatchGet) reads both paths, compares them viaEBeanDAOUtils.compareResults(order-insensitive; logs a WARN on mismatch, never throws), and returns the per-aspect (primary) result only. So multi-aspect can be shadow-validated in production with zero member-facing risk before flipping toMULTI_ASPECT. Comparison is by aspect identity(urn, aspect, version), which catches the real read-path risks (missing / extra / duplicate aspects, count and soft-delete-filtering differences).Tests
The two stub tests from #641 are upgraded to full assertions, plus new coverage for the review feedback:
SQLStatementUtilsTest.testCreateMultiAspectReadSql— asserts the exact bundled SQL forincludeSoftDeletedfalse/true, using real production dataset URNs (from the PR test(ebean-dao): baseline benchmark for pre-#622 multi-aspect read [META-24100] #640 read benchmark).EbeanLocalAccessTest.testBatchGetUnionMultiAspectMatchesPerAspect— per-aspect vs multi-aspect result parity, including missing-column and non-existent-urn skip.EbeanLocalAccessTest.testBatchGetUnionMultiAspectExcludesSoftDeletedAspect— soft-delete behavior (requested in review): a soft-deleted aspect is excluded whenincludeSoftDeleted=falseand returned/flagged whentrue, with per-aspect parity.EbeanLocalAccessTest.testBatchGetUnionMultiAspectDeduplicatesRepeatedKeys— duplicate(urn, aspect)keys collapse to one result, matching the per-aspect path.All pass locally (only the unrelated pre-existing macOS-only
testParseLocalRelationshipFieldflake fails).Review feedback addressed
(urn, aspect)key parity (+ test).validator.columnExistspass.Notes / follow-up
IN (...)lists is intentionally not included here and can be added in a later change.list/listUrns) support is a separate follow-up (PR 3), reusingreadSqlRowForAspect.AspectReadStrategy(PER_ASPECTdefault →DUALcompare →MULTI_ASPECT).