-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix(timeline-service): fail marker creation when create request fails #19369
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 |
|---|---|---|
|
|
@@ -28,8 +28,10 @@ | |
| import org.apache.hudi.common.util.HoodieTimer; | ||
| import org.apache.hudi.common.util.Option; | ||
| import org.apache.hudi.common.util.StringUtils; | ||
| import org.apache.hudi.common.util.VisibleForTesting; | ||
| import org.apache.hudi.config.HoodieWriteConfig; | ||
| import org.apache.hudi.exception.HoodieEarlyConflictDetectionException; | ||
| import org.apache.hudi.exception.HoodieIOException; | ||
| import org.apache.hudi.exception.HoodieRemoteException; | ||
| import org.apache.hudi.storage.StoragePath; | ||
| import org.apache.hudi.table.HoodieTable; | ||
|
|
@@ -137,7 +139,7 @@ protected Option<StoragePath> create(String partitionPath, String fileName, IOTy | |
| if (success) { | ||
| return Option.of(new StoragePath(FSUtils.constructAbsolutePath(markerDirPath, partitionPath), markerFileName)); | ||
| } else { | ||
| return Option.empty(); | ||
| throw new HoodieIOException("[timeline-server-based] Failed to create marker for partition " + partitionPath + ", fileName " + fileName + " with IOType " + type); | ||
|
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. 🤖 This |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -168,7 +170,8 @@ public Option<StoragePath> createWithEarlyConflictDetection(String partitionPath | |
| * @param markerFileName Marker file name. | ||
| * @return {@code true} if successful; {@code false} otherwise. | ||
| */ | ||
| private boolean executeCreateMarkerRequest(Map<String, String> paramsMap, String partitionPath, String markerFileName) { | ||
| @VisibleForTesting | ||
| boolean executeCreateMarkerRequest(Map<String, String> paramsMap, String partitionPath, String markerFileName) { | ||
| boolean success; | ||
| try { | ||
| success = executeRequestToTimelineServer( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,14 +23,17 @@ | |
| import org.apache.hudi.common.config.HoodieMetadataConfig; | ||
| import org.apache.hudi.common.engine.HoodieEngineContext; | ||
| import org.apache.hudi.common.engine.HoodieLocalEngineContext; | ||
| import org.apache.hudi.common.model.IOType; | ||
| import org.apache.hudi.common.table.marker.MarkerType; | ||
| import org.apache.hudi.common.table.view.FileSystemViewManager; | ||
| import org.apache.hudi.common.table.view.FileSystemViewStorageConfig; | ||
| import org.apache.hudi.common.table.view.FileSystemViewStorageType; | ||
| import org.apache.hudi.common.util.MarkerUtils; | ||
| import org.apache.hudi.exception.HoodieIOException; | ||
| import org.apache.hudi.exception.HoodieRemoteException; | ||
| import org.apache.hudi.io.util.FileIOUtils; | ||
| import org.apache.hudi.storage.StoragePath; | ||
| import org.apache.hudi.table.HoodieTable; | ||
| import org.apache.hudi.testutils.HoodieClientTestUtils; | ||
| import org.apache.hudi.timeline.service.TimelineService; | ||
| import org.apache.hudi.timeline.service.TimelineServiceTestHarness; | ||
|
|
@@ -40,6 +43,7 @@ | |
| import org.apache.spark.api.java.JavaSparkContext; | ||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.function.Executable; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.EnumSource; | ||
|
|
@@ -49,13 +53,15 @@ | |
| import java.io.InputStream; | ||
| import java.util.Collection; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import static org.apache.hudi.common.table.view.FileSystemViewStorageType.SPILLABLE_DISK; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertIterableEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assertions.fail; | ||
|
|
||
| @Slf4j | ||
| public class TestTimelineServerBasedWriteMarkers extends TestWriteMarkersBase { | ||
|
|
@@ -162,6 +168,54 @@ private void restartServerAndClient(int numberOfSimulatedConnectionFailures, | |
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testMarkerCreationFailure() throws IOException { | ||
| FileSystemViewStorageConfig.Builder builder = FileSystemViewStorageConfig.newBuilder().withRemoteServerHost("localhost") | ||
| .withRemoteServerPort(timelineService.getServerPort()) | ||
| .withRemoteTimelineClientTimeoutSecs(DEFAULT_READ_TIMEOUT_SECS); | ||
| MockTimelineServerBasedWriteMarkers timelineServerBasedWriteMarkers = new MockTimelineServerBasedWriteMarkers(basePath, markerFolderPath.toString(), "000", builder.build()); | ||
| // this should succeed. | ||
| timelineServerBasedWriteMarkers.create("2020/06/01", "file1", IOType.MERGE); | ||
|
|
||
| assertTrue(storage.exists(markerFolderPath)); | ||
| assertTrue(writeMarkers.doesMarkerDirExist()); | ||
|
|
||
| // lets fail the marker creation | ||
| timelineServerBasedWriteMarkers.failMarkerCreation = true; | ||
| try { | ||
| timelineServerBasedWriteMarkers.create("2020/06/01", "file2", IOType.MERGE); | ||
| fail("Should not have reached here"); | ||
| } catch (HoodieIOException ioe) { | ||
| assertTrue(ioe.getMessage().contains("[timeline-server-based] Failed to create marker for partition")); | ||
| } finally { | ||
|
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. @AfterEach cleanup() already closes timelineService, so closing it again in this finally double-closes the service. Drop the finally - the try only needs to wrap the failing create. |
||
| if (timelineService != null) { | ||
| timelineService.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static class MockTimelineServerBasedWriteMarkers extends TimelineServerBasedWriteMarkers { | ||
|
|
||
| boolean failMarkerCreation = false; | ||
|
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. 🤖 nit: |
||
|
|
||
| public MockTimelineServerBasedWriteMarkers(HoodieTable table, String instantTime) { | ||
|
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. This MockTimelineServerBasedWriteMarkers(HoodieTable, String) constructor is unused - the test builds the mock through the (String, String, String, FileSystemViewStorageConfig) constructor. Remove it. |
||
| super(table, instantTime); | ||
| } | ||
|
|
||
| MockTimelineServerBasedWriteMarkers(String basePath, String markerFolderPath, String instantTime, FileSystemViewStorageConfig fileSystemViewStorageConfig) { | ||
| super(basePath, markerFolderPath, instantTime, fileSystemViewStorageConfig); | ||
| } | ||
|
|
||
| @Override | ||
| boolean executeCreateMarkerRequest(Map<String, String> paramsMap, String partitionPath, String markerFileName) { | ||
| if (!failMarkerCreation) { | ||
| return super.executeCreateMarkerRequest(paramsMap, partitionPath, markerFileName); | ||
|
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. 🤖 nit: the |
||
| } else { | ||
| return false; | ||
|
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. Returning false here mirrors a create failure, but the real server also returns false when the marker already exists (MarkerDirState: !exists), so the test cannot tell a genuine failure from a benign already-exists and never covers createIfNotExists on an existing marker. Add a case that creates the same marker twice to lock down the intended behavior. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private static TimelineServerBasedWriteMarkers initWriteMarkers(String basePath, | ||
| String markerFolderPath, | ||
| int serverPort, | ||
|
|
||
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.
A false result here does not mean the create failed: the server returns false when the marker already exists (MarkerDirState sets isSuccessful to !exists), and a genuine I/O failure already surfaces as HoodieRemoteException, not as false. So this throw fires on an already-existing marker, which createIfNotExists/createLogMarkerIfNotExists treat as a harmless Option.empty no-op - is turning already-exists into a hard failure intended, or should the throw be limited to a real create failure?
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.
hey @wombatu-kun : thanks for the note.
none of our code path should try to create an already existing marker. every file is immutable including log files. and task retries will use a diff write token. so, a client can never ask for to create a marker that already exists.
let me know if I am missing any case here
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.
🤖 One case worth double-checking here:
createIfNotExists/createLogMarkerIfNotExistsroute through this same overriddencreate(..., checkIfExists=true), and this timeline-server impl ignorescheckIfExists. So the server returningfalsefor an already-existing marker (MarkerDirState#processMarkerCreationRequestsdoesfuture.setIsSuccessful(!exists)) now throws for those callers too, where it previously returned a benignOption.empty().A couple of real callers depend on that no-op rather than a fresh write token:
HoodieWriteHandle#getLogCreationCallbackcallscreateLogMarkerIfNotExists(...).isPresent()(so an existing APPEND log marker is expected to yield empty, not an exception), andRollbackHelperV1usescreateIfNotExistsfor APPEND markers during rollback retries. Since genuine I/O failures already surface asHoodieRemoteException, would it be safer to scope the throw to thecheckIfExists == falsepath (or have the server distinguish already-exists from a real create failure) so the*IfNotExistscontract is preserved?