[compat] Make maxNearlineRecordSizeBytes a per-version config - #2986
[compat] Make maxNearlineRecordSizeBytes a per-version config#2986KaiSernLim wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR snapshots maxNearlineRecordSizeBytes onto each Version at version-creation time so nearline (realtime-topic) oversized-record enforcement remains stable for an in-flight version, while leaving batch maxRecordSizeBytes as a live store-level read.
Changes:
- Add
StoreMetaValueschema v47 withStoreVersion.maxNearlineRecordSizeBytes, bump metadata system schema version, and plumb the new field throughVersion/VersionImpl, cloning, and store/version conversions. - Update ingestion read/merge paths to avoid assembling chunked values when the stored manifest indicates the record is already over the nearline ceiling; emit a new OTel counter + warning log on skips.
- Add/extend unit + integration tests covering snapshotting/immutability, chunk read veto behavior, ingestion skipping semantics, and metric entity registration.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/TestNearlineLargeRecordSkipping.java | New multi-region E2E test validating nearline partial-update skipping and reset behavior. |
| internal/venice-common/src/test/java/com/linkedin/venice/meta/TestZKStore.java | Unit tests for version-level snapshotting/default/clone persistence. |
| internal/venice-common/src/main/resources/avro/StoreMetaValue/v47/StoreMetaValue.avsc | New schema version adding StoreVersion.maxNearlineRecordSizeBytes. |
| internal/venice-common/src/main/java/com/linkedin/venice/serialization/avro/AvroProtocolDefinition.java | Bump metadata system store schema version to v47. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/VersionImpl.java | Back get/setMaxNearlineRecordSizeBytes with Avro StoreVersion field; preserve in clone. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/Version.java | Add version-level getter/setter for nearline record size ceiling. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/ReadOnlyStore.java | Delegate version-level getter in ReadOnlyVersion; include field in version conversion. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/AbstractStore.java | Snapshot store-level nearline limit onto newly-added versions. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/storage/chunking/ChunkingTest.java | Tests ensuring chunked reads are vetoed before chunk fetch/assembly when over ceiling. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/stats/ServerMetricEntityTest.java | Update expected metric entity count for new ingestion metric. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/stats/ingestion/IngestionOtelMetricEntityTest.java | Register expectation for new nearline large-record skipped counter metric. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTaskTest.java | Unit tests for inclusive boundary, per-record behavior, and unlimited ceiling cases. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/storage/chunking/ChunkingUtils.java | Short-circuit chunk assembly when manifest indicates size ceiling exceeded. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/storage/chunking/ChunkedValueManifestContainer.java | Add ceiling + isSizeLimitExceeded() signaling to disambiguate null results. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ingestion/NoOpIngestionOtelStats.java | No-op override for the new large-record skipped metric. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ingestion/IngestionOtelStats.java | Add OTel metric entity state + recording method for large-record skipped count. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/ingestion/IngestionOtelMetricEntity.java | Define ingestion.partial_update.large_record_skipped_count metric entity. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/stats/AggVersionedIngestionStats.java | Add aggregator method to record the new nearline skip counter. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/StoreIngestionTask.java | Expose version snapshot via getVersion() for version-frozen config reads. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTask.java | Read nearline limit from version snapshot; enforce skip before assembly; emit metric/log. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/ActiveActiveStoreIngestionTask.java | Apply ceiling only to partial updates; discard merge output when oversized-record skip occurs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
d7665b1 to
928d9c7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTask.java:2998
- Switching nearline max-record enforcement to read from Version means versions created before the new StoreMetaValue/v48 schema will deserialize StoreVersion.maxNearlineRecordSizeBytes as the Avro default (-1). With the current backfill logic, that effectively forces those in-flight versions to use serverConfig.getDefaultMaxRecordSizeBytes(), ignoring any previously configured store-level maxNearlineRecordSizeBytes.
If this is meant to only affect versions created after the snapshot feature, consider adding a migration/backfill path keyed off the metadata system store value schema id (<48) so pre-v48 versions get initialized (once) with the store-level maxNearlineRecordSizeBytes at upgrade time (or introduce an explicit migration flag like the other store→version config migrations).
protected int getMaxRecordSizeBytes() {
return backfillRecordSizeLimit(storeRepository.getStore(storeName).getMaxRecordSizeBytes());
}
protected int getMaxNearlineRecordSizeBytes() {
return backfillRecordSizeLimit(getVersion().getMaxNearlineRecordSizeBytes());
}
928d9c7 to
c9eff5c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTask.java:3003
- The version-level snapshot uses the same sentinel (-1 / VeniceWriter.UNLIMITED_MAX_RECORD_SIZE) as both (a) an explicitly snapshotted "unset" value for versions created after v48 (AbstractStore.addVersion copies the store-level value, which is often -1), and (b) the Avro default used when the field is missing on pre-v48 versions. With the current check, any v48+ version whose snapshot is -1 will fall back to the live store-level value, so changing the store config later can retroactively change enforcement for already-created versions—contradicting the per-version snapshot goal.
// the upgrade instead of silently reverting to the fleet default. Versions created after the snapshot feature
// always carry an explicit value, so this fallback never overrides an intentional per-version limit.
int recordSizeLimit = (versionLevelLimit != VeniceWriter.UNLIMITED_MAX_RECORD_SIZE)
? versionLevelLimit
: storeRepository.getStore(storeName).getMaxNearlineRecordSizeBytes();
internal/venice-common/src/main/resources/avro/StoreMetaValue/v48/StoreMetaValue.avsc:271
- Using a default of -1 for StoreVersion.maxNearlineRecordSizeBytes makes pre-v48 versions (field absent) indistinguishable from v48+ versions that explicitly snapshot an unset value (-1). Since the server logic treats -1 as "unset" and falls back to the live store-level config, this can reintroduce retroactive behavior for v48+ versions created while the store-level value was unset.
{"name": "maxNearlineRecordSizeBytes", "type": "int", "default": -1, "doc": "Version-level max record size in bytes for nearline jobs with partial updates, snapshotted from the store-level config when this version is created. If not specified (-1), the server config 'default.max.record.size.bytes' (100MB default) will be backfilled at runtime."}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/venice-common/src/main/resources/avro/StoreMetaValue/v48/StoreMetaValue.avsc:98
- The field doc for
clientDecompressionEnabledcontains a typo ("le/Disable"). Since this v48 schema is newly introduced, consider correcting it here to improve generated-schema docs (the same typo also exists in the immutable v47 schema).
{"name": "clientDecompressionEnabled", "type": "boolean", "default": true, "doc": "le/Disable client-side record decompression (default: true)"},
91c4768 to
9ced9f0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
internal/venice-common/src/main/resources/avro/StoreMetaValue/v48/StoreMetaValue.avsc:171
- This new v48 store-level doc says editing the value only affects versions created afterward, but the implementation intentionally treats a null version snapshot (pre-v48 versions) as a signal to read this live store value. Therefore a store-level edit does affect those pre-v48 versions retroactively. Please qualify the statement to distinguish non-null snapshots from versions without a snapshot.
{"name": "maxNearlineRecordSizeBytes", "type": "int", "default": -1, "doc": "Store-level source for the nearline max record size (bytes) for partial-update jobs. This value is snapshotted onto each StoreVersion.maxNearlineRecordSizeBytes at version-creation time, so editing it only affects versions created afterward; the server enforces the per-version snapshot, not this store-level value directly. If not specified (-1), the server config 'default.max.record.size.bytes' (100MB default) is backfilled. This may converge with maxRecordSizeBytes in the future"},
9ced9f0 to
3d45f08
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTask.java:3000
- This new per-version getter is not used by the production ingestion path:
StoreIngestionTask.recordAssembledRecordSize()callscalculateAssembledRecordSizeRatio(), whose implementation below still usesgetMaxRecordSizeBytes(), and there are no other production callers ofgetMaxNearlineRecordSizeBytes(). As a result, changing the version snapshot has no effect on nearline record-size handling despite the fallback logic being unit-tested. Please wire this value into the actual nearline path while retaining the batch path's live limit.
Integer versionLevelLimit = getVersion().getMaxNearlineRecordSizeBytes();
// A null snapshot means the version was created before this field existed (StoreMetaValue < v48); fall back to
// the live store-level value so a previously configured nearline limit survives the upgrade instead of reverting
// to the fleet default. A non-null snapshot (including -1) is used as-is, so a later store-level change never
// retroactively alters enforcement for an already-created version.
internal/venice-common/src/main/java/com/linkedin/venice/meta/ReadOnlyStore.java:2090
- The new serialization mapping is not covered by the added tests.
TestZKStoreonly reads the field from the in-memoryVersion, whileReadOnlyStoreTestcurrently asserts version count but not version fields; a regression in this line would silently drop the snapshot whenReadOnlyStore.cloneStoreProperties()writes metadata. Add a round-trip assertion for a non-null value (and ideally-1/null) oncloned.getVersions().get(0), following the existing version-field tests inReadOnlyStoreTestaround lines 330–348.
storeVersion.setMaxNearlineRecordSizeBytes(version.getMaxNearlineRecordSizeBytes());
maxNearlineRecordSizeBytes) Introduce StoreMetaValue schema v48, which adds a version-level maxNearlineRecordSizeBytes field on the nested StoreVersion record (nullable union ["null","int"], default null) and refines the doc on the existing store-level StoreProperties.maxNearlineRecordSizeBytes to describe it as the snapshot source (and to note that only null-snapshot legacy versions fall back to reading it live). Following the staged-protocol convention, generation is pinned to v47 via the compileAvro versionOverrides so the generated classes stay on v47 and no code path can serialize v48 yet. The override is removed and METADATA_SYSTEM_SCHEMA_STORE is bumped to v48 in the follow-up PR (linkedin#2986) that reads and snapshots the per-version limit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
3d45f08 to
0fb682e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/LeaderFollowerStoreIngestionTask.java:2999
- This snapshot-aware accessor is not used by any production ingestion path:
StoreIngestionTask.recordAssembledRecordSize()callscalculateAssembledRecordSizeRatio(), and this class still implements that withgetMaxRecordSizeBytes()(and the ratio reporter repeats the same live store-level lookup). As a result, changingStoreVersion.maxNearlineRecordSizeByteshas no runtime effect, so the per-version nearline limit described by this PR is not actually enforced. Wire this accessor into the hybrid/partial-update path while retaining the batch path'smaxRecordSizeBytesbehavior, and cover that path rather than only testing the accessor directly.
protected int getMaxNearlineRecordSizeBytes() {
Integer versionLevelLimit = getVersion().getMaxNearlineRecordSizeBytes();
// A null snapshot means the version was created before this field existed (StoreMetaValue < v48); fall back to
// the live store-level value so a previously configured nearline limit survives the upgrade instead of reverting
// to the fleet default. A non-null snapshot (including -1) is used as-is, so a later store-level change never
…RecordSizeBytes) (#2991) Introduce StoreMetaValue schema v48, which adds a version-level maxNearlineRecordSizeBytes field on the nested StoreVersion record (nullable union ["null","int"], default null) and refines the doc on the existing store-level StoreProperties.maxNearlineRecordSizeBytes to describe it as the snapshot source (and to note that only null-snapshot legacy versions fall back to reading it live). Following the staged-protocol convention, generation is pinned to v47 via the compileAvro versionOverrides so the generated classes stay on v47 and no code path can serialize v48 yet. The override is removed and METADATA_SYSTEM_SCHEMA_STORE is bumped to v48 in the follow-up PR (#2986) that reads and snapshots the per-version limit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Read and snapshot the nearline oversized-record limit per Version instead of live from the Store. The limit is captured onto the Version at version-creation time in AbstractStore.addVersion (mirroring chunkingEnabled/storageMode); nearline ingestion is continuous, so reading it live from the store would retroactively change enforcement for already in-flight versions. The server reads the per-version snapshot in LeaderFollowerStoreIngestionTask.getMaxNearlineRecordSizeBytes() via a new StoreIngestionTask.getVersion() accessor. A null snapshot (a version created before the v48 field existed) falls back to the live store-level value; a non-null value (including -1) is an immutable per-version snapshot that later store-level changes do not affect. Activates StoreMetaValue v48 (staged in linkedin#2991) by removing the compileAvro versionOverrides pin and bumping METADATA_SYSTEM_SCHEMA_STORE 47->48, so the generated StoreVersion carries the new maxNearlineRecordSizeBytes field this logic reads/writes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
0fb682e to
d731438
Compare
sixpluszero
left a comment
There was a problem hiding this comment.
lgtm, thank you for the changes!
Why
Nearline (realtime-topic partial-update) ingestion is continuous. Reading the oversized-record limit live from the store would retroactively change enforcement for already in-flight versions. This makes
maxNearlineRecordSizeBytesa per-version config, snapshotted onto theVersionat version-creation time (mirroringchunkingEnabled/storageMode), so a later store-config change only affects future versions.Batch
maxRecordSizeBytesis intentionally left as a live store-level read — a batch push writes a version exactly once, so its value is already de-facto frozen per version.Wire contract
StoreVersion.maxNearlineRecordSizeBytesis a nullable union["null","int"], defaultnull:null= pre-v48 version, no snapshot → the server falls back to the live store-level value at runtime (preserves behavior for metadata written before v48).-1= an immutable per-version snapshot; later store-level changes never affect it (-1= store was unset at creation, backfilled to the server default at runtime).Changes
build.gradle, bumpMETADATA_SYSTEM_SCHEMA_STORE47→48 (schema itself introduced in [protocol][compat] Stage StoreMetaValue v48 (version-level maxNearlineRecordSizeBytes) #2991).Integer get/setMaxNearlineRecordSizeBytesbacked by the nullableStoreVersionfield; carried throughcloneVersion().convertVersionreverse mapping.LeaderFollowerStoreIngestionTask.getMaxNearlineRecordSizeBytes()reads the per-version snapshot via a newStoreIngestionTask.getVersion()accessor, with the null→live-store fallback above.TestZKStore: snapshot + immutability, null default,cloneVersionpreservation, and acloneStoreProperties().getVersions()round-trip.LeaderFollowerStoreIngestionTaskTest: version→store→fleet-default fallback and no-retroactive-change.VALIDATION_OVERRIDE
The
enforce-lines-addedcheck rejects a PR that touches both asrc/main.avscand asrc/main.javafile. This PR does, because it activates schema v48 (present in the diff via the stacked #2991 commit) in the same change that adds the consuming Java — they must be atomic so the generatedStoreVersionversion matches the registeredMETADATA_SYSTEM_SCHEMA_STORE. Totals are well under the line caps; only the schema-plus-Java coupling rule is triggered, soVALIDATION_OVERRIDEis the sanctioned path.Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com