-
Notifications
You must be signed in to change notification settings - Fork 4k
[feature](ivm) Add the per-partition refresh state and its journal channel #68193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| package org.apache.doris.mtmv; | ||
|
|
||
| import com.google.gson.annotations.SerializedName; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
|
|
||
| /** | ||
| * The per-partition refresh state of one MV partition. | ||
| * | ||
| * <p>{@code refreshEpoch} says which generation of data the partition currently holds, {@code | ||
| * latestEpoch} says which generation it must hold. A partition whose {@code latestEpoch} is ahead of | ||
| * its {@code refreshEpoch} is dirty: it holds rows read before a metadata-only change of a base table | ||
| * (a dropped / truncated / replaced / recovered partition emits no row binlog), so those rows can no | ||
| * longer be removed incrementally and the partition has to be rebuilt. | ||
| * | ||
| * <p>Keyed by MV partition name in {@code MTMV.partitionStates}. The name is deliberately the only | ||
| * identity: an MV partition is rewritten by {@code INSERT OVERWRITE} on every refresh and gets a new | ||
| * partition id each time, so an id would stop matching as soon as the partition is refreshed. | ||
| * | ||
| * <p>The two values are plain {@code long}s rather than atomics because this is a persisted DTO: it is | ||
| * serialized into the alter journal, so it has to stay a plain bean. | ||
| */ | ||
| public class MTMVPartitionState { | ||
| /** The generation of the data this MV partition currently holds; 0 means it was never refreshed. */ | ||
| @SerializedName("re") | ||
| private long refreshEpoch; | ||
|
|
||
| /** The generation the data must reach; starts at 1 and grows on every invalidation. */ | ||
| @SerializedName("le") | ||
| private long latestEpoch; | ||
|
|
||
| public MTMVPartitionState() { | ||
| } | ||
|
|
||
| public MTMVPartitionState(long refreshEpoch, long latestEpoch) { | ||
| this.refreshEpoch = refreshEpoch; | ||
| this.latestEpoch = latestEpoch; | ||
| } | ||
|
|
||
| public MTMVPartitionState(MTMVPartitionState other) { | ||
| this.refreshEpoch = other.refreshEpoch; | ||
| this.latestEpoch = other.latestEpoch; | ||
| } | ||
|
|
||
| /** | ||
| * Deep-copies a state map, or returns null for null. | ||
| * | ||
| * <p>The journal needs this on both sides. A payload is serialized by the journal thread, which | ||
| * runs after the submitting thread released the MV lock, so a payload that shared state with the | ||
| * live map could be written out half-mutated. The replay path goes through the same helper so that | ||
| * both sides of the journal follow one rule instead of two. | ||
| */ | ||
| public static Map<String, MTMVPartitionState> copyOf(Map<String, MTMVPartitionState> states) { | ||
| if (states == null) { | ||
| return null; | ||
| } | ||
| Map<String, MTMVPartitionState> copy = new LinkedHashMap<>(); | ||
| for (Entry<String, MTMVPartitionState> entry : states.entrySet()) { | ||
| copy.put(entry.getKey(), new MTMVPartitionState(entry.getValue())); | ||
| } | ||
| return copy; | ||
| } | ||
|
|
||
| public long getRefreshEpoch() { | ||
| return refreshEpoch; | ||
| } | ||
|
|
||
| public void setRefreshEpoch(long refreshEpoch) { | ||
| this.refreshEpoch = refreshEpoch; | ||
| } | ||
|
|
||
| public long getLatestEpoch() { | ||
| return latestEpoch; | ||
| } | ||
|
|
||
| public void setLatestEpoch(long latestEpoch) { | ||
| this.latestEpoch = latestEpoch; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,8 @@ | |
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
|
|
||
|
|
@@ -403,6 +405,44 @@ public void testAlterIvmInfoPersistence() throws Exception { | |
| Assertions.assertEquals(schemaChangeVersion, mtmv.getSchemaChangeVersion()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testReplayAlterPartitionStates() throws Exception { | ||
| Config.enable_table_stream = true; | ||
| createDatabaseAndUse("alter_partition_states_test"); | ||
| createTable("CREATE TABLE alter_partition_states_test.states_base (k1 int, v1 int)\n" | ||
| + "DUPLICATE KEY(k1)\n" | ||
| + "DISTRIBUTED BY HASH(k1) BUCKETS 1\n" | ||
| + "PROPERTIES ('replication_num' = '1', 'binlog.enable' = 'true', 'binlog.format' = 'ROW')"); | ||
| createMvByNereids("CREATE MATERIALIZED VIEW states_mv\n" | ||
| + " BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL\n" | ||
| + " DISTRIBUTED BY RANDOM BUCKETS 2\n" | ||
| + " PROPERTIES ('replication_num' = '1')\n" | ||
| + " AS SELECT k1, v1 FROM states_base"); | ||
|
|
||
| MTMV mtmv = (MTMV) Env.getCurrentInternalCatalog() | ||
| .getDb("alter_partition_states_test").get() | ||
| .getTableOrMetaException("states_mv"); | ||
| String partitionName = mtmv.getPartitionNames().iterator().next(); | ||
|
|
||
| MTMVPartitionState state = new MTMVPartitionState(0, 1); | ||
| Map<String, MTMVPartitionState> states = new LinkedHashMap<>(); | ||
| states.put(partitionName, state); | ||
| TableNameInfo tableName = new TableNameInfo(mtmv.getQualifiedDbName(), mtmv.getName()); | ||
| AlterMTMV replayAlter = new AlterMTMV(tableName, MTMVAlterOpType.ALTER_PARTITION_STATES); | ||
| replayAlter.setPartitionStates(states); | ||
| // The live map keeps moving after the payload was taken; the payload must not follow it. | ||
| state.setLatestEpoch(7); | ||
| // The MV starts without any state, so only the replayed payload can put it there. | ||
| mtmv.alterPartitionStates(Map.of()); | ||
|
|
||
| Env.getCurrentEnv().getAlterInstance().processAlterMTMV(replayAlter, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Exercise the journal wire round trip before replay This calls |
||
|
|
||
| Map<String, MTMVPartitionState> replayed = mtmv.getPartitionStates(); | ||
| Assertions.assertEquals(Set.of(partitionName), replayed.keySet()); | ||
| Assertions.assertEquals(0, replayed.get(partitionName).getRefreshEpoch()); | ||
| Assertions.assertEquals(1, replayed.get(partitionName).getLatestEpoch()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testCreateIncrementalMtmvAutoCreatesStream() throws Exception { | ||
| createDatabaseAndUse("stream_test"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Keep partition-state ownership inside
mvRwLockThis returns the live map after the
finallyreleasesmvRwLock, and each value is mutable too. A caller using the pattern in the new tests (getPartitionStates().put(...)) can therefore add/remove entries or change an epoch whileaddTaskResult()is copying the sameLinkedHashMapfor the journal, producing aConcurrentModificationExceptionor a mixed snapshot. If replay replaces the field first, the retained reference instead accepts a silently lost update. Since this API is the persistence foundation for the follow-up invalidation/alignment code, please return a deep detached/unmodifiable snapshot for reads and add lock-owning MTMV mutation methods that mutate and enqueueALTER_PARTITION_STATESunder the same write lock.