This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 18b118249ca branch-4.2: [fix](fe) Refresh S3 streaming source file 
metadata #69363 (#69521)
18b118249ca is described below

commit 18b118249cad31506f39e22ff2da2e02bc2017fe
Author: wudi <[email protected]>
AuthorDate: Sat Oct 24 10:30:05 2026 +0800

    branch-4.2: [fix](fe) Refresh S3 streaming source file metadata #69363 
(#69521)
    
    Cherry-picked from #69363
---
 be/src/exec/scan/file_scanner.cpp                  |  6 +++
 be/src/exec/scan/file_scanner_v2.cpp               |  7 ++++
 .../job/offset/s3/S3EventSourceOffsetProvider.java |  6 +--
 .../job/offset/s3/S3SourceOffsetProvider.java      |  3 +-
 .../ExternalFileTableValuedFunction.java           | 26 +++++++++---
 .../offset/s3/S3EventSourceOffsetProviderTest.java | 28 +++++++++----
 .../job/offset/s3/S3SourceOffsetProviderTest.java  | 29 +++++++++++++
 .../tablefunction/S3TableValuedFunctionTest.java   | 48 +++++++++++++++++++++-
 .../test_streaming_insert_job_s3_event_aksk.groovy | 21 +++-------
 9 files changed, 139 insertions(+), 35 deletions(-)

diff --git a/be/src/exec/scan/file_scanner.cpp 
b/be/src/exec/scan/file_scanner.cpp
index ee5203f16f0..0a172da4216 100644
--- a/be/src/exec/scan/file_scanner.cpp
+++ b/be/src/exec/scan/file_scanner.cpp
@@ -540,6 +540,8 @@ Status FileScanner::_get_block_wrapped(RuntimeState* state, 
Block* block, bool*
                                           
config::ignore_not_found_file_in_external_table)) {
                 _cur_reader_eof = true;
                 COUNTER_UPDATE(_not_found_file_counter, 1);
+                LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                          << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
                 continue;
             } else if (st.is<ErrorCode::END_OF_FILE>()) {
                 _cur_reader_eof = true;
@@ -580,6 +582,8 @@ Status FileScanner::_get_block_wrapped(RuntimeState* state, 
Block* block, bool*
             if (st.is<ErrorCode::NOT_FOUND>() && 
config::ignore_not_found_file_in_external_table) {
                 _cur_reader_eof = true;
                 COUNTER_UPDATE(_not_found_file_counter, 1);
+                LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                          << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
                 continue;
             }
             RETURN_IF_ERROR(st);
@@ -1324,6 +1328,8 @@ Status FileScanner::_get_next_reader() {
             if (can_ignore_not_found_file(_current_range,
                                           
config::ignore_not_found_file_in_external_table)) {
                 COUNTER_UPDATE(_not_found_file_counter, 1);
+                LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                          << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
                 continue;
             }
             return Status::InternalError("failed to find reader, err: {}", 
init_status.to_string());
diff --git a/be/src/exec/scan/file_scanner_v2.cpp 
b/be/src/exec/scan/file_scanner_v2.cpp
index ab055f61dc3..2ad9897a6bb 100644
--- a/be/src/exec/scan/file_scanner_v2.cpp
+++ b/be/src/exec/scan/file_scanner_v2.cpp
@@ -30,6 +30,7 @@
 #include "common/cast_set.h"
 #include "common/config.h"
 #include "common/consts.h"
+#include "common/logging.h"
 #include "common/metrics/doris_metrics.h"
 #include "common/status.h"
 #include "core/assert_cast.h"
@@ -569,6 +570,8 @@ Status FileScannerV2::_get_block_impl(RuntimeState* state, 
Block* block, bool* e
                                 _current_range, 
config::ignore_not_found_file_in_external_table))) {
                 RETURN_IF_ERROR(_table_reader->abort_split());
                 COUNTER_UPDATE(_not_found_file_counter, 1);
+                LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                          << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
                 RETURN_IF_ERROR(_complete_current_split());
                 _has_prepared_split = false;
                 
block->clear_column_data(cast_set<int64_t>(_projected_columns.size()));
@@ -659,6 +662,8 @@ Status FileScannerV2::_prepare_next_split(bool* eos) {
                                               
config::ignore_not_found_file_in_external_table))) {
             RETURN_IF_ERROR(_table_reader->abort_split());
             COUNTER_UPDATE(_not_found_file_counter, 1);
+            LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                      << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
             RETURN_IF_ERROR(_complete_current_split());
             continue;
         }
@@ -692,6 +697,8 @@ Status FileScannerV2::_prepare_next_split(bool* eos) {
             if (ignored_split_status == IgnoredSplitStatus::NOT_FOUND) {
                 RETURN_IF_ERROR(_table_reader->abort_split());
                 COUNTER_UPDATE(_not_found_file_counter, 1);
+                LOG(INFO) << "Skip missing file: query_id=" << 
print_id(_state->query_id())
+                          << ", path=" << _current_range.path << ", 
reason=NOT_FOUND";
                 RETURN_IF_ERROR(_complete_current_split());
                 continue;
             }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProvider.java
 
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProvider.java
index 1a949a9eb59..687127e2f03 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProvider.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProvider.java
@@ -572,10 +572,10 @@ public class S3EventSourceOffsetProvider implements 
SourceOffsetProvider {
             if (isCommitted(key, message.messageId(), 
message.receiptHandle())) {
                 continue;
             }
+            String path = "s3://" + bucket + "/" + key;
             files.add(key);
-            // The object must remain unchanged until ingestion completes; 
notifications identify keys, not versions.
-            fileStatuses.add(new TBrokerFileStatus("s3://" + bucket + "/" + 
key, false,
-                    record.getS3().getObject().getSizeAsLong(), true));
+            fileStatuses.add(new TBrokerFileStatus(path, false, 
record.getS3().getObject().getSizeAsLong(), true)
+                    .setModificationTime(System.currentTimeMillis()));
             if (record.getEventTime() != null) {
                 eventTimestampSeconds = Math.max(eventTimestampSeconds, 
record.getEventTime().getMillis() / 1000);
             }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3SourceOffsetProvider.java
 
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3SourceOffsetProvider.java
index 02b22a95ed5..72427f0dd0a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3SourceOffsetProvider.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/s3/S3SourceOffsetProvider.java
@@ -97,7 +97,8 @@ public class S3SourceOffsetProvider implements 
SourceOffsetProvider {
                 String beginFile = rfiles.get(0).getName().replace(bucketBase, 
"");
                 String lastFile = rfiles.get(rfiles.size() - 
1).getName().replace(bucketBase, "");
                 offset.setFileStatuses(rfiles.stream().map(file -> new 
TBrokerFileStatus(
-                        file.getName(), file.isDirectory(), file.getSize(), 
!file.isDirectory()))
+                        file.getName(), file.isDirectory(), file.getSize(), 
!file.isDirectory())
+                        .setModificationTime(file.getModificationTime()))
                         .collect(Collectors.toList()));
                 offset.setStartFile(beginFile);
                 offset.setEndFile(lastFile);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
index c26bc2f472f..bfdf1d21437 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
@@ -104,6 +104,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -333,7 +334,6 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
             return columns;
         }
         // get one BE address
-        columns = Lists.newArrayList();
         Backend be = getBackend();
         if (be == null) {
             String computeGroupHints = "";
@@ -359,17 +359,28 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
 
         TNetworkAddress address = new TNetworkAddress(be.getHost(), 
be.getBrpcPort());
         try {
-            PFetchTableSchemaRequest request = getFetchTableStructureRequest();
+            if (getTFileType() == TFileType.FILE_STREAM) {
+                fileStatuses.add(new TBrokerFileStatus("", false, -1, true));
+            }
+            Iterator<TBrokerFileStatus> files = fileStatuses.iterator();
+            PFetchTableSchemaRequest request = 
getFetchTableStructureRequest(files);
             InternalService.PFetchTableSchemaResult result = null;
 
             // `request == null` means we don't need to get schemas from BE,
             // and we fill a dummy col for this table.
-            if (request != null) {
+            while (request != null) {
                 Future<InternalService.PFetchTableSchemaResult> future = 
BackendServiceProxy.getInstance()
                         .fetchTableStructureAsync(address, request);
 
                 result = future.get();
                 TStatusCode code = 
TStatusCode.findByValue(result.getStatus().getStatusCode());
+                // Lance uses a dataset URI rather than an iterator over 
individual files.
+                if (code == TStatusCode.NOT_FOUND && !isLanceFormat()) {
+                    request = getFetchTableStructureRequest(files);
+                    if (request != null) {
+                        continue;
+                    }
+                }
                 String errMsg;
                 if (code != TStatusCode.OK) {
                     if (!result.getStatus().getErrorMsgsList().isEmpty()) {
@@ -381,7 +392,9 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
                     }
                     throw new AnalysisException(errMsg);
                 }
+                break;
             }
+            columns = Lists.newArrayList();
             fillColumns(result);
         } catch (RpcException e) {
             throw new AnalysisException("fetchTableStructureResult rpc 
exception", e);
@@ -568,7 +581,8 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
         }
     }
 
-    private PFetchTableSchemaRequest getFetchTableStructureRequest() throws 
TException {
+    private PFetchTableSchemaRequest 
getFetchTableStructureRequest(Iterator<TBrokerFileStatus> files)
+            throws TException {
         // set TFileScanRangeParams
         TFileScanRangeParams fileScanRangeParams = new TFileScanRangeParams();
         
fileScanRangeParams.setFormatType(fileFormatProperties.getFileFormatType());
@@ -597,7 +611,6 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
         }
 
         if (getTFileType() == TFileType.FILE_STREAM) {
-            fileStatuses.add(new TBrokerFileStatus("", false, -1, true));
             fileScanRangeParams.setFileType(getTFileType());
         }
 
@@ -616,7 +629,8 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
             firstFile = new TBrokerFileStatus(getFilePath(), false, -1, false);
         } else {
             firstFile = null;
-            for (TBrokerFileStatus fileStatus : fileStatuses) {
+            while (files.hasNext()) {
+                TBrokerFileStatus fileStatus = files.next();
                 if (isFileContentEmpty(fileStatus)) {
                     continue;
                 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProviderTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProviderTest.java
index 1bca291f532..d59402b710e 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProviderTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3EventSourceOffsetProviderTest.java
@@ -271,17 +271,21 @@ public class S3EventSourceOffsetProviderTest {
     }
 
     @Test
-    public void testNotificationMetadataDoesNotRequireHead() {
+    public void testNotificationUsesEventSizeAndCurrentTimeWithoutHead() {
         
Mockito.when(sqsClient.receiveMessage(Mockito.any(ReceiveMessageRequest.class)))
                 .thenReturn(response(message("literal", record("bucket", 
"logs/a%2Cb%5B1%5D.csv", 123))));
+        long before = System.currentTimeMillis();
         provider.fetchRemoteMeta(jobProperties(1), tvfProperties);
+        long after = System.currentTimeMillis();
 
         S3EventOffset offset = nextOffset();
         Assertions.assertEquals(Collections.singletonList("logs/a,b[1].csv"), 
offset.getFiles());
         Assertions.assertEquals("s3://bucket/logs/a,b[1].csv", 
offset.getFileStatuses().get(0).getPath());
         Assertions.assertEquals(123, 
offset.getFileStatuses().get(0).getSize());
-        
Assertions.assertFalse(offset.toSerializedJson().contains("fileStatuses"));
+        long modificationTime = 
offset.getFileStatuses().get(0).getModificationTime();
+        Assertions.assertTrue(modificationTime >= before && modificationTime 
<= after);
         Mockito.verify(s3Client, 
Mockito.never()).headObject(Mockito.any(HeadObjectRequest.class));
+        
Assertions.assertFalse(offset.toSerializedJson().contains("fileStatuses"));
         Mockito.verify(sqsClient, 
Mockito.never()).deleteMessageBatch(Mockito.any(DeleteMessageBatchRequest.class));
     }
 
@@ -639,9 +643,10 @@ public class S3EventSourceOffsetProviderTest {
     }
 
     @Test
-    public void 
testInvalidRecordsDoNotDiscardValidFilesAndDeletionWaitsForCommit() {
+    public void 
testSkippedRecordsDoNotDiscardValidFilesAndDeletionWaitsForCommit() {
         
Mockito.when(sqsClient.receiveMessage(Mockito.any(ReceiveMessageRequest.class)))
-                .thenReturn(response(message("mixed", record("bucket", 
"logs/a.csv", 1), "null",
+                .thenReturn(response(message("mixed", record("bucket", 
"logs/missing.csv", 1),
+                        record("bucket", "logs/a.csv", 1), "null",
                         record("bucket", "logs/deleted.csv", 
1).replace("ObjectCreated:Put", "ObjectRemoved:Delete"),
                         
"{\"eventSource\":\"aws:s3\",\"eventName\":\"ObjectCreated:Put\"}",
                         record("bucket", "", 1), record("bucket", 
"logs/invalid.csv", -1),
@@ -651,10 +656,13 @@ public class S3EventSourceOffsetProviderTest {
         provider.fetchRemoteMeta(jobProperties(2), tvfProperties);
 
         S3EventOffset offset = nextOffset();
-        Assertions.assertEquals(Collections.singletonList("logs/a.csv"), 
offset.getFiles());
+        Assertions.assertEquals(Arrays.asList("logs/missing.csv", 
"logs/a.csv"), offset.getFiles());
+        Assertions.assertEquals(2, offset.getFileStatuses().size());
+        Assertions.assertEquals("s3://bucket/logs/missing.csv", 
offset.getFileStatuses().get(0).getPath());
+        Assertions.assertEquals("s3://bucket/logs/a.csv", 
offset.getFileStatuses().get(1).getPath());
         Mockito.verify(sqsClient, 
Mockito.never()).deleteMessageBatch(Mockito.any(DeleteMessageBatchRequest.class));
         provider.updateOffset(offset);
-        provider.onTaskCommitted(1, 1);
+        provider.onTaskCommitted(2, 2);
         Mockito.verify(sqsClient, 
Mockito.never()).deleteMessageBatch(Mockito.any(DeleteMessageBatchRequest.class));
         provider.fetchRemoteMeta(jobProperties(2), tvfProperties);
         ArgumentCaptor<DeleteMessageBatchRequest> deleted = 
ArgumentCaptor.forClass(DeleteMessageBatchRequest.class);
@@ -783,16 +791,20 @@ public class S3EventSourceOffsetProviderTest {
 
     private void assertResumeReceivesFreshReceipt(boolean running) {
         
Mockito.when(sqsClient.receiveMessage(Mockito.any(ReceiveMessageRequest.class)))
-                .thenReturn(response(filesMessage("old", "logs/a.csv")),
-                        response(filesMessage("fresh", "logs/a.csv")));
+                .thenReturn(response(message("old", record("bucket", 
"logs/a.csv", 19))),
+                        response(message("fresh", record("bucket", 
"logs/a.csv", 170))));
         provider.fetchRemoteMeta(jobProperties(1), tvfProperties);
         if (running) {
             nextOffset();
         }
         provider.replayIfNeed(Mockito.mock(StreamingInsertJob.class));
         Assertions.assertFalse(provider.hasMoreDataToConsume());
+        long before = System.currentTimeMillis();
         provider.fetchRemoteMeta(jobProperties(1), tvfProperties);
         Assertions.assertEquals(Collections.singletonList("logs/a.csv"), 
nextOffset().getFiles());
+        Assertions.assertEquals(170, 
nextOffset().getFileStatuses().get(0).getSize());
+        long modificationTime = 
nextOffset().getFileStatuses().get(0).getModificationTime();
+        Assertions.assertTrue(modificationTime >= before && modificationTime 
<= System.currentTimeMillis());
         provider.updateOffset(nextOffset());
         provider.onTaskCommitted(1, 1);
         
Mockito.when(sqsClient.receiveMessage(Mockito.any(ReceiveMessageRequest.class)))
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3SourceOffsetProviderTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3SourceOffsetProviderTest.java
index 897537366a5..fdd51d00c7b 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3SourceOffsetProviderTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/s3/S3SourceOffsetProviderTest.java
@@ -129,6 +129,35 @@ public class S3SourceOffsetProviderTest {
         }
     }
 
+    @Test
+    public void testListingRefreshesFileSizeAndModificationTime() throws 
Exception {
+        RemoteFileSystem fs = Mockito.mock(RemoteFileSystem.class);
+        try (MockedStatic<FileSystemFactory> factory = 
Mockito.mockStatic(FileSystemFactory.class)) {
+            factory.when(() -> 
FileSystemFactory.get(Mockito.any(StorageProperties.class))).thenReturn(fs);
+            for (S3SourceOffsetProvider provider : ImmutableList.of(
+                    new S3SourceOffsetProvider(), new 
S3SourceOffsetProvider(ONCE_PROPS))) {
+                Mockito.when(fs.globListWithLimit(Mockito.anyString(), 
Mockito.anyList(), Mockito.isNull(),
+                        Mockito.anyLong(), 
Mockito.anyLong())).thenAnswer(invocation -> {
+                            List<RemoteFile> files = invocation.getArgument(1);
+                            files.add(new RemoteFile("s3://bucket/data/a.csv", 
true, 19, 0, 1767225600000L));
+                            return new GlobListResult(Status.OK, "data/a.csv", 
"bucket", "data/");
+                        }).thenAnswer(invocation -> {
+                            List<RemoteFile> files = invocation.getArgument(1);
+                            files.add(new RemoteFile("s3://bucket/data/a.csv", 
true, 170, 0, 1767225601000L));
+                            return new GlobListResult(Status.OK, "data/a.csv", 
"bucket", "data/");
+                        });
+
+                TBrokerFileStatus first = provider.getNextOffset(ONCE_PROPS, 
TVF_PROPS).getFileStatuses().get(0);
+                Assertions.assertEquals(19, first.getSize());
+                Assertions.assertEquals(1767225600000L, 
first.getModificationTime());
+                // An uncommitted batch must pick up an overwrite on the next 
attempt.
+                TBrokerFileStatus retried = provider.getNextOffset(ONCE_PROPS, 
TVF_PROPS).getFileStatuses().get(0);
+                Assertions.assertEquals(170, retried.getSize());
+                Assertions.assertEquals(1767225601000L, 
retried.getModificationTime());
+            }
+        }
+    }
+
     @Test
     public void testEmptyAndListingErrorRemainDistinct() throws Exception {
         RemoteFileSystem fs = Mockito.mock(RemoteFileSystem.class);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/S3TableValuedFunctionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/S3TableValuedFunctionTest.java
index 2d79b5a776a..cb388f2e198 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/S3TableValuedFunctionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/S3TableValuedFunctionTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.tablefunction;
 
+import org.apache.doris.catalog.PrimitiveType;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.util.S3Util;
@@ -24,19 +25,28 @@ import org.apache.doris.fs.FileSystemFactory;
 import org.apache.doris.nereids.StatementContext;
 import org.apache.doris.nereids.trees.expressions.Properties;
 import org.apache.doris.nereids.trees.expressions.functions.table.S3;
+import org.apache.doris.proto.InternalService.PFetchTableSchemaRequest;
 import org.apache.doris.proto.InternalService.PFetchTableSchemaResult;
+import org.apache.doris.proto.Types.PScalarType;
 import org.apache.doris.proto.Types.PStatus;
+import org.apache.doris.proto.Types.PTypeDesc;
+import org.apache.doris.proto.Types.PTypeNode;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.rpc.BackendServiceProxy;
 import org.apache.doris.system.Backend;
 import org.apache.doris.thrift.TBrokerFileStatus;
+import org.apache.doris.thrift.TFileScanRange;
+import org.apache.doris.thrift.TPrimitiveType;
 import org.apache.doris.thrift.TStatusCode;
+import org.apache.doris.thrift.TTypeNodeType;
 
 import com.google.common.collect.ImmutableMap;
+import org.apache.thrift.TDeserializer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
@@ -85,11 +95,40 @@ public class S3TableValuedFunctionTest {
     }
 
     @Test
-    public void testSchemaFailureDoesNotSkipFiles() throws Exception {
+    public void testSchemaFailurePreservesFilesAndAllowsRetry() throws 
Exception {
         assertSchemaFailure(TStatusCode.NOT_FOUND);
         assertSchemaFailure(TStatusCode.INTERNAL_ERROR);
     }
 
+    @Test
+    public void testSchemaInferenceUsesNextFileAfterNotFound() throws 
Exception {
+        S3TableValuedFunction tvf = tvf();
+        BackendServiceProxy proxy = Mockito.mock(BackendServiceProxy.class);
+        PFetchTableSchemaResult schema = PFetchTableSchemaResult.newBuilder()
+                
.setStatus(PStatus.newBuilder().setStatusCode(TStatusCode.OK.getValue()))
+                .setColumnNums(1).addColumnNames("id")
+                
.addColumnTypes(PTypeDesc.newBuilder().addTypes(PTypeNode.newBuilder()
+                        .setType(TTypeNodeType.SCALAR.getValue())
+                        
.setScalarType(PScalarType.newBuilder().setType(TPrimitiveType.INT.getValue())))).build();
+        Mockito.when(proxy.fetchTableStructureAsync(Mockito.any(), 
Mockito.any()))
+                
.thenReturn(CompletableFuture.completedFuture(result(TStatusCode.NOT_FOUND)),
+                        CompletableFuture.completedFuture(schema));
+        try (MockedStatic<BackendServiceProxy> service = 
Mockito.mockStatic(BackendServiceProxy.class)) {
+            service.when(BackendServiceProxy::getInstance).thenReturn(proxy);
+            Assertions.assertEquals("id", 
tvf.getTableColumns().get(0).getName());
+            Assertions.assertEquals(PrimitiveType.INT, 
tvf.getTableColumns().get(0).getType().getPrimitiveType());
+            ArgumentCaptor<PFetchTableSchemaRequest> requests =
+                    ArgumentCaptor.forClass(PFetchTableSchemaRequest.class);
+            Mockito.verify(proxy, 
Mockito.times(2)).fetchTableStructureAsync(Mockito.any(), requests.capture());
+            for (int i = 0; i < 2; i++) {
+                TFileScanRange range = new TFileScanRange();
+                new TDeserializer().deserialize(range, 
requests.getAllValues().get(i).getFileScanRange().toByteArray());
+                
Assertions.assertEquals(tvf.getFileStatuses().get(i).getPath(), 
range.getRanges().get(0).getPath());
+            }
+            Assertions.assertEquals(2, tvf.getFileStatuses().size());
+        }
+    }
+
     @Test
     public void testExactFilesAreScopedToTvf() throws Exception {
         S3TableValuedFunction first = new S3TableValuedFunction(PROPERTIES,
@@ -120,7 +159,12 @@ public class S3TableValuedFunctionTest {
         try (MockedStatic<BackendServiceProxy> service = 
Mockito.mockStatic(BackendServiceProxy.class)) {
             service.when(BackendServiceProxy::getInstance).thenReturn(proxy);
             Assertions.assertThrows(AnalysisException.class, 
tvf::getTableColumns);
-            Mockito.verify(proxy).fetchTableStructureAsync(Mockito.any(), 
Mockito.any());
+            int attempts = code == TStatusCode.NOT_FOUND ? 2 : 1;
+            Mockito.verify(proxy, Mockito.times(attempts))
+                    .fetchTableStructureAsync(Mockito.any(), Mockito.any());
+            Assertions.assertThrows(AnalysisException.class, 
tvf::getTableColumns);
+            Mockito.verify(proxy, Mockito.times(attempts * 2))
+                    .fetchTableStructureAsync(Mockito.any(), Mockito.any());
             Assertions.assertEquals(2, tvf.getFileStatuses().size());
         }
     }
diff --git 
a/regression-test/suites/job_p0/streaming_job/test_streaming_insert_job_s3_event_aksk.groovy
 
b/regression-test/suites/job_p0/streaming_job/test_streaming_insert_job_s3_event_aksk.groovy
index 6b519b621a8..b92c4b072f7 100644
--- 
a/regression-test/suites/job_p0/streaming_job/test_streaming_insert_job_s3_event_aksk.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/test_streaming_insert_job_s3_event_aksk.groovy
@@ -102,6 +102,11 @@ suite("test_streaming_insert_job_s3_event_aksk") {
     }
     try {
         sql "DROP JOB IF EXISTS WHERE jobname='${jobName}'"
+        // Observe the invalid message before starting a consumer; PAUSE can 
leave a receive in flight.
+        
sqsClient.sendMessage(SendMessageRequest.builder().queueUrl(queueUrl).messageBody("{}").build())
+        Awaitility.await().pollInSameThread().atMost(120, 
SECONDS).pollInterval(1, SECONDS).until {
+            
queueCounts()[QueueAttributeName.APPROXIMATE_NUMBER_OF_MESSAGES].toLong() > 0
+        }
         sql "DROP TABLE IF EXISTS test_s3_event_aksk"
         sql """CREATE TABLE test_s3_event_aksk (id INT, value STRING)
                UNIQUE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
@@ -131,6 +136,7 @@ suite("test_streaming_insert_job_s3_event_aksk") {
                )"""
         awaitRows(1)
         awaitOffsets(["z.csv"])
+        awaitQueueEmpty()
         order_qt_first "SELECT * FROM test_s3_event_aksk"
 
         // A smaller key uploaded after the first commit must still be 
discovered in NOTIFICATION mode.
@@ -191,21 +197,6 @@ suite("test_streaming_insert_job_s3_event_aksk") {
             }
             metricsReady
         }
-
-        String committedOffset = sql("SELECT CurrentOffset FROM 
jobs(\"type\"=\"insert\") WHERE Name='${jobName}'")[0][0]
-        // A message without S3 records is acknowledged without advancing the 
job.
-        sql "PAUSE JOB WHERE jobname='${jobName}'"
-        
sqsClient.sendMessage(SendMessageRequest.builder().queueUrl(queueUrl).messageBody("{}").build())
-        Awaitility.await().pollInSameThread().atMost(120, 
SECONDS).pollInterval(1, SECONDS).until {
-            
queueCounts()[QueueAttributeName.APPROXIMATE_NUMBER_OF_MESSAGES].toLong() > 0
-        }
-        sql "RESUME JOB WHERE jobname='${jobName}'"
-        awaitQueueEmpty()
-        Awaitility.await().pollInSameThread().atMost(120, 
SECONDS).pollInterval(1, SECONDS).until {
-            def job = sql("SELECT Status, CurrentOffset FROM 
jobs(\"type\"=\"insert\") WHERE Name='${jobName}'")
-            job[0][0] == "RUNNING" && job[0][1] == committedOffset &&
-                    (sql("SELECT COUNT(*) FROM test_s3_event_aksk")[0][0] as 
long) == 5
-        }
     } finally {
         try {
             sql "DROP JOB IF EXISTS WHERE jobname='${jobName}'"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to