|
18 | 18 |
|
19 | 19 | import java.io.IOException; |
20 | 20 | import java.io.InputStream; |
| 21 | +import java.nio.ByteBuffer; |
21 | 22 | import java.nio.channels.ClosedByInterruptException; |
| 23 | +import java.nio.charset.StandardCharsets; |
22 | 24 | import java.nio.file.Files; |
23 | 25 | import java.nio.file.Path; |
24 | 26 | import java.util.Map; |
| 27 | +import java.util.Optional; |
25 | 28 | import java.util.stream.Stream; |
26 | 29 |
|
27 | 30 | import org.apache.kafka.common.TopicIdPartition; |
28 | 31 | import org.apache.kafka.common.TopicPartition; |
29 | 32 | import org.apache.kafka.common.Uuid; |
| 33 | +import org.apache.kafka.server.log.remote.storage.LogSegmentData; |
30 | 34 | import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentId; |
31 | 35 | import org.apache.kafka.server.log.remote.storage.RemoteLogSegmentMetadata; |
32 | 36 | import org.apache.kafka.server.log.remote.storage.RemoteStorageException; |
|
36 | 40 | import io.aiven.kafka.tieredstorage.storage.StorageBackendException; |
37 | 41 |
|
38 | 42 | import org.junit.jupiter.api.BeforeEach; |
| 43 | +import org.junit.jupiter.api.Test; |
39 | 44 | import org.junit.jupiter.api.io.TempDir; |
40 | 45 | import org.junit.jupiter.params.ParameterizedTest; |
41 | 46 | import org.junit.jupiter.params.provider.Arguments; |
|
45 | 50 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
46 | 51 | import static org.junit.jupiter.params.provider.Arguments.arguments; |
47 | 52 | import static org.mockito.ArgumentMatchers.any; |
| 53 | +import static org.mockito.ArgumentMatchers.anyBoolean; |
48 | 54 | import static org.mockito.ArgumentMatchers.anyInt; |
| 55 | +import static org.mockito.Mockito.doCallRealMethod; |
| 56 | +import static org.mockito.Mockito.doThrow; |
49 | 57 | import static org.mockito.Mockito.mock; |
| 58 | +import static org.mockito.Mockito.spy; |
50 | 59 | import static org.mockito.Mockito.when; |
51 | 60 |
|
52 | 61 | class RemoteStorageManagerTest { |
@@ -221,6 +230,85 @@ void fetchSegmentNonInterruptionExceptionWhenGettingSegment( |
221 | 230 | .hasRootCauseInstanceOf(exceptionClass); |
222 | 231 | } |
223 | 232 |
|
| 233 | + @Test |
| 234 | + void deleteObjectsWhenUploadFails( |
| 235 | + @TempDir final Path partitionDir |
| 236 | + ) throws IOException, StorageBackendException, RemoteStorageException { |
| 237 | + // given a sample local segment to be uploaded |
| 238 | + final var segmentPath = Files.createFile(partitionDir.resolve("0000.log")); |
| 239 | + final var segmentContent = "test"; |
| 240 | + Files.writeString(segmentPath, segmentContent); |
| 241 | + final var indexPath = Files.createFile(partitionDir.resolve("0000.index")); |
| 242 | + final var timeIndexPath = Files.createFile(partitionDir.resolve("0000.timeindex")); |
| 243 | + final var producerSnapshotPath = Files.createFile(partitionDir.resolve("0000.snapshot")); |
| 244 | + final var logSegmentData = new LogSegmentData( |
| 245 | + segmentPath, |
| 246 | + indexPath, |
| 247 | + timeIndexPath, |
| 248 | + Optional.empty(), |
| 249 | + producerSnapshotPath, |
| 250 | + ByteBuffer.wrap("test".getBytes(StandardCharsets.UTF_8)) |
| 251 | + ); |
| 252 | + |
| 253 | + final var remoteLogSegmentMetadata = new RemoteLogSegmentMetadata( |
| 254 | + REMOTE_SEGMENT_ID, 0, 1L, |
| 255 | + 0, 0, 0, segmentContent.length(), Map.of(0, 0L)); |
| 256 | + |
| 257 | + final var remotePartitionPath = targetDir.resolve(TOPIC_ID_PARTITION.topic() + "-" + TOPIC_ID) |
| 258 | + .resolve(String.valueOf(TOPIC_ID_PARTITION.partition())); |
| 259 | + |
| 260 | + final var config = Map.of( |
| 261 | + "chunk.size", "1", |
| 262 | + "storage.backend.class", "io.aiven.kafka.tieredstorage.storage.filesystem.FileSystemStorage", |
| 263 | + "storage.root", targetDir.toString() |
| 264 | + ); |
| 265 | + rsm.configure(config); |
| 266 | + rsm = spy(rsm); |
| 267 | + |
| 268 | + // when first upload fails |
| 269 | + doThrow(IOException.class).when(rsm).uploadSegmentLog(any(), any(), anyBoolean(), any(), any()); |
| 270 | + |
| 271 | + assertThatThrownBy(() -> rsm.copyLogSegmentData(remoteLogSegmentMetadata, logSegmentData)) |
| 272 | + .isInstanceOf(RemoteStorageException.class) |
| 273 | + .hasRootCauseInstanceOf(IOException.class); |
| 274 | + |
| 275 | + // then no files stored in remote |
| 276 | + assertThat(remotePartitionPath).doesNotExist(); |
| 277 | + |
| 278 | + // fallback to real method |
| 279 | + doCallRealMethod().when(rsm).uploadSegmentLog(any(), any(), anyBoolean(), any(), any()); |
| 280 | + |
| 281 | + // when second upload fails |
| 282 | + doThrow(IOException.class).when(rsm).uploadIndexes(any(), any(), any(), any()); |
| 283 | + |
| 284 | + assertThatThrownBy(() -> rsm.copyLogSegmentData(remoteLogSegmentMetadata, logSegmentData)) |
| 285 | + .isInstanceOf(RemoteStorageException.class) |
| 286 | + .hasRootCauseInstanceOf(IOException.class); |
| 287 | + |
| 288 | + // then no files stored in remote |
| 289 | + assertThat(remotePartitionPath).doesNotExist(); |
| 290 | + |
| 291 | + // fallback to real method |
| 292 | + doCallRealMethod().when(rsm).uploadIndexes(any(), any(), any(), any()); |
| 293 | + |
| 294 | + // when third upload fails |
| 295 | + doThrow(IOException.class).when(rsm).uploadManifest(any(), any(), any(), anyBoolean(), any(), any()); |
| 296 | + |
| 297 | + assertThatThrownBy(() -> rsm.copyLogSegmentData(remoteLogSegmentMetadata, logSegmentData)) |
| 298 | + .isInstanceOf(RemoteStorageException.class) |
| 299 | + .hasRootCauseInstanceOf(IOException.class); |
| 300 | + |
| 301 | + // then no files stored in remote |
| 302 | + assertThat(remotePartitionPath).doesNotExist(); |
| 303 | + |
| 304 | + // fallback to real method |
| 305 | + doCallRealMethod().when(rsm).uploadManifest(any(), any(), any(), anyBoolean(), any(), any()); |
| 306 | + |
| 307 | + // when all good |
| 308 | + rsm.copyLogSegmentData(remoteLogSegmentMetadata, logSegmentData); |
| 309 | + assertThat(Files.list(remotePartitionPath)).hasSize(3); |
| 310 | + } |
| 311 | + |
224 | 312 | static Stream<Arguments> provideNonInterruptionExceptions() { |
225 | 313 | return Stream.of( |
226 | 314 | arguments(null, Exception.class), |
|
0 commit comments