[lake] Carry opaque table-level tiering state in lake offsets file and RPC#3660
[lake] Carry opaque table-level tiering state in lake offsets file and RPC#3660beryllw wants to merge 4 commits into
Conversation
cd134f0 to
3cbbdd1
Compare
…hCode, writeTree, readTree helper)
3cbbdd1 to
3fec3cf
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds an opaque, table-level tiering state payload (JSON bytes) that is persisted in the lake offsets file and transported via tiering-related RPCs, enabling future features (e.g., partition mark-done) without additional proto/RPC evolution.
Changes:
- Introduces
LakeTieringTableStateplus JSON serde, and embeds optionaltiering_stateintoTableBucketOffsetsJSON (offsets file format staysversion=1). - Extends tiering RPCs (
PbTableOffsets,GetLakeSnapshotResponse) withoptional bytes tiering_state_jsonand bumps API max versions to1for capability detection. - Threads the payload through server/client plumbing and adds focused tests for serde behavior, merge semantics, RPC passthrough, and lazy parsing.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| fluss-server/src/test/java/org/apache/fluss/server/zk/data/lake/LakeTableHelperTest.java | Adds merge/overwrite vs keep-previous tests for tiering state during offsets merge. |
| fluss-server/src/test/java/org/apache/fluss/server/utils/ServerRpcMessageUtilsTieringStateTest.java | Verifies PREPARE→GET passthrough and absence behavior for tiering state bytes. |
| fluss-server/src/main/java/org/apache/fluss/server/zk/data/lake/LakeTableSnapshot.java | Extends snapshot metadata to carry optional tiering state bytes. |
| fluss-server/src/main/java/org/apache/fluss/server/zk/data/lake/LakeTableHelper.java | Implements merge semantics: offsets merge partially; tiering state uses whole-value overwrite/keep behavior. |
| fluss-server/src/main/java/org/apache/fluss/server/zk/data/lake/LakeTable.java | Reads tiering state bytes from offsets file into LakeTableSnapshot. |
| fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java | Passes tiering state bytes from/to RPC messages without interpretation. |
| fluss-rpc/src/main/proto/FlussApi.proto | Adds optional bytes tiering_state_json to relevant RPC messages. |
| fluss-rpc/src/main/java/org/apache/fluss/rpc/protocol/ApiKeys.java | Bumps max versions for GET/PREPARE lake snapshot APIs for capability detection. |
| fluss-common/src/test/java/org/apache/fluss/utils/json/TableBucketOffsetsJsonSerdeTest.java | Adds tiering_state embed round-trip and bad-payload rejection tests. |
| fluss-common/src/test/java/org/apache/fluss/utils/json/LakeTieringTableStateJsonSerdeTest.java | Adds tests for format, defaults, and higher-version tolerance. |
| fluss-common/src/main/java/org/apache/fluss/utils/json/TableBucketOffsetsJsonSerde.java | Embeds and reads tiering_state JSON in the offsets serde. |
| fluss-common/src/main/java/org/apache/fluss/utils/json/TableBucketOffsets.java | Extends model to carry optional tiering state bytes. |
| fluss-common/src/main/java/org/apache/fluss/utils/json/LakeTieringTableStateJsonSerde.java | Implements JSON serde for the new tiering state model. |
| fluss-common/src/main/java/org/apache/fluss/utils/json/JsonSerdeUtils.java | Adds readTree(byte[]) helper for pass-through JSON payload validation/embedding. |
| fluss-common/src/main/java/org/apache/fluss/lake/committer/LakeTieringTableState.java | Adds the public-evolving, versioned tiering state model with JSON helpers. |
| fluss-client/src/test/java/org/apache/fluss/client/metadata/LakeSnapshotTest.java | Tests lazy parsing and isolation from corrupt tiering state bytes. |
| fluss-client/src/main/java/org/apache/fluss/client/utils/ClientRpcMessageUtils.java | Passes tiering state bytes from RPC response into LakeSnapshot. |
| fluss-client/src/main/java/org/apache/fluss/client/metadata/LakeSnapshot.java | Stores tiering state bytes and parses them on demand. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // read the opaque tiering-state payload (optional) as raw JSON bytes; absent yields null. | ||
| byte[] tieringStateJson = null; | ||
| JsonNode tieringStateNode = node.get(TIERING_STATE_KEY); | ||
| if (tieringStateNode != null && !tieringStateNode.isNull()) { | ||
| tieringStateJson = tieringStateNode.toString().getBytes(StandardCharsets.UTF_8); | ||
| } |
| Map<Long, Long> partitionUpdateTimes = new HashMap<>(); | ||
| JsonNode updateTimesNode = node.get(PARTITION_UPDATE_TIMES_KEY); | ||
| if (updateTimesNode != null && updateTimesNode.isObject()) { | ||
| Iterator<Map.Entry<String, JsonNode>> fields = updateTimesNode.fields(); | ||
| while (fields.hasNext()) { | ||
| Map.Entry<String, JsonNode> field = fields.next(); | ||
| partitionUpdateTimes.put(Long.valueOf(field.getKey()), field.getValue().asLong()); | ||
| } | ||
| } |
platinumhamburg
left a comment
There was a problem hiding this comment.
@beryllw Thanks for adding generic support for table-level tiering state. Having the Coordinator pass the state through without interpreting it keeps the RPC and server code independent of any specific use case, which is a reasonable design choice.
My main reason for requesting changes is that the lifecycle of the state remains somewhat ambiguous. In a normal lake commit, tiered offsets and state may change together; in that case, publishing them atomically as one result is reasonable. The problem arises when the state changes without new lake data. In #3635, a state-only update creates a new offsets file, reuses the previous snapshot ID, and appends another lake snapshot metadata entry. As a result, the same snapshot ID can refer to multiple entries. Latest and by-ID queries may return different state, while the behavior of readable snapshots, retention, and file cleanup also becomes unclear.
Before the first consumer is merged, please clarify whether the state belongs to a lake snapshot or can change independently. If it belongs to a lake snapshot, it should change only when a new snapshot is committed. If it can change independently, a state-only update should not create another lake snapshot record with the same snapshot ID. The API should also define which state GetLakeSnapshot returns for latest, historical, and readable requests. PREPARE and COMMIT should validate tieringEpoch to prevent a writer from an old assignment from overwriting newer state.
For #3660 itself, the current API still has several correctness issues: the V1 path may lose existing state; a PREPARE carrying state does not require API v1, and its response cannot confirm whether the state was accepted; the API does not prevent an older job from using or rewriting a state version it does not support; and known JSON fields are not validated strictly. These issues should be resolved before the API is considered safe for rolling upgrades.
| NOTIFY_LAKE_TABLE_OFFSET(1031, 0, 0, PRIVATE), | ||
| GET_LAKE_SNAPSHOT(1032, 0, 0, PUBLIC), | ||
| // Version 1: response may carry the opaque tiering_state_json (for capability detection). | ||
| GET_LAKE_SNAPSHOT(1032, 0, 1, PUBLIC), |
There was a problem hiding this comment.
why we need to bump the version?
|
@wuchong Could you please help review the api changes. Here's a summary: |
…state, commit epoch fencing
|
The key fact is that mark-done state can change without a new lake commit (a partition is marked done on idle timeout, with no new data), so it's table-level and doesn't naturally belong to a single snapshot. There are two ways to carry it. Option 1: keep it embedded in the offsets file (current). Simple, no new files, offsets and state published together. But a state-only change needs a new offsets file, which forces reusing the snapshot ID and appending a duplicate entry — the ambiguity you flagged around latest/by-id/readable and retention. Option 2: split it into a dedicated table-level file with its own pointer. State advances independently, so IDs stay 1:1 and retention/GetLakeSnapshot stay clean, V1 never touches state, and the state path can validate tieringEpoch on its own. Cost is one extra file with its own lifecycle. |
Purpose
Linked issue: close #3314
Lakehouse tiering needs to persist table-level state in Fluss (beyond per-bucket offsets) and share it across tiering rounds and multiple stateless jobs. This PR introduces a generic, opaque carrier for such state on the lake offsets file and the tiering RPCs, so future state (e.g. partition mark-done) can be added without further proto/RPC changes.
The carrier is intentionally decoupled from any consumer: the coordinator moves it as an opaque blob and never interprets it. The first consumer (partition mark-done) will be delivered separately on top of this contract.
Brief change log
LakeTieringTableState(@PublicEvolving) , a flat versioned model, and its JsonSerde; it evolves by adding fields and bumping the format version, and a consumer that reads a higher version than it understands must not overwrite the state so it never drops fields written by a newer build. TableBucketOffsets / TableBucketOffsetsJsonSerde carry the state as an embedded JSON object (top-level version stays 1), validated to be a JSON object on write.optional bytes tiering_state_jsontoPbTableOffsetsandGetLakeSnapshotResponse; bumpGET_LAKE_SNAPSHOT/PREPARE_LAKE_TABLE_SNAPSHOTmaxVersion to 1 for capability detection.ServerRpcMessageUtils,LakeTable,LakeTableHelper,LakeTableSnapshot); on merge the latest value overwrites, and a request without it keeps theprevious one.
LakeSnapshotholds the raw bytes and parses them lazily ingetLakeTieringTableState(), so a corrupt/incompatible payload never blocks access to the bucket offsets.Tests
LakeTieringTableStateJsonSerdeTest: format, round-trip/defaults, and higher-version tolerance (unknown fields ignored).TableBucketOffsetsJsonSerdeTest: carrier embed round-trip (via base) and rejection of malformed / non-object payloads.ServerRpcMessageUtilsTieringStateTest: PREPARE→GET passthrough round-trip and absent-state handling.LakeTableHelperTest: merge overwrite and keep-previous-when-absent.LakeSnapshotTest: lazy parse and corrupt-state isolation.API and Format
versionunchanged (1); adds an optional embeddedtiering_stateJSON object. Old readers ignore it; new readers default to absent. Non-partitioned tables are unchanged.Documentation
No user-facing docs; the carrier is internal plumbing. The first feature built on it (partition mark-done) will document itself.