6
6
7
7
import static org .junit .jupiter .api .Assertions .assertEquals ;
8
8
import static org .junit .jupiter .api .Assertions .assertThrows ;
9
- import static org .mockito .ArgumentMatchers .any ;
9
+ import static org .mockito .ArgumentMatchers .anyInt ;
10
10
import static org .mockito .Mockito .RETURNS_DEEP_STUBS ;
11
- import static org .mockito .Mockito .clearInvocations ;
12
11
import static org .mockito .Mockito .doReturn ;
13
12
import static org .mockito .Mockito .mock ;
13
+ import static org .mockito .Mockito .mockConstructionWithAnswer ;
14
14
import static org .mockito .Mockito .verify ;
15
15
16
+ import alex .mojaki .s3upload .StreamTransferManager ;
16
17
import com .amazonaws .services .s3 .AmazonS3Client ;
17
18
import io .airbyte .db .jdbc .JdbcDatabase ;
18
19
import io .airbyte .integrations .destination .ExtendedNameTransformer ;
19
20
import io .airbyte .integrations .destination .jdbc .SqlOperations ;
20
21
import io .airbyte .integrations .destination .s3 .S3DestinationConfig ;
21
22
import io .airbyte .protocol .models .AirbyteRecordMessage ;
22
23
import io .airbyte .protocol .models .DestinationSyncMode ;
24
+ import java .util .List ;
23
25
import java .util .UUID ;
26
+ import org .junit .jupiter .api .AfterEach ;
24
27
import org .junit .jupiter .api .BeforeEach ;
25
28
import org .junit .jupiter .api .Test ;
26
29
import org .junit .platform .commons .util .ExceptionUtils ;
30
+ import org .mockito .MockedConstruction ;
27
31
28
32
/**
29
33
* Tests to help define what the legacy S3 stream copier did.
30
34
* <p>
31
- * Somewhat sketchily verifies what the AmazonS3Client does, even though the stream copier only actually interacts with it via StreamTransferManager
32
- * instances. The interactions are mostly obvious enough that this feels fine.
33
- * <p>
34
35
* Does not verify SQL operations, as they're fairly transparent.
35
36
*/
36
37
public class LegacyS3StreamCopierTest {
37
38
39
+ public static final int PART_SIZE = 5 ;
40
+
38
41
private AmazonS3Client s3Client ;
39
42
private JdbcDatabase db ;
40
43
private SqlOperations sqlOperations ;
41
44
private LegacyS3StreamCopier copier ;
42
45
46
+ private MockedConstruction <StreamTransferManager > streamTransferManagerMockedConstruction ;
47
+
43
48
@ BeforeEach
44
49
public void setup () {
45
50
s3Client = mock (AmazonS3Client .class , RETURNS_DEEP_STUBS );
46
51
db = mock (JdbcDatabase .class );
47
52
sqlOperations = mock (SqlOperations .class );
48
53
54
+ streamTransferManagerMockedConstruction = mockConstructionWithAnswer (StreamTransferManager .class , RETURNS_DEEP_STUBS );
55
+
49
56
copier = new LegacyS3StreamCopier (
50
57
"fake-staging-folder" ,
51
58
DestinationSyncMode .OVERWRITE ,
@@ -60,6 +67,7 @@ public void setup() {
60
67
"fake-region" ,
61
68
"fake-access-key-id" ,
62
69
"fake-secret-access-key" ,
70
+ PART_SIZE ,
63
71
null
64
72
),
65
73
new ExtendedNameTransformer (),
@@ -74,45 +82,66 @@ public void copyS3CsvFileIntoTable(
74
82
final S3DestinationConfig s3Config ) {
75
83
throw new UnsupportedOperationException ("not implemented" );
76
84
}
77
-
78
85
};
79
86
}
80
87
88
+ @ AfterEach
89
+ public void teardown () {
90
+ streamTransferManagerMockedConstruction .close ();
91
+ }
92
+
81
93
@ Test
82
94
public void createSequentialStagingFiles_when_multipleFilesRequested () {
83
- // Each file will contain multiple parts, so the first MAX_PARTS_PER_FILE will all go into the same file
84
- for (var i = 0 ; i < LegacyS3StreamCopier .MAX_PARTS_PER_FILE ; i ++) {
85
- final String file1 = copier .prepareStagingFile ();
86
- assertEquals ("fake-staging-folder/fake-schema/fake-stream_00000" , file1 , "preparing file number " + i );
95
+ // When we call prepareStagingFile() the first time, it should create exactly one upload manager
96
+ final String firstFile = copier .prepareStagingFile ();
97
+ assertEquals ("fake-staging-folder/fake-schema/fake-stream_00000" , firstFile );
98
+ final List <StreamTransferManager > firstManagers = streamTransferManagerMockedConstruction .constructed ();
99
+ final StreamTransferManager firstManager = firstManagers .get (0 );
100
+ verify (firstManager .numUploadThreads (anyInt ()).queueCapacity (anyInt ())).partSize (PART_SIZE );
101
+ assertEquals (1 , firstManagers .size (), "There were actually " + firstManagers .size () + " upload managers" );
102
+
103
+ // Each file will contain multiple parts, so the first MAX_PARTS_PER_FILE will all go into the same file (i.e. we should not start more uploads)
104
+ // We've already called prepareStagingFile() once, so only go to MAX_PARTS_PER_FILE - 1
105
+ for (var i = 0 ; i < LegacyS3StreamCopier .MAX_PARTS_PER_FILE - 1 ; i ++) {
106
+ final String existingFile = copier .prepareStagingFile ();
107
+ assertEquals ("fake-staging-folder/fake-schema/fake-stream_00000" , existingFile , "preparing file number " + i );
108
+ final int streamManagerCount = streamTransferManagerMockedConstruction .constructed ().size ();
109
+ assertEquals (1 , streamManagerCount , "There were actually " + streamManagerCount + " upload managers" );
87
110
}
88
- verify (s3Client ).initiateMultipartUpload (any ());
89
- clearInvocations (s3Client );
90
111
91
- final String file2 = copier .prepareStagingFile ();
92
- assertEquals ("fake-staging-folder/fake-schema/fake-stream_00001" , file2 );
93
- verify (s3Client ).initiateMultipartUpload (any ());
112
+ // Now that we've hit the MAX_PARTS_PER_FILE, we should start a new upload
113
+ final String secondFile = copier .prepareStagingFile ();
114
+ assertEquals ("fake-staging-folder/fake-schema/fake-stream_00001" , secondFile );
115
+ final List <StreamTransferManager > secondManagers = streamTransferManagerMockedConstruction .constructed ();
116
+ final StreamTransferManager secondManager = secondManagers .get (1 );
117
+ verify (secondManager .numUploadThreads (anyInt ()).queueCapacity (anyInt ())).partSize (PART_SIZE );
118
+ assertEquals (2 , secondManagers .size (), "There were actually " + secondManagers .size () + " upload managers" );
94
119
}
95
120
96
121
@ Test
97
122
public void closesS3Upload_when_stagingUploaderClosedSuccessfully () throws Exception {
98
- final String file = copier .prepareStagingFile ();
99
- copier .write (UUID .randomUUID (), new AirbyteRecordMessage ().withEmittedAt (84L ), file );
123
+ copier .prepareStagingFile ();
100
124
101
125
copier .closeStagingUploader (false );
102
126
103
- verify (s3Client ).completeMultipartUpload (any ());
127
+ final List <StreamTransferManager > managers = streamTransferManagerMockedConstruction .constructed ();
128
+ final StreamTransferManager manager = managers .get (0 );
129
+ verify (manager ).numUploadThreads (10 );
130
+ verify (manager ).complete ();
104
131
}
105
132
106
133
@ Test
107
134
public void closesS3Upload_when_stagingUploaderClosedFailingly () throws Exception {
108
135
final String file = copier .prepareStagingFile ();
136
+ // This is needed to trick the StreamTransferManager into thinking it has data that needs to be written.
109
137
copier .write (UUID .randomUUID (), new AirbyteRecordMessage ().withEmittedAt (84L ), file );
110
138
111
139
// TODO why does this throw an interruptedexception
112
140
final RuntimeException exception = assertThrows (RuntimeException .class , () -> copier .closeStagingUploader (true ));
113
141
114
142
// the wrapping chain is RuntimeException -> ExecutionException -> RuntimeException -> InterruptedException
115
- assertEquals (InterruptedException .class , exception .getCause ().getCause ().getCause ().getClass (), "Original exception: " + ExceptionUtils .readStackTrace (exception ));
143
+ assertEquals (InterruptedException .class , exception .getCause ().getCause ().getCause ().getClass (),
144
+ "Original exception: " + ExceptionUtils .readStackTrace (exception ));
116
145
}
117
146
118
147
@ Test
0 commit comments