|
19 | 19 | import static org.assertj.core.api.Assertions.assertThat; |
20 | 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
21 | 21 | import static org.mockito.ArgumentMatchers.any; |
| 22 | +import static org.mockito.Mockito.mock; |
22 | 23 | import static org.mockito.Mockito.never; |
23 | 24 | import static org.mockito.Mockito.verify; |
| 25 | +import static org.mockito.Mockito.when; |
24 | 26 |
|
25 | 27 | import java.io.BufferedReader; |
26 | 28 | import java.io.IOException; |
@@ -431,9 +433,7 @@ void failedForStringValuesByDefault() { |
431 | 433 |
|
432 | 434 | ); |
433 | 435 |
|
434 | | - task.put(records); |
435 | | - |
436 | | - assertThatThrownBy(() -> task.flush(null)).isInstanceOf(ConnectException.class) |
| 436 | + assertThatThrownBy(() -> task.put(records)).isInstanceOf(ConnectException.class) |
437 | 437 | .hasMessage("Record value schema type must be BYTES, STRING given"); |
438 | 438 | } |
439 | 439 |
|
@@ -501,9 +501,7 @@ void failedForStructValuesByDefault() { |
501 | 501 | createRecordWithStructValueSchema("topic0", 1, "key1", "name1", 20, 1001), |
502 | 502 | createRecordWithStructValueSchema("topic1", 0, "key2", "name2", 30, 1002)); |
503 | 503 |
|
504 | | - task.put(records); |
505 | | - |
506 | | - assertThatThrownBy(() -> task.flush(null)).isInstanceOf(ConnectException.class) |
| 504 | + assertThatThrownBy(() -> task.put(records)).isInstanceOf(ConnectException.class) |
507 | 505 | .hasMessage("Record value schema type must be BYTES, STRUCT given"); |
508 | 506 | } |
509 | 507 |
|
@@ -689,17 +687,92 @@ void supportUnwrappedJsonEnvelopeForStructAndClassicJson() throws IOException { |
689 | 687 | void requestCredentialProviderFromFactoryOnStart() { |
690 | 688 | final S3SinkTask task = new S3SinkTask(); |
691 | 689 |
|
692 | | - final AwsCredentialProviderFactory mockedFactory = Mockito.mock(AwsCredentialProviderFactory.class); |
693 | | - final AWSCredentialsProvider provider = Mockito.mock(AWSCredentialsProvider.class); |
| 690 | + final AwsCredentialProviderFactory mockedFactory = mock(AwsCredentialProviderFactory.class); |
| 691 | + final AWSCredentialsProvider provider = mock(AWSCredentialsProvider.class); |
694 | 692 |
|
695 | 693 | task.credentialFactory = mockedFactory; |
696 | | - Mockito.when(mockedFactory.getProvider(any(S3SinkConfig.class))).thenReturn(provider); |
| 694 | + when(mockedFactory.getProvider(any(S3SinkConfig.class))).thenReturn(provider); |
697 | 695 |
|
698 | 696 | task.start(properties); |
699 | 697 |
|
700 | 698 | verify(mockedFactory, Mockito.times(1)).getProvider(any(S3SinkConfig.class)); |
701 | 699 | } |
702 | 700 |
|
| 701 | + @Test |
| 702 | + void mutliPartUploadWriteOnlyExpectedRecordsAndFilesToS3() throws IOException { |
| 703 | + final String compression = "none"; |
| 704 | + properties.put(S3SinkConfig.FILE_COMPRESSION_TYPE_CONFIG, compression); |
| 705 | + properties.put(S3SinkConfig.FORMAT_OUTPUT_FIELDS_CONFIG, "value"); |
| 706 | + properties.put(S3SinkConfig.FORMAT_OUTPUT_ENVELOPE_CONFIG, "false"); |
| 707 | + properties.put(S3SinkConfig.FORMAT_OUTPUT_TYPE_CONFIG, "json"); |
| 708 | + properties.put(S3SinkConfig.AWS_S3_PREFIX_CONFIG, "prefix-"); |
| 709 | + |
| 710 | + final S3SinkTask task = new S3SinkTask(); |
| 711 | + task.start(properties); |
| 712 | + int timestamp = 1000; |
| 713 | + int offset1 = 10; |
| 714 | + int offset2 = 20; |
| 715 | + int offset3 = 30; |
| 716 | + final List<List<SinkRecord>> allRecords = new ArrayList<>(); |
| 717 | + for (int i = 0; i < 3; i++) { |
| 718 | + allRecords.add( |
| 719 | + List.of(createRecordWithStructValueSchema("topic0", 0, "key0", "name0", offset1++, timestamp++), |
| 720 | + createRecordWithStructValueSchema("topic0", 1, "key1", "name1", offset2++, timestamp++), |
| 721 | + createRecordWithStructValueSchema("topic1", 0, "key2", "name2", offset3++, timestamp++))); |
| 722 | + } |
| 723 | + final TopicPartition tp00 = new TopicPartition("topic0", 0); |
| 724 | + final TopicPartition tp01 = new TopicPartition("topic0", 1); |
| 725 | + final TopicPartition tp10 = new TopicPartition("topic1", 0); |
| 726 | + final Collection<TopicPartition> tps = List.of(tp00, tp01, tp10); |
| 727 | + task.open(tps); |
| 728 | + |
| 729 | + allRecords.forEach(task::put); |
| 730 | + |
| 731 | + final Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>(); |
| 732 | + offsets.put(tp00, new OffsetAndMetadata(offset1)); |
| 733 | + offsets.put(tp01, new OffsetAndMetadata(offset2)); |
| 734 | + offsets.put(tp10, new OffsetAndMetadata(offset3)); |
| 735 | + task.flush(offsets); |
| 736 | + |
| 737 | + final CompressionType compressionType = CompressionType.forName(compression); |
| 738 | + |
| 739 | + List<String> expectedBlobs = Lists.newArrayList( |
| 740 | + "prefix-topic0-0-00000000000000000010" + compressionType.extension(), |
| 741 | + "prefix-topic0-1-00000000000000000020" + compressionType.extension(), |
| 742 | + "prefix-topic1-0-00000000000000000030" + compressionType.extension()); |
| 743 | + assertThat(expectedBlobs).allMatch(blobName -> testBucketAccessor.doesObjectExist(blobName)); |
| 744 | + |
| 745 | + assertThat(testBucketAccessor.readLines("prefix-topic0-0-00000000000000000010", compression)) |
| 746 | + .containsExactly("[", "{\"name\":\"name0\"},", "{\"name\":\"name0\"},", "{\"name\":\"name0\"}", "]"); |
| 747 | + assertThat(testBucketAccessor.readLines("prefix-topic0-1-00000000000000000020", compression)) |
| 748 | + .containsExactly("[", "{\"name\":\"name1\"},", "{\"name\":\"name1\"},", "{\"name\":\"name1\"}", "]"); |
| 749 | + assertThat(testBucketAccessor.readLines("prefix-topic1-0-00000000000000000030", compression)) |
| 750 | + .containsExactly("[", "{\"name\":\"name2\"},", "{\"name\":\"name2\"},", "{\"name\":\"name2\"}", "]"); |
| 751 | + // Reset and send another batch of records to S3 |
| 752 | + allRecords.clear(); |
| 753 | + for (int i = 0; i < 3; i++) { |
| 754 | + allRecords.add( |
| 755 | + List.of(createRecordWithStructValueSchema("topic0", 0, "key0", "name0", offset1++, timestamp++), |
| 756 | + createRecordWithStructValueSchema("topic0", 1, "key1", "name1", offset2++, timestamp++), |
| 757 | + createRecordWithStructValueSchema("topic1", 0, "key2", "name2", offset3++, timestamp++))); |
| 758 | + } |
| 759 | + allRecords.forEach(task::put); |
| 760 | + offsets.clear(); |
| 761 | + offsets.put(tp00, new OffsetAndMetadata(offset1)); |
| 762 | + offsets.put(tp01, new OffsetAndMetadata(offset2)); |
| 763 | + offsets.put(tp10, new OffsetAndMetadata(offset3)); |
| 764 | + task.flush(offsets); |
| 765 | + expectedBlobs.clear(); |
| 766 | + expectedBlobs = Lists.newArrayList("prefix-topic0-0-00000000000000000010" + compressionType.extension(), |
| 767 | + "prefix-topic0-1-00000000000000000020" + compressionType.extension(), |
| 768 | + "prefix-topic1-0-00000000000000000030" + compressionType.extension(), |
| 769 | + "prefix-topic0-0-00000000000000000013" + compressionType.extension(), |
| 770 | + "prefix-topic0-1-00000000000000000023" + compressionType.extension(), |
| 771 | + "prefix-topic1-0-00000000000000000033" + compressionType.extension()); |
| 772 | + assertThat(expectedBlobs).allMatch(blobName -> testBucketAccessor.doesObjectExist(blobName)); |
| 773 | + |
| 774 | + } |
| 775 | + |
703 | 776 | private SinkRecord createRecordWithStringValueSchema(final String topic, final int partition, final String key, |
704 | 777 | final String value, final int offset, final long timestamp) { |
705 | 778 | return new SinkRecord(topic, partition, Schema.STRING_SCHEMA, key, Schema.STRING_SCHEMA, value, offset, |
|
0 commit comments