-
Notifications
You must be signed in to change notification settings - Fork 14k
[FLINK-39964][state] Fix unrestorable checkpoint after CLAIM restore from native savepoint #28709
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
fbe2e6b
e84e8bb
70dff60
3d0dda1
2b7a5c8
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,175 @@ | ||
| /* | ||
| * 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.flink.state.api.output; | ||
|
|
||
| import org.apache.flink.api.common.io.FirstAttemptInitializationContext; | ||
| import org.apache.flink.core.fs.Path; | ||
| import org.apache.flink.runtime.checkpoint.OperatorState; | ||
| import org.apache.flink.runtime.checkpoint.OperatorSubtaskState; | ||
| import org.apache.flink.runtime.checkpoint.metadata.CheckpointMetadata; | ||
| import org.apache.flink.runtime.state.IncrementalKeyedStateHandle.HandleAndLocalPath; | ||
| import org.apache.flink.runtime.state.IncrementalRemoteKeyedStateHandle; | ||
| import org.apache.flink.runtime.state.KeyGroupRange; | ||
| import org.apache.flink.runtime.state.StreamStateHandle; | ||
| import org.apache.flink.runtime.state.filesystem.RelativeFileStateHandle; | ||
| import org.apache.flink.runtime.state.memory.ByteStreamStateHandle; | ||
| import org.apache.flink.state.api.runtime.OperatorIDGenerator; | ||
| import org.apache.flink.state.api.runtime.SavepointLoader; | ||
| import org.apache.flink.streaming.util.MockStreamingRuntimeContext; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.io.TempDir; | ||
|
|
||
| import java.io.File; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.util.Collections; | ||
| import java.util.UUID; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| /** | ||
| * Tests that a savepoint written by {@link SavepointOutputFormat} stays self-contained when it | ||
| * retains operator state from an existing savepoint, i.e. that it holds no references back into the | ||
| * source savepoint it was derived from. | ||
| * | ||
| * <p>{@code SavepointWriter.fromExistingSavepoint(...)} keeps retained operator state by copying | ||
| * the state files into the new savepoint directory ({@link FileCopyFunction}, keeping the file | ||
| * names), while the retained {@link RelativeFileStateHandle}s still carry the paths of the original | ||
| * files in the source savepoint. The new savepoint is self-contained only if its metadata | ||
| * references those files by file name alone: such a reference resolves against the directory the | ||
| * metadata is read from, which is the new savepoint holding the copies. If the metadata recorded | ||
| * the handles' absolute paths instead, the new savepoint would keep depending on the source | ||
| * savepoint and become unrestorable once the source is deleted. | ||
| * | ||
| * <p>See the comment in {@link SavepointOutputFormat#writeRecord} for why the metadata must be | ||
| * written with {@code Checkpoints.storeCheckpointMetadataWithoutExclusiveDir}. {@code | ||
| * SavepointDeepCopyTest} covers the same guarantee end-to-end on a cluster, where a regression | ||
| * surfaces only as a restore failure far from its cause; this test pins the encoding decision | ||
| * itself, so a regression fails fast and points at the responsible code. | ||
| */ | ||
| class SavepointOutputFormatSelfContainedTest { | ||
|
|
||
| private static final int PARALLELISM = 1; | ||
| private static final int MAX_PARALLELISM = 128; | ||
| private static final int SUBTASK_INDEX = 0; | ||
| private static final long CHECKPOINT_ID = 1L; | ||
| private static final String OPERATOR_UID = "uid"; | ||
| private static final String STATE_FILE_NAME = "sst-0001.state"; | ||
|
|
||
| @Test | ||
| void testSavepointWithRetainedStateStaysSelfContained(@TempDir File tempDir) throws Exception { | ||
| File sourceSavepointDir = new File(tempDir, "sp-old"); | ||
| File targetSavepointDir = new File(tempDir, "sp-new"); | ||
| assertThat(sourceSavepointDir.mkdirs()).isTrue(); | ||
| assertThat(targetSavepointDir.mkdirs()).isTrue(); | ||
|
|
||
| // A retained state file living in the source savepoint, referenced by a relative handle | ||
| // whose full path still points at the file in the source directory (this is what loading | ||
| // an existing savepoint produces). | ||
| byte[] stateBytes = "retained-shared-state".getBytes(StandardCharsets.UTF_8); | ||
| File sourceStateFile = new File(sourceSavepointDir, STATE_FILE_NAME); | ||
| Files.write(sourceStateFile.toPath(), stateBytes); | ||
|
|
||
| Path targetSavepointPath = new Path(targetSavepointDir.getAbsolutePath()); | ||
| Path sourceStatePath = | ||
| new Path(new Path(sourceSavepointDir.getAbsolutePath()), STATE_FILE_NAME); | ||
| RelativeFileStateHandle retainedHandle = | ||
| new RelativeFileStateHandle(sourceStatePath, STATE_FILE_NAME, stateBytes.length); | ||
|
|
||
| // Copy the retained file the way SavepointWriter.fromExistingSavepoint does: through | ||
| // FileCopyFunction, which copies by bare file name into the new savepoint directory. The | ||
| // metadata written below is self-contained only together with this naming contract. | ||
| FileCopyFunction copyFunction = new FileCopyFunction(targetSavepointDir.getAbsolutePath()); | ||
| copyFunction.open(FirstAttemptInitializationContext.of(SUBTASK_INDEX, PARALLELISM)); | ||
| copyFunction.writeRecord(sourceStatePath); | ||
| copyFunction.close(); | ||
|
|
||
| CheckpointMetadata metadata = createMetadata(retainedHandle); | ||
|
|
||
| SavepointOutputFormat format = new SavepointOutputFormat(targetSavepointPath); | ||
| format.setRuntimeContext(new MockStreamingRuntimeContext(PARALLELISM, SUBTASK_INDEX)); | ||
| format.open(FirstAttemptInitializationContext.of(SUBTASK_INDEX, PARALLELISM)); | ||
| format.writeRecord(metadata); | ||
| format.close(); | ||
|
|
||
| // Reload the written metadata; relative handles resolve against the directory the | ||
| // metadata is read from, i.e. the new savepoint holding the copies. | ||
| CheckpointMetadata reloaded = | ||
| SavepointLoader.loadSavepointMetadata(targetSavepointPath.getPath()); | ||
| StreamStateHandle reloadedShared = extractSingleSharedStateHandle(reloaded); | ||
|
|
||
| assertThat(reloadedShared) | ||
| .as( | ||
| "Retained state must reload as a relative handle that resolves inside " | ||
| + "the new savepoint; an absolute path back into the source " | ||
| + "savepoint would break the new savepoint once the source is " | ||
| + "deleted") | ||
| .isInstanceOf(RelativeFileStateHandle.class); | ||
|
|
||
| RelativeFileStateHandle reloadedRelative = (RelativeFileStateHandle) reloadedShared; | ||
| assertThat(reloadedRelative.getRelativePath()).isEqualTo(STATE_FILE_NAME); | ||
| assertThat(reloadedRelative.getFilePath().getPath()) | ||
| .isEqualTo(new Path(targetSavepointPath, STATE_FILE_NAME).getPath()); | ||
| assertThat(new File(reloadedRelative.getFilePath().getPath())).exists(); | ||
| } | ||
|
|
||
| private static CheckpointMetadata createMetadata(RelativeFileStateHandle retainedHandle) { | ||
| StreamStateHandle metaStateHandle = | ||
| new ByteStreamStateHandle( | ||
| "backend-meta", "backend-meta".getBytes(StandardCharsets.UTF_8)); | ||
| IncrementalRemoteKeyedStateHandle keyedStateHandle = | ||
| new IncrementalRemoteKeyedStateHandle( | ||
| UUID.randomUUID(), | ||
| KeyGroupRange.of(0, MAX_PARALLELISM - 1), | ||
| CHECKPOINT_ID, | ||
| Collections.singletonList( | ||
| HandleAndLocalPath.of(retainedHandle, STATE_FILE_NAME)), | ||
| Collections.emptyList(), | ||
| metaStateHandle); | ||
|
|
||
| OperatorSubtaskState subtaskState = | ||
| OperatorSubtaskState.builder().setManagedKeyedState(keyedStateHandle).build(); | ||
|
|
||
| OperatorState operatorState = | ||
| new OperatorState( | ||
| null, | ||
| OPERATOR_UID, | ||
| OperatorIDGenerator.fromUid(OPERATOR_UID), | ||
| PARALLELISM, | ||
| MAX_PARALLELISM); | ||
| operatorState.putState(SUBTASK_INDEX, subtaskState); | ||
|
|
||
| return new CheckpointMetadata( | ||
| CHECKPOINT_ID, Collections.singleton(operatorState), Collections.emptyList()); | ||
| } | ||
|
|
||
| private static StreamStateHandle extractSingleSharedStateHandle(CheckpointMetadata metadata) { | ||
| IncrementalRemoteKeyedStateHandle keyedStateHandle = | ||
| (IncrementalRemoteKeyedStateHandle) | ||
| metadata.getOperatorStates() | ||
| .iterator() | ||
| .next() | ||
| .getState(SUBTASK_INDEX) | ||
| .getManagedKeyedState() | ||
| .iterator() | ||
| .next(); | ||
| return keyedStateHandle.getSharedState().get(0).getHandle(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
|
|
||
| import org.apache.flink.api.common.JobID; | ||
| import org.apache.flink.configuration.Configuration; | ||
| import org.apache.flink.core.fs.Path; | ||
| import org.apache.flink.runtime.OperatorIDPair; | ||
| import org.apache.flink.runtime.checkpoint.metadata.CheckpointMetadata; | ||
| import org.apache.flink.runtime.checkpoint.metadata.MetadataSerializer; | ||
|
|
@@ -28,12 +29,15 @@ | |
| import org.apache.flink.runtime.executiongraph.ExecutionJobVertex; | ||
| import org.apache.flink.runtime.jobgraph.JobVertexID; | ||
| import org.apache.flink.runtime.jobgraph.OperatorID; | ||
| import org.apache.flink.runtime.state.CheckpointMetadataOutputStream; | ||
| import org.apache.flink.runtime.state.CheckpointStorage; | ||
| import org.apache.flink.runtime.state.CheckpointStorageLoader; | ||
| import org.apache.flink.runtime.state.CheckpointedStateScope; | ||
| import org.apache.flink.runtime.state.CompletedCheckpointStorageLocation; | ||
| import org.apache.flink.runtime.state.StateBackend; | ||
| import org.apache.flink.runtime.state.StateBackendLoader; | ||
| import org.apache.flink.runtime.state.StreamStateHandle; | ||
| import org.apache.flink.runtime.state.filesystem.RelativeFileStateHandle; | ||
| import org.apache.flink.runtime.state.hashmap.HashMapStateBackend; | ||
| import org.apache.flink.runtime.state.storage.JobManagerCheckpointStorage; | ||
| import org.apache.flink.util.CollectionUtil; | ||
|
|
@@ -64,6 +68,14 @@ | |
| * <pre>[MagicNumber (int) | Format Version (int) | Checkpoint Metadata (variable)]</pre> | ||
| * | ||
| * <p>The actual savepoint serialization is version-specific via the {@link MetadataSerializer}. | ||
| * | ||
| * <p>Checkpoint metadata may reference state files through {@link RelativeFileStateHandle}s, which | ||
| * store only a file name. On recovery such a reference is resolved against the checkpoint's | ||
| * <i>exclusive directory</i>: the per-checkpoint directory that contains the {@code _metadata} file | ||
| * and the checkpoint's own, non-shared state files, e.g. {@code /checkpoints/<job-id>/chk-42/} (see | ||
| * {@link CheckpointedStateScope#EXCLUSIVE}). This yields the correct path only for files that | ||
| * actually live in that directory, which is why the {@code storeCheckpointMetadata} variants below | ||
| * differ in how they encode relative handles. | ||
| */ | ||
| public class Checkpoints { | ||
|
|
||
|
|
@@ -76,29 +88,85 @@ public class Checkpoints { | |
| // Writing out checkpoint metadata | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| public static void storeCheckpointMetadata( | ||
| /** | ||
| * Stores the checkpoint metadata with every {@link RelativeFileStateHandle} kept relative | ||
| * unconditionally, regardless of where its file currently lives. This is only correct if the | ||
| * caller guarantees that every referenced file is, or will be, located in the directory the | ||
| * metadata is eventually read from. The state processor API establishes that guarantee by | ||
| * copying the referenced files next to the new metadata (see {@code SavepointOutputFormat}); | ||
| * everything else should use {@link #storeCheckpointMetadata(CheckpointMetadata, | ||
| * CheckpointMetadataOutputStream)}. | ||
| * | ||
| * <p>The deliberately different method name (rather than an overload) makes choosing this | ||
| * encoding an explicit decision at the call site. | ||
| */ | ||
| public static void storeCheckpointMetadataWithoutExclusiveDir( | ||
|
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. why do we need to keep the I would expect that every checkpoint we are writing has it's own exclusive directory, and incorrectly referencing relative files outside of that should be explicitly forbidden?
Contributor
Author
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. The main use case is the state processor API's deep copy: the carried-over handles refer to outside the directory at write time, while FileCopyFunction puts a copy of every referenced file next to the new _metadata, so on read all relative references are correct. "Forbidding" would break that usage, which is correct. |
||
| CheckpointMetadata checkpointMetadata, OutputStream out) throws IOException { | ||
|
|
||
| DataOutputStream dos = new DataOutputStream(out); | ||
| storeCheckpointMetadata(checkpointMetadata, dos); | ||
| storeCheckpointMetadataWithoutExclusiveDir(checkpointMetadata, dos); | ||
| } | ||
|
|
||
| /** | ||
| * Stores the checkpoint metadata, letting the serializer check every {@link | ||
| * RelativeFileStateHandle} against the exclusive directory the stream writes into ({@link | ||
| * CheckpointMetadataOutputStream#getExclusiveCheckpointDir()}): a handle whose file lives in | ||
| * that directory keeps the relative encoding, while a handle pointing anywhere else (e.g. an | ||
| * SST file reused from a claimed savepoint) is persisted with its absolute path, because the | ||
| * relative form would be re-anchored to the wrong directory when the metadata is read back. | ||
| * Use this variant when writing an actual checkpoint or savepoint. | ||
| */ | ||
|
Comment on lines
+91
to
+118
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. After reading those two java docs, I don't understand the difference between the two methods. What is the difference between:
? Maybe me and this java doc is lacking explanation of what are exclusive directories?
Contributor
Author
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.
Just to clarify, this PR uses pre-PR dictionary. E.g. in https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/metadata/MetadataV2V3SerializerBase.java#L934 or in https://github.com/apache/flink/blob/master/flink-runtime/src/main/java/org/apache/flink/runtime/state/CheckpointedStateScope.java#L31 we already have this term.
I will work on these java docs.
Contributor
Author
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. java docs updated in 70dff60 |
||
| public static void storeCheckpointMetadata( | ||
| CheckpointMetadata checkpointMetadata, CheckpointMetadataOutputStream out) | ||
| throws IOException { | ||
|
|
||
| DataOutputStream dos = new DataOutputStream(out); | ||
| storeCheckpointMetadata( | ||
| checkpointMetadata, | ||
| dos, | ||
| MetadataV6Serializer.INSTANCE, | ||
| out.getExclusiveCheckpointDir()); | ||
| } | ||
|
|
||
| /** | ||
| * Variant of {@link #storeCheckpointMetadataWithoutExclusiveDir(CheckpointMetadata, | ||
| * OutputStream)} for a {@link DataOutputStream}. | ||
| */ | ||
| public static void storeCheckpointMetadataWithoutExclusiveDir( | ||
| CheckpointMetadata checkpointMetadata, DataOutputStream out) throws IOException { | ||
| storeCheckpointMetadata(checkpointMetadata, out, MetadataV6Serializer.INSTANCE); | ||
| storeCheckpointMetadataWithoutExclusiveDir( | ||
| checkpointMetadata, out, MetadataV6Serializer.INSTANCE); | ||
| } | ||
|
|
||
| public static void storeCheckpointMetadata( | ||
| CheckpointMetadata checkpointMetadata, | ||
| DataOutputStream out, | ||
| @Nullable Path exclusiveDir) | ||
| throws IOException { | ||
| storeCheckpointMetadata( | ||
| checkpointMetadata, out, MetadataV6Serializer.INSTANCE, exclusiveDir); | ||
| } | ||
|
|
||
| public static void storeCheckpointMetadataWithoutExclusiveDir( | ||
| CheckpointMetadata checkpointMetadata, | ||
| DataOutputStream out, | ||
| MetadataSerializer serializer) | ||
| throws IOException { | ||
| storeCheckpointMetadata(checkpointMetadata, out, serializer, null); | ||
| } | ||
|
|
||
| public static void storeCheckpointMetadata( | ||
| CheckpointMetadata checkpointMetadata, | ||
| DataOutputStream out, | ||
| MetadataSerializer serializer, | ||
| @Nullable Path exclusiveDir) | ||
| throws IOException { | ||
|
|
||
| // write generic header | ||
| out.writeInt(HEADER_MAGIC_NUMBER); | ||
|
|
||
| out.writeInt(serializer.getVersion()); | ||
| serializer.serialize(checkpointMetadata, out); | ||
| serializer.serialize(checkpointMetadata, out, exclusiveDir); | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------------ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.