This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new ffe50311340 [fix](fe) Guard stream load schema metadata reads (#68964)
ffe50311340 is described below
commit ffe50311340b86b8340d035ef395f346113281c8
Author: Oliveira <[email protected]>
AuthorDate: Thu Oct 15 09:33:44 2026 +0800
[fix](fe) Guard stream load schema metadata reads (#68964)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #68726
Problem Summary:
Stream Load reads the base schema version from table metadata outside
the planning lock. Concurrent schema change removes the old base-index
metadata before switching the base index ID under the table write lock.
An unlocked read in that interval can dereference missing metadata and
fail the load. A deterministic unit test reproduces the null dereference
on the pre-optimization baseline.
For regular Stream Load, populate the response database ID, table ID,
and base schema version inside the existing `generatePlan(OlapTable)`
read-lock scope. Populate them only for the first planned table when a
response is present, preserving multi-table behavior. Planning and
response metadata use the same lock scope without an additional lock
acquisition.
For HTTP `load_sql`, obtain the response table ID and base schema
version from the generated `OlapTableSink`. These values were already
captured during planning under the existing table lock. Keep the other
table-property reads as before. This removes the second metadata-lock
acquisition and the transaction-abort branch introduced solely for its
timeout. Existing transaction handling and Thrift fields are preserved.
This fix is separate from the scheduling and file TVF COUNT optimization
in #68726.
### Release note
Prevent Stream Load response schema-version reads from racing with
schema change by reusing the existing planning lock and planned sink
metadata.
### Check List (For Author)
- Test
- [x] Regression test
- [x] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason
Validation performed:
- Standard focused FE UT reactor passed `StreamLoadHandlerTest` (14) and
`HttpStreamLoadTablePropertiesTest` (2): 16 tests, zero failures/errors.
Maven Checkstyle passed.
- Unit coverage includes real concurrent base-index replacement with one
read-lock acquisition, metadata read-lock ownership and exceptional
release, the existing planning timeout, first-table response semantics,
and planning without a response. HTTP tests verify planned metadata is
used even when current base-index metadata is absent, for both regular
loads and Group Commit, without another read-lock acquisition.
- Standard ASAN FE/BE build, deployment, and health checks passed in the
personal shared-nothing single-BE environment. P0
`load_p0/http_stream/test_stream_load_schema_metadata` and
`load_p0/http_stream/test_http_stream_properties` passed with zero
failed suites/fatal scripts. Existing regression snapshots were compared
without regeneration or manual edits.
- P0 `schema_change_p0/test_schema_change_unique_mow` also passed: one
suite, zero failed suites/fatal scripts. All three targeted suites used
the binaries built from this revised implementation. Source changes pass
`git diff --check`.
- The concurrency unit test controls the race window; SQL regression
exercises compatibility and does not guarantee it hits that exact
window. This local shared-nothing single-BE environment is not complete
TeamCity or Cloud coverage.
- Behavior changed:
- [ ] No.
- [x] Yes. Response metadata comes from the existing protected planning
scope or the completed HTTP plan, avoiding an unlocked base-index lookup
and additional lock waiting. Multi-table first-table behavior and
protocol fields are preserved.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../org/apache/doris/load/StreamLoadHandler.java | 12 +-
.../apache/doris/service/FrontendServiceImpl.java | 21 ++-
.../apache/doris/load/StreamLoadHandlerTest.java | 170 +++++++++++++++++++++
.../service/HttpStreamLoadTablePropertiesTest.java | 78 ++++++++++
.../test_stream_load_schema_metadata.out | 13 ++
.../test_stream_load_schema_metadata.groovy | 98 ++++++++++++
6 files changed, 378 insertions(+), 14 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
b/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
index 8ab0f9db99d..06fde6758dc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/load/StreamLoadHandler.java
@@ -248,13 +248,6 @@ public class StreamLoadHandler {
if (tables.isEmpty()) {
throw new MetaNotFoundException("table not found");
}
-
- if (result != null) {
- OlapTable olapTable = tables.get(0);
- result.setDbId(db.getId());
- result.setTableId(olapTable.getId());
- result.setBaseSchemaVersion(olapTable.getBaseSchemaVersion());
- }
}
/**
@@ -300,6 +293,11 @@ public class StreamLoadHandler {
result.query_options.setFeProcessUuid(ExecuteEnv.getInstance().getProcessUUID());
result.setIsMowTable(table.getEnableUniqueKeyMergeOnWrite());
result.setEnableTso(table.enableTso());
+ if (this.result != null && fragmentParams.isEmpty()) {
+ this.result.setDbId(db.getId());
+ this.result.setTableId(table.getId());
+ this.result.setBaseSchemaVersion(table.getBaseSchemaVersion());
+ }
fragmentParams.add(result);
if (StringUtils.isEmpty(streamLoadTask.getGroupCommit())) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
index de1dd61bb23..03d5a2c9f8d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
@@ -270,6 +270,7 @@ import org.apache.doris.thrift.TNodeInfo;
import org.apache.doris.thrift.TNullableStringLiteral;
import org.apache.doris.thrift.TOlapTableIndexTablets;
import org.apache.doris.thrift.TOlapTablePartition;
+import org.apache.doris.thrift.TOlapTableSink;
import org.apache.doris.thrift.TPartitionMeta;
import org.apache.doris.thrift.TPipelineFragmentParams;
import org.apache.doris.thrift.TPipelineWorkloadGroup;
@@ -3190,6 +3191,18 @@ public class FrontendServiceImpl implements
FrontendService.Iface {
return httpStreamParams;
}
+ private void setHttpStreamLoadTableProperties(HttpStreamParams
httpStreamParams,
+ TStreamLoadPutResult result) {
+ OlapTable table = (OlapTable) httpStreamParams.getTable();
+ TOlapTableSink sink =
httpStreamParams.getParams().getFragment().getOutputSink().getOlapTableSink();
+
result.getPipelineParams().setIsMowTable(table.getEnableUniqueKeyMergeOnWrite());
+ result.getPipelineParams().setEnableTso(table.enableTso());
+ result.setTableId(sink.getTableId());
+ result.setBaseSchemaVersion(sink.getBaseSchemaVersion());
+ result.setGroupCommitIntervalMs(table.getGroupCommitIntervalMs());
+ result.setGroupCommitDataBytes(table.getGroupCommitDataBytes());
+ }
+
private void httpStreamPutImpl(TStreamLoadPutRequest request,
TStreamLoadPutResult result)
throws UserException {
if (LOG.isDebugEnabled()) {
@@ -3248,14 +3261,8 @@ public class FrontendServiceImpl implements
FrontendService.Iface {
result.getPipelineParams().setTableName(httpStreamParams.getTable().getName());
result.getPipelineParams().setTxnConf(new
TTxnParams().setTxnId(httpStreamParams.getTxnId()));
result.getPipelineParams().setImportLabel(httpStreamParams.getLabel());
- result.getPipelineParams()
- .setIsMowTable(((OlapTable)
httpStreamParams.getTable()).getEnableUniqueKeyMergeOnWrite());
- result.getPipelineParams().setEnableTso(((OlapTable)
httpStreamParams.getTable()).enableTso());
result.setDbId(httpStreamParams.getDb().getId());
- result.setTableId(httpStreamParams.getTable().getId());
- result.setBaseSchemaVersion(((OlapTable)
httpStreamParams.getTable()).getBaseSchemaVersion());
- result.setGroupCommitIntervalMs(((OlapTable)
httpStreamParams.getTable()).getGroupCommitIntervalMs());
- result.setGroupCommitDataBytes(((OlapTable)
httpStreamParams.getTable()).getGroupCommitDataBytes());
+ setHttpStreamLoadTableProperties(httpStreamParams, result);
result.setWaitInternalGroupCommitFinish(Config.wait_internal_group_commit_finish);
} catch (UserException e) {
LOG.warn("exec sql error", e);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
index b8231f963cd..6ec4ea387c0 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/load/StreamLoadHandlerTest.java
@@ -18,14 +18,22 @@
package org.apache.doris.load;
import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.MaterializedIndexMeta;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.TableIf.TableType;
import org.apache.doris.cloud.catalog.CloudEnv;
import org.apache.doris.cloud.system.CloudSystemInfoService;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
+import org.apache.doris.common.UserException;
import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.common.lock.MonitoredReentrantReadWriteLock;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.Auth;
+import org.apache.doris.nereids.load.NereidsStreamLoadPlanner;
+import org.apache.doris.nereids.load.NereidsStreamLoadTask;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.system.Backend;
import org.apache.doris.system.SystemInfoService;
@@ -38,6 +46,7 @@ import org.apache.doris.thrift.TOlapTablePartitionParam;
import org.apache.doris.thrift.TOlapTableSink;
import org.apache.doris.thrift.TPipelineFragmentParams;
import org.apache.doris.thrift.TPlanFragment;
+import org.apache.doris.thrift.TQueryOptions;
import org.apache.doris.thrift.TStreamLoadPutRequest;
import org.apache.doris.thrift.TStreamLoadPutResult;
import org.apache.doris.thrift.TTabletLocation;
@@ -45,13 +54,174 @@ import org.apache.doris.thrift.TTabletLocation;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
public class StreamLoadHandlerTest {
+ @Test
+ public void testSchemaVersionLockTimeoutDoesNotReadMetadata() throws
Exception {
+ OlapTable table = Mockito.mock(OlapTable.class);
+ Mockito.when(table.getName()).thenReturn("tbl");
+ Mockito.when(table.tryReadLock(1,
TimeUnit.MILLISECONDS)).thenReturn(false);
+ verifySchemaVersionReadFailure(table, UserException.class);
+ Mockito.verify(table).tryReadLock(1, TimeUnit.MILLISECONDS);
+ Mockito.verify(table, Mockito.never()).getBaseSchemaVersion();
+ Mockito.verify(table, Mockito.never()).readUnlock();
+ }
+
+ @Test
+ public void testSchemaVersionReadFailureReleasesLock() throws Exception {
+ OlapTable table = Mockito.spy(new OlapTable());
+ Mockito.doThrow(new IllegalStateException("schema read
failed")).when(table).getBaseSchemaVersion();
+ verifySchemaVersionReadFailure(table, IllegalStateException.class);
+ MonitoredReentrantReadWriteLock lock = Deencapsulation.getField(table,
"rwLock");
+ Assertions.assertEquals(0, lock.getReadHoldCount());
+ Mockito.verify(table).readUnlock();
+ }
+
+ private void verifySchemaVersionReadFailure(OlapTable table, Class<?
extends Throwable> failure) throws Exception {
+ Env env = Mockito.mock(Env.class);
+ InternalCatalog catalog = Mockito.mock(InternalCatalog.class);
+ Database database = Mockito.mock(Database.class);
+ Mockito.when(env.getInternalCatalog()).thenReturn(catalog);
+ Mockito.when(catalog.getDbNullable("db")).thenReturn(database);
+ Mockito.when(database.getTableOrMetaException("tbl",
TableType.OLAP)).thenReturn(table);
+ TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+ request.setDb("db");
+ request.setTbl("tbl");
+ request.setThriftRpcTimeoutMs(1);
+ StreamLoadHandler handler = new StreamLoadHandler(request, null, new
TStreamLoadPutResult(), "127.0.0.1");
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Deencapsulation.invoke(handler, "setDbAndTable");
+ Assertions.assertThrows(failure, () ->
generateMetadataPlan(handler, table, request));
+ }
+ }
+
+ @Test
+ public void testSchemaVersionReadWaitsForBaseIndexReplacement() throws
Exception {
+ OlapTable table = Mockito.spy(new OlapTable());
+ table.setBaseIndexId(1L);
+ MaterializedIndexMeta oldIndexMeta =
Mockito.mock(MaterializedIndexMeta.class);
+ Mockito.when(oldIndexMeta.getSchemaVersion()).thenReturn(6);
+ table.getMutableIndexIdToMeta().put(1L, oldIndexMeta);
+ MaterializedIndexMeta newIndexMeta =
Mockito.mock(MaterializedIndexMeta.class);
+ Mockito.when(newIndexMeta.getSchemaVersion()).thenReturn(7);
+ table.getMutableIndexIdToMeta().put(2L, newIndexMeta);
+ Mockito.doAnswer(invocation -> {
+ MonitoredReentrantReadWriteLock lock =
Deencapsulation.getField(table, "rwLock");
+ Assertions.assertEquals(1, lock.getReadHoldCount());
+ return invocation.callRealMethod();
+ }).when(table).getBaseSchemaVersion();
+ CountDownLatch oldIndexRemoved = new CountDownLatch(1);
+ CountDownLatch readerStarted = new CountDownLatch(1);
+ Mockito.doAnswer(invocation -> {
+ readerStarted.countDown();
+ return invocation.callRealMethod();
+ }).when(table).tryReadLock(Mockito.anyLong(),
Mockito.eq(TimeUnit.MILLISECONDS));
+
+ Env env = Mockito.mock(Env.class);
+ InternalCatalog catalog = Mockito.mock(InternalCatalog.class);
+ Database database = Mockito.mock(Database.class);
+ Mockito.when(env.getInternalCatalog()).thenReturn(catalog);
+ Mockito.when(catalog.getDbNullable("db")).thenReturn(database);
+ Mockito.when(database.getTableOrMetaException("tbl",
TableType.OLAP)).thenReturn(table);
+ TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+ request.setDb("db");
+ request.setTbl("tbl");
+ TStreamLoadPutResult result = new TStreamLoadPutResult();
+ StreamLoadHandler handler = new StreamLoadHandler(request, null,
result, "127.0.0.1");
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<?> schemaChange = executor.submit(() -> {
+ table.writeLock();
+ try {
+ // Schema change removes the old base index before switching
to the shadow index.
+ table.getMutableIndexIdToMeta().remove(1L);
+ oldIndexRemoved.countDown();
+ Assertions.assertTrue(readerStarted.await(5,
TimeUnit.SECONDS));
+ table.setBaseIndexId(2L);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ } finally {
+ table.writeUnlock();
+ }
+ });
+ try (MockedStatic<Env> mockedEnv = Mockito.mockStatic(Env.class)) {
+ mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+ Assertions.assertTrue(oldIndexRemoved.await(5, TimeUnit.SECONDS));
+ Deencapsulation.invoke(handler, "setDbAndTable");
+ // Catalog lookup must not touch index metadata or acquire an
extra read lock.
+ Mockito.verify(table, Mockito.never()).getBaseSchemaVersion();
+ generateMetadataPlan(handler, table, request);
+ Assertions.assertEquals(7, result.getBaseSchemaVersion());
+ Mockito.verify(table, Mockito.times(1)).tryReadLock(5000,
TimeUnit.MILLISECONDS);
+ } finally {
+ readerStarted.countDown();
+ try {
+ schemaChange.get(5, TimeUnit.SECONDS);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+ }
+
+ private void generateMetadataPlan(StreamLoadHandler handler, OlapTable
table,
+ TStreamLoadPutRequest request) throws Exception {
+ NereidsStreamLoadTask task = Mockito.mock(NereidsStreamLoadTask.class);
+ Mockito.when(task.getGroupCommit()).thenReturn("sync_mode");
+ Mockito.doReturn(false).when(table).getEnableUniqueKeyMergeOnWrite();
+ Mockito.doReturn(false).when(table).enableTso();
+ TPipelineFragmentParams plan = buildStreamLoadParams(new
TOlapTableSink());
+ plan.setQueryOptions(new TQueryOptions());
+ try (MockedStatic<NereidsStreamLoadTask> tasks =
Mockito.mockStatic(NereidsStreamLoadTask.class);
+ MockedConstruction<NereidsStreamLoadPlanner> planners =
Mockito.mockConstruction(
+ NereidsStreamLoadPlanner.class, (planner, context) ->
+ Mockito.when(planner.plan(Mockito.any(),
Mockito.anyInt())).thenReturn(plan))) {
+ tasks.when(() ->
NereidsStreamLoadTask.fromTStreamLoadPutRequest(request)).thenReturn(task);
+ handler.generatePlan(table);
+ }
+ }
+
+ @Test
+ public void testResponseKeepsFirstTableMetadata() throws Exception {
+ Database database = new Database(17, "db");
+ TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+ TStreamLoadPutResult result = new TStreamLoadPutResult();
+ StreamLoadHandler handler = new StreamLoadHandler(request, null,
result, "127.0.0.1");
+ Deencapsulation.setField(handler, "db", database);
+ OlapTable first = Mockito.spy(new OlapTable());
+ Mockito.doReturn(41L).when(first).getId();
+ Mockito.doReturn(7).when(first).getBaseSchemaVersion();
+ generateMetadataPlan(handler, first, request);
+ OlapTable second = Mockito.spy(new OlapTable());
+ Mockito.doReturn(42L).when(second).getId();
+ generateMetadataPlan(handler, second, request);
+ Assertions.assertEquals(17, result.getDbId());
+ Assertions.assertEquals(41, result.getTableId());
+ Assertions.assertEquals(7, result.getBaseSchemaVersion());
+ Mockito.verify(second, Mockito.never()).getBaseSchemaVersion();
+ }
+
+ @Test
+ public void testPlanningWithoutResponseDoesNotReadExtraSchemaMetadata()
throws Exception {
+ TStreamLoadPutRequest request = new TStreamLoadPutRequest();
+ StreamLoadHandler handler = new StreamLoadHandler(request, null, null,
"127.0.0.1");
+ OlapTable table = Mockito.spy(new OlapTable());
+ generateMetadataPlan(handler, table, request);
+ Mockito.verify(table, Mockito.never()).getBaseSchemaVersion();
+ Mockito.verify(table, Mockito.times(1)).tryReadLock(5000,
TimeUnit.MILLISECONDS);
+ }
+
@Test
public void testSelectBackendSkipsDecommissioningBackend() throws
Exception {
SystemInfoService originalSystemInfoService =
Env.getCurrentSystemInfo();
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/service/HttpStreamLoadTablePropertiesTest.java
b/fe/fe-core/src/test/java/org/apache/doris/service/HttpStreamLoadTablePropertiesTest.java
new file mode 100644
index 00000000000..1ddf9eefac9
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/service/HttpStreamLoadTablePropertiesTest.java
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.service;
+
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.qe.HttpStreamParams;
+import org.apache.doris.thrift.TDataSink;
+import org.apache.doris.thrift.TOlapTableSink;
+import org.apache.doris.thrift.TPipelineFragmentParams;
+import org.apache.doris.thrift.TPlanFragment;
+import org.apache.doris.thrift.TStreamLoadPutResult;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.concurrent.TimeUnit;
+
+public class HttpStreamLoadTablePropertiesTest {
+ @Test
+ public void testHttpLoadUsesPlannedSchemaVersion() {
+ verifyPlannedMetadata(23);
+ }
+
+ @Test
+ public void testGroupCommitUsesPlannedSchemaVersion() {
+ verifyPlannedMetadata(-1);
+ }
+
+ private void verifyPlannedMetadata(long txnId) {
+ OlapTable table = Mockito.spy(new OlapTable());
+ // The current table has no base-index metadata. The response must use
the
+ // completed plan, even if schema change has since replaced the index.
+ table.setBaseIndexId(9L);
+ Mockito.doReturn(false).when(table).getEnableUniqueKeyMergeOnWrite();
+ Mockito.doReturn(false).when(table).enableTso();
+ Mockito.doReturn(1000).when(table).getGroupCommitIntervalMs();
+ Mockito.doReturn(1024).when(table).getGroupCommitDataBytes();
+ TOlapTableSink sink = new TOlapTableSink();
+ sink.setTableId(41);
+ sink.setBaseSchemaVersion(7);
+ TPipelineFragmentParams plan = new TPipelineFragmentParams();
+ plan.setFragment(new TPlanFragment().setOutputSink(new
TDataSink().setOlapTableSink(sink)));
+ HttpStreamParams params = new HttpStreamParams();
+ params.setTable(table);
+ params.setTxnId(txnId);
+ params.setParams(plan);
+ TStreamLoadPutResult result = new TStreamLoadPutResult();
+ result.setPipelineParams(plan);
+ FrontendServiceImpl service = Mockito.mock(FrontendServiceImpl.class);
+
+ Deencapsulation.invoke(service, "setHttpStreamLoadTableProperties",
params, result);
+
+ Assertions.assertEquals(41, result.getTableId());
+ Assertions.assertEquals(7, result.getBaseSchemaVersion());
+ Assertions.assertEquals(1000, result.getGroupCommitIntervalMs());
+ Assertions.assertEquals(1024, result.getGroupCommitDataBytes());
+ Mockito.verify(table, Mockito.never()).getBaseSchemaVersion();
+ Mockito.verify(table, Mockito.never()).tryReadLock(Mockito.anyLong(),
Mockito.any(TimeUnit.class));
+ Mockito.verify(table, Mockito.never()).readUnlock();
+ }
+}
diff --git
a/regression-test/data/load_p0/http_stream/test_stream_load_schema_metadata.out
b/regression-test/data/load_p0/http_stream/test_stream_load_schema_metadata.out
new file mode 100644
index 00000000000..f32e8436544
--- /dev/null
+++
b/regression-test/data/load_p0/http_stream/test_stream_load_schema_metadata.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !before --
+1 10
+2 20
+
+-- !after_replacement --
+1 10
+2 20
+
+-- !after_addition --
+1 10 7
+2 20 7
+
diff --git
a/regression-test/suites/load_p0/http_stream/test_stream_load_schema_metadata.groovy
b/regression-test/suites/load_p0/http_stream/test_stream_load_schema_metadata.groovy
new file mode 100644
index 00000000000..853abb28461
--- /dev/null
+++
b/regression-test/suites/load_p0/http_stream/test_stream_load_schema_metadata.groovy
@@ -0,0 +1,98 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_stream_load_schema_metadata", "p0") {
+ sql "DROP TABLE IF EXISTS test_stream_load_schema_metadata"
+ sql """
+ CREATE TABLE test_stream_load_schema_metadata (
+ id INT NOT NULL,
+ value INT NOT NULL
+ ) UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "light_schema_change" = "false"
+ )
+ """
+
+ def db = context.config.getDbNameByFile(context.file)
+ def loadRows = { boolean http, boolean groupCommit ->
+ streamLoad {
+ if (http) {
+ set 'version', '1'
+ set 'sql', """
+ INSERT INTO ${db}.test_stream_load_schema_metadata (id,
value)
+ SELECT c1, c2 FROM http_stream("format"="csv",
"column_separator"=",")
+ """
+ } else {
+ table 'test_stream_load_schema_metadata'
+ set 'column_separator', ','
+ set 'columns', 'id,value'
+ }
+ if (groupCommit) {
+ set 'group_commit', 'sync_mode'
+ unset 'label'
+ }
+ inputStream new
ByteArrayInputStream("1,10\n2,20\n".getBytes("UTF-8"))
+ time 10000
+ check { result, exception, startTime, endTime ->
+ if (exception != null) {
+ throw exception
+ }
+ def json = parseJson(result)
+ assertEquals("success", json.Status.toLowerCase())
+ assertEquals(2, json.NumberLoadedRows)
+ assertEquals(0, json.NumberFilteredRows)
+ if (groupCommit) {
+ assertTrue(json.GroupCommit)
+ }
+ }
+ }
+ }
+
+ loadRows(false, false)
+ loadRows(true, false)
+ order_qt_before "SELECT * FROM test_stream_load_schema_metadata"
+
+ // Repeated loads exercise both metadata response paths while the base
index is replaced.
+ sql "ALTER TABLE test_stream_load_schema_metadata MODIFY COLUMN value
BIGINT NOT NULL"
+ for (int i = 0; i < 5; i++) {
+ loadRows(false, false)
+ loadRows(true, false)
+ }
+ waitForSchemaChangeDone {
+ sql """SHOW ALTER TABLE COLUMN WHERE
TableName='test_stream_load_schema_metadata'
+ ORDER BY CreateTime DESC LIMIT 1"""
+ time 120
+ }
+ loadRows(false, false)
+ loadRows(true, false)
+ order_qt_after_replacement "SELECT * FROM test_stream_load_schema_metadata"
+
+ sql "ALTER TABLE test_stream_load_schema_metadata ADD COLUMN added INT
DEFAULT '7'"
+ waitForSchemaChangeDone {
+ sql """SHOW ALTER TABLE COLUMN WHERE
TableName='test_stream_load_schema_metadata'
+ ORDER BY CreateTime DESC LIMIT 1"""
+ time 120
+ }
+ loadRows(false, false)
+ loadRows(true, false)
+ loadRows(true, true)
+ sql "sync"
+ order_qt_after_addition "SELECT * FROM test_stream_load_schema_metadata"
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]