1717
1818package org .apache .doris .avro ;
1919
20+ import org .apache .doris .thrift .TFileType ;
21+
2022import org .apache .avro .Schema ;
2123import org .apache .avro .SchemaBuilder ;
2224import org .apache .avro .file .DataFileWriter ;
4345import org .mockito .Mockito ;
4446
4547import java .io .File ;
48+ import java .io .FileNotFoundException ;
4649import java .io .IOException ;
4750import java .lang .reflect .Field ;
4851import java .lang .reflect .Method ;
5356import java .nio .file .Files ;
5457import java .util .ArrayList ;
5558import java .util .Collections ;
59+ import java .util .HashMap ;
5660import java .util .List ;
5761import java .util .Map ;
5862import java .util .concurrent .atomic .AtomicReference ;
@@ -92,13 +96,12 @@ public void failedDataConstructionClosesInputAndOwnedClient() throws Exception {
9296 try (RemoteClients clients = new RemoteClients (Files .readAllBytes (file .toPath ()))) {
9397 int baseline = cacheSize ();
9498 clients .invalidDataHeader = true ;
95- HDFSFileReader reader = guardedReader (0 );
96- try {
97- Assert .assertThrows (IOException .class , () -> reader .open (context (file .length (), 0 ), false ));
98- Assert .assertNull (reader .dataReader );
99- } finally {
100- reader .close ();
101- }
99+ AvroJNIScanner scanner = scanner (clients , false , file .length ());
100+ Assert .assertThrows (IOException .class , scanner ::open );
101+ Field readerField = AvroJNIScanner .class .getDeclaredField ("avroReader" );
102+ readerField .setAccessible (true );
103+ Assert .assertNull (((AvroReader ) readerField .get (scanner )).dataReader );
104+ Assert .assertEquals (2 , clients .streams .size ());
102105 Assert .assertEquals (1 , clients .clients .size ());
103106 clients .assertClosed ();
104107 Assert .assertEquals (baseline , cacheSize ());
@@ -121,6 +124,62 @@ public void closeFailureStillClosesDataReaderAndOwnedClient() throws Exception {
121124 }
122125 }
123126
127+ @ Test
128+ public void failedProductionSchemaOpenClosesResourcesWithoutCallerClose () throws Exception {
129+ File file = records ();
130+ for (boolean missing : new boolean [] {false , true }) {
131+ try (RemoteClients clients = new RemoteClients (Files .readAllBytes (file .toPath ()))) {
132+ int baseline = cacheSize ();
133+ clients .missingInput = missing ;
134+ clients .truncatedSchema = !missing ;
135+ AvroJNIScanner scanner = scanner (clients , true , file .length ());
136+ Assert .assertThrows (IOException .class , scanner ::open );
137+ Assert .assertEquals (1 , clients .clients .size ());
138+ Assert .assertEquals (missing ? 0 : 1 , clients .streams .size ());
139+ // JNI skips Java close after a failed open; no manual scanner.close here.
140+ clients .assertClosed ();
141+ Assert .assertEquals (baseline , cacheSize ());
142+ }
143+ }
144+ }
145+
146+ @ Test
147+ public void productionOpenPreservesFailureAndSuppressesCleanupErrors () throws Exception {
148+ File file = records ();
149+ try (RemoteClients clients = new RemoteClients (Files .readAllBytes (file .toPath ()))) {
150+ int baseline = cacheSize ();
151+ clients .truncatedSchema = true ;
152+ clients .failStreamClose = true ;
153+ clients .failClientClose = true ;
154+ AvroJNIScanner scanner = scanner (clients , true , file .length ());
155+ IOException failure = Assert .assertThrows (IOException .class , scanner ::open );
156+ Assert .assertFalse (failure .getMessage () != null && failure .getMessage ().startsWith ("injected" ));
157+ Assert .assertEquals (2 , failure .getSuppressed ().length );
158+ clients .assertClosed ();
159+ Assert .assertEquals (baseline , cacheSize ());
160+ }
161+ }
162+
163+ private AvroJNIScanner scanner (RemoteClients clients , boolean schemaOnly , long length ) throws Exception {
164+ Map <String , String > params = new HashMap <>();
165+ params .put (AvroProperties .IS_GET_TABLE_SCHEMA , Boolean .toString (schemaOnly ));
166+ params .put (AvroProperties .FILE_TYPE , Integer .toString (TFileType .FILE_HDFS .getValue ()));
167+ params .put (AvroProperties .URI , "viewfs://cluster/data/records.avro" );
168+ params .put (TvfGuard .POLICY_KEY , TvfGuard .NATIVE_POLICY );
169+ params .put (AvroProperties .COLUMNS_TYPES , "int" );
170+ params .put (AvroProperties .REQUIRED_FIELDS , "id" );
171+ params .put (AvroProperties .HIVE_SERDE , "org.apache.hadoop.hive.serde2.avro.AvroSerDe" );
172+ params .put (AvroProperties .SPLIT_START_OFFSET , "0" );
173+ params .put (AvroProperties .SPLIT_SIZE , Long .toString (length ));
174+ params .put (AvroProperties .SPLIT_FILE_SIZE , Long .toString (length ));
175+ AvroJNIScanner scanner = new AvroJNIScanner (8 , params );
176+ // Give the production scanner its isolated deployment resources, without mocking open/close.
177+ Field loaderField = AvroJNIScanner .class .getDeclaredField ("classLoader" );
178+ loaderField .setAccessible (true );
179+ loaderField .set (scanner , clients .loader );
180+ return scanner ;
181+ }
182+
124183 private HDFSFileReader guardedReader (int index ) {
125184 HDFSFileReader reader = new HDFSFileReader ("viewfs://cluster/data/records-" + index + ".avro" );
126185 reader .setTvfPolicy (TvfGuard .NATIVE_POLICY );
@@ -189,6 +248,10 @@ private final class RemoteClients implements AutoCloseable {
189248 private final Method baseClose ;
190249 private final byte [] bytes ;
191250 private boolean invalidDataHeader ;
251+ private boolean missingInput ;
252+ private boolean truncatedSchema ;
253+ private boolean failStreamClose ;
254+ private boolean failClientClose ;
192255
193256 private RemoteClients (byte [] bytes ) throws Exception {
194257 this .bytes = bytes ;
@@ -230,11 +293,17 @@ private DistributedFileSystem remote(Configuration conf) throws Exception {
230293 Mockito .when (client .getFileStatus (Mockito .any (Path .class ))).thenAnswer (call ->
231294 new FileStatus (bytes .length , false , 1 , 1 , 0 , call .getArgument (0 )));
232295 Mockito .when (client .open (Mockito .any (Path .class ), Mockito .anyInt ())).thenAnswer (call -> {
233- byte [] content = invalidDataHeader && streams .size () % 2 == 1
234- ? new byte [] {0 , 0 , 0 , 0 } : bytes ;
296+ if (missingInput ) {
297+ throw new FileNotFoundException ("recording Avro input is missing" );
298+ }
299+ byte [] content = truncatedSchema ? new byte [] {'O' , 'b' , 'j' , 1 }
300+ : invalidDataHeader && streams .size () % 2 == 1 ? new byte [] {0 , 0 , 0 , 0 } : bytes ;
235301 SeekableByteArrayInput input = new SeekableByteArrayInput (content );
236302 FSDataInputStream stream = Mockito .mock (FSDataInputStream .class );
237303 streams .add (stream );
304+ if (failStreamClose ) {
305+ Mockito .doThrow (new IOException ("injected stream close failure" )).when (stream ).close ();
306+ }
238307 Mockito .when (stream .read (Mockito .any (byte [].class ), Mockito .anyInt (), Mockito .anyInt ()))
239308 .thenAnswer (read -> input .read (read .getArgument (0 ), read .getArgument (1 ), read .getArgument (2 )));
240309 Mockito .when (stream .read ()).thenAnswer (read -> {
@@ -250,6 +319,9 @@ private DistributedFileSystem remote(Configuration conf) throws Exception {
250319 });
251320 Mockito .doAnswer (call -> {
252321 baseClose .invoke (client );
322+ if (failClientClose ) {
323+ throw new IOException ("injected client close failure" );
324+ }
253325 return null ;
254326 }).when (client ).close ();
255327 return client ;
0 commit comments