This is an automated email from the ASF dual-hosted git repository.
yujun777 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 7129a3e8c6b [fix](ivm) Propagate and compensate failures of the IVM
excluded-trigger-tables ALTER (#67665)
7129a3e8c6b is described below
commit 7129a3e8c6bf087cb5a3a818e53343f5fee2c297
Author: yujun <[email protected]>
AuthorDate: Thu Sep 10 11:22:11 2026 +0800
[fix](ivm) Propagate and compensate failures of the IVM
excluded-trigger-tables ALTER (#67665)
Follow-up of the #62606 review round.
ALTER MATERIALIZED VIEW ... SET ('excluded_trigger_tables' = ...) on an
IVM MTMV transitions the base-table streams alongside the property. That
transition used to run inside `processAlterMTMV`, whose catch swallowed
any `UserException`: a mid-transition failure (e.g. a stream create
throwing after an earlier create was already journaled) reported success
to the client while the property stayed unchanged and stray streams
remained.
Key changes:
- Live ALTER PROPERTY statements now run through the new
`Alter.processAlterMTMVProperty`, which propagates failures to the
client; the journal replay path keeps the tolerant `processAlterMTMV`
behavior.
- The stream transition is reordered: create the streams of newly
un-excluded bases first (compensated by dropping exactly the streams
created in this call on failure), then apply the property, then
best-effort drop the streams of newly excluded bases - a failed drop
only leaks a stream of an already excluded table and never fails the
ALTER after the property took effect.
- Two test debug points (count-based, independent of the base-table
iteration order) inject stream create/drop failures.
Tests: `AlterMTMVTest` two new cases covering create-failure
compensation and drop-failure best effort with multi-table excluded-set
changes; the full class (24 tests) passes.
Trace issue: https://github.com/apache/doris/issues/65418
---
.../main/java/org/apache/doris/alter/Alter.java | 172 ++++++++++++++++++---
.../java/org/apache/doris/catalog/Database.java | 5 +
.../main/java/org/apache/doris/catalog/Env.java | 6 +-
.../java/org/apache/doris/mtmv/AlterMTMVTest.java | 105 +++++++++++++
4 files changed, 264 insertions(+), 24 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
b/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
index 2a246e64deb..8c30d35a733 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/Alter.java
@@ -43,6 +43,7 @@ import org.apache.doris.common.DdlException;
import org.apache.doris.common.MetaNotFoundException;
import org.apache.doris.common.UserException;
import org.apache.doris.common.cache.NereidsSqlCacheManager;
+import org.apache.doris.common.util.DebugPointUtil;
import org.apache.doris.common.util.DynamicPartitionUtil;
import org.apache.doris.common.util.MetaLockUtils;
import org.apache.doris.common.util.PropertyAnalyzer;
@@ -128,6 +129,15 @@ import java.util.stream.Collectors;
public class Alter {
private static final Logger LOG = LogManager.getLogger(Alter.class);
+ // Test hooks for the IVM excluded_trigger_tables stream transition (see
+ // alterIvmExcludedTriggerTables): when enabled, the "value" param is the
number of
+ // successful stream creates/drops to allow first (e.g. "1" allows one and
fails the
+ // next), independent of the base-table iteration order.
+ public static final String DEBUG_POINT_CREATE_EXCLUDED_STREAM_FAIL =
+ "Alter.alterIvmExcludedTriggerTables.create_stream_fail";
+ public static final String DEBUG_POINT_DROP_EXCLUDED_STREAM_FAIL =
+ "Alter.alterIvmExcludedTriggerTables.drop_stream_fail";
+
private AlterHandler schemaChangeHandler;
private AlterHandler materializedViewHandler;
private SystemHandler systemHandler;
@@ -1315,16 +1325,10 @@ public class Alter {
mtmv.alterStatus(alterMTMV.getStatus());
break;
case ALTER_PROPERTY:
- if (mtmv.isIvm() &&
alterMTMV.getMvProperties().containsKey(
-
PropertyAnalyzer.PROPERTIES_EXCLUDED_TRIGGER_TABLES)) {
- Set<TableNameInfo> oldExcludedTriggerTables =
mtmv.getExcludedTriggerTables();
- Set<TableNameInfo> newExcludedTriggerTables =
MTMVPropertyUtil.parseTableNameInfos(
- alterMTMV.getMvProperties().get(
-
PropertyAnalyzer.PROPERTIES_EXCLUDED_TRIGGER_TABLES));
- updateIvmStreamsForExcludedTables(db, mtmv,
oldExcludedTriggerTables,
- newExcludedTriggerTables, isReplay);
- }
- mtmv.alterMvProperties(alterMTMV, isReplay);
+ // Live ALTER PROPERTY statements call
processAlterMTMVProperty directly
+ // (see Env.alterMTMVProperty) so that failures surface to
the client;
+ // this path only replays the journaled op, where errors
stay tolerated.
+ processAlterMTMVProperty(alterMTMV, isReplay);
return;
case ADD_TASK:
if (!mtmv.addTaskResult(alterMTMV, isReplay)) {
@@ -1355,28 +1359,141 @@ public class Alter {
}
}
- private void updateIvmStreamsForExcludedTables(Database db, MTMV mtmv,
+ /**
+ * Applies an ALTER PROPERTY op. Live statements call this method directly
through
+ * {@code Env.alterMTMVProperty} so that a failure (e.g. a partial IVM
stream
+ * transition) propagates to the client instead of being swallowed; the
replay path
+ * runs it through {@link #processAlterMTMV} where errors are tolerated.
+ */
+ public void processAlterMTMVProperty(AlterMTMV alterMTMV, boolean
isReplay) throws UserException {
+ MTMV mtmv;
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException(alterMTMV.getMvName().getDb());
+ // Fail before touching anything when the database is being dropped: a
later step
+ // (e.g. the stream drop under the db write lock) would otherwise fail
after the
+ // property was already applied.
+ if (db.isDropped()) {
+ throw new DdlException("unknown db, dbName=" + db.getFullName());
+ }
+ mtmv = (MTMV)
db.getTableOrMetaException(alterMTMV.getMvName().getTbl(),
TableType.MATERIALIZED_VIEW);
+ if (mtmv.isIvm() && alterMTMV.getMvProperties().containsKey(
+ PropertyAnalyzer.PROPERTIES_EXCLUDED_TRIGGER_TABLES)) {
+ Set<TableNameInfo> oldExcludedTriggerTables =
mtmv.getExcludedTriggerTables();
+ Set<TableNameInfo> newExcludedTriggerTables =
MTMVPropertyUtil.parseTableNameInfos(
+
alterMTMV.getMvProperties().get(PropertyAnalyzer.PROPERTIES_EXCLUDED_TRIGGER_TABLES));
+ alterIvmExcludedTriggerTables(db, mtmv, oldExcludedTriggerTables,
+ newExcludedTriggerTables, alterMTMV, isReplay);
+ return;
+ }
+ mtmv.alterMvProperties(alterMTMV, isReplay);
+ }
+
+ private void alterIvmExcludedTriggerTables(Database db, MTMV mtmv,
+ Set<TableNameInfo> oldExcludedTriggerTables, Set<TableNameInfo>
newExcludedTriggerTables,
+ AlterMTMV alterMTMV, boolean isReplay) throws UserException {
+ // Step 1: create streams for base tables leaving the excluded set
(skipped on
+ // replay, each create journals itself). A mid-loop failure is
compensated by
+ // dropping exactly the streams created here, leaving the previous
property in
+ // effect without stray streams.
+ List<String> createdStreamNames = new ArrayList<>();
+ if (!isReplay) {
+ try {
+ createStreamsForUnExcludedTables(db, mtmv,
oldExcludedTriggerTables,
+ newExcludedTriggerTables, createdStreamNames);
+ } catch (UserException e) {
+ try {
+ dropStreamsByNames(db, createdStreamNames, false);
+ } catch (UserException compensateException) {
+ // Keep the original failure for the client; a failed
compensation
+ // leaves stray streams that the idempotent retry of the
ALTER
+ // cleans up.
+ LOG.error("failed to compensate streams created for mv={}
after create failure",
+ mtmv.getName(), compensateException);
+ }
+ throw e;
+ }
+ }
+ // Step 2: apply the property, the source of truth for refresh.
+ mtmv.alterMvProperties(alterMTMV, isReplay);
+ // Step 3: drop streams of base tables that just joined the excluded
set. This
+ // runs after the property on purpose: a failed drop then only leaks
the stream
+ // of a table that is already excluded (harmless, and stream leaks are
a
+ // pre-existing risk), whereas dropping first could remove the stream
of a table
+ // that is still active under the old property.
+ try {
+ dropStreamsForExcludedTables(db, mtmv, newExcludedTriggerTables,
isReplay);
+ } catch (UserException e) {
+ LOG.warn("failed to drop IVM streams for excluded trigger tables
of mv={}, "
+ + "the streams may leak: {}", mtmv.getName(),
e.getMessage());
+ }
+ }
+
+ private void createStreamsForUnExcludedTables(Database db, MTMV mtmv,
Set<TableNameInfo> oldExcludedTriggerTables, Set<TableNameInfo>
newExcludedTriggerTables,
- boolean isReplay) throws UserException {
+ List<String> createdStreamNames) throws UserException {
MTMVRelation relation = mtmv.getRelation();
if (relation == null || relation.getBaseTables() == null) {
return;
}
- Set<BaseTableInfo> baseTables = relation.getBaseTables();
- for (BaseTableInfo baseTableInfo : baseTables) {
+ int createdCount = 0;
+ for (BaseTableInfo baseTableInfo : relation.getBaseTables()) {
TableNameInfo baseTableName = new
TableNameInfo(baseTableInfo.getCtlName(),
baseTableInfo.getDbName(), baseTableInfo.getTableName());
- boolean wasExcluded = MTMVPartitionUtil.isTableExcluded(
- oldExcludedTriggerTables, baseTableName);
- boolean isExcluded = MTMVPartitionUtil.isTableExcluded(
- newExcludedTriggerTables, baseTableName);
- if (wasExcluded && !isExcluded) {
- if (!isReplay) {
- TableIf baseTable = MTMVUtil.getTable(baseTableInfo);
- CreateMTMVCommand.createTableStream(ConnectContext.get(),
db, mtmv, baseTable);
+ if (!MTMVPartitionUtil.isTableExcluded(oldExcludedTriggerTables,
baseTableName)
+ ||
MTMVPartitionUtil.isTableExcluded(newExcludedTriggerTables, baseTableName)) {
+ continue;
+ }
+ failCreateStreamIfDebugPointed(createdCount);
+ TableIf baseTable = MTMVUtil.getTable(baseTableInfo);
+ CreateMTMVCommand.createTableStream(ConnectContext.get(), db,
mtmv, baseTable);
+ createdStreamNames.add(IvmUtil.streamName(mtmv.getId(),
baseTable.getFullQualifiers()));
+ createdCount++;
+ }
+ }
+
+ /** Fails before the (allowedCreates+1)-th create when the create debug
point is enabled. */
+ private void failCreateStreamIfDebugPointed(int createdCount) throws
DdlException {
+ String allowCreates =
DebugPointUtil.getDebugParamOrDefault(DEBUG_POINT_CREATE_EXCLUDED_STREAM_FAIL,
"");
+ if (!allowCreates.isEmpty() && createdCount >=
Integer.parseInt(allowCreates)) {
+ throw new DdlException("debug point: creating IVM stream for an
excluded-trigger base table "
+ + "failed after " + allowCreates + " successful
create(s)");
+ }
+ }
+
+ /** Drops the named stream tables under the db write lock;
missing/non-stream tables are skipped. */
+ private void dropStreamsByNames(Database db, List<String> streamNames,
boolean isReplay)
+ throws UserException {
+ if (streamNames.isEmpty()) {
+ return;
+ }
+ db.writeLockOrDdlException();
+ try {
+ for (String streamName : streamNames) {
+ TableIf streamTable = db.getTableNullable(streamName);
+ if (!(streamTable instanceof BaseTableStream)) {
+ continue;
+ }
+ Table table = (Table) streamTable;
+ table.writeLock();
+ try {
+ Env.getCurrentEnv().unprotectDropTable(db, table, true,
isReplay, 0L);
+ } finally {
+ table.writeUnlock();
}
+ LOG.info("dropped IVM stream {}", streamName);
}
+ } finally {
+ db.writeUnlock();
+ }
+ }
+
+ private void dropStreamsForExcludedTables(Database db, MTMV mtmv,
+ Set<TableNameInfo> newExcludedTriggerTables, boolean isReplay)
throws UserException {
+ MTMVRelation relation = mtmv.getRelation();
+ if (relation == null || relation.getBaseTables() == null) {
+ return;
}
+ Set<BaseTableInfo> baseTables = relation.getBaseTables();
+ int droppedCount = 0;
db.writeLockOrDdlException();
try {
for (BaseTableInfo baseTableInfo : baseTables) {
@@ -1385,6 +1502,7 @@ public class Alter {
if
(!MTMVPartitionUtil.isTableExcluded(newExcludedTriggerTables, baseTableName)) {
continue;
}
+ failDropStreamIfDebugPointed(droppedCount);
List<String> baseTableFullQualifiers = baseTableInfo.toList();
String streamName = IvmUtil.streamName(mtmv.getId(),
baseTableFullQualifiers);
TableIf streamTable = db.getTableNullable(streamName);
@@ -1409,9 +1527,19 @@ public class Alter {
}
LOG.info("dropped IVM stream {} because its base table is
excluded from MTMV",
streamName);
+ droppedCount++;
}
} finally {
db.writeUnlock();
}
}
+
+ /** Fails before the (allowedDrops+1)-th drop when the drop debug point is
enabled. */
+ private void failDropStreamIfDebugPointed(int droppedCount) throws
DdlException {
+ String allowDrops =
DebugPointUtil.getDebugParamOrDefault(DEBUG_POINT_DROP_EXCLUDED_STREAM_FAIL,
"");
+ if (!allowDrops.isEmpty() && droppedCount >=
Integer.parseInt(allowDrops)) {
+ throw new DdlException("debug point: dropping an IVM stream of an
excluded-trigger base table "
+ + "failed after " + allowDrops + " successful drop(s)");
+ }
+ }
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Database.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Database.java
index 05369b9cbd2..55d3ec15602 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Database.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Database.java
@@ -176,6 +176,11 @@ public class Database extends MetaObject implements
Writable, DatabaseIf<Table>,
isDropped = false;
}
+ /** True while the database is being dropped (before it is removed from
the catalog). */
+ public boolean isDropped() {
+ return isDropped;
+ }
+
public void readLock() {
this.rwLock.readLock().lock();
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
index 50151ddb08f..51833705acf 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
@@ -7658,10 +7658,12 @@ public class Env {
this.alter.processAlterMTMV(alter, false);
}
- public void alterMTMVProperty(AlterMTMVPropertyInfo info) {
+ public void alterMTMVProperty(AlterMTMVPropertyInfo info) throws
UserException {
AlterMTMV alter = new AlterMTMV(info.getMvName(),
MTMVAlterOpType.ALTER_PROPERTY);
alter.setMvProperties(info.getProperties());
- this.alter.processAlterMTMV(alter, false);
+ // Runs outside the tolerant processAlterMTMV catch so that failures
(e.g. a
+ // partial IVM excluded-trigger-tables stream transition) reach the
client.
+ this.alter.processAlterMTMVProperty(alter, false);
}
public void alterMTMVStatus(TableNameInfo mvName, MTMVStatus status) {
diff --git a/fe/fe-core/src/test/java/org/apache/doris/mtmv/AlterMTMVTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/AlterMTMVTest.java
index ab525e44ecc..deeed90db1d 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/mtmv/AlterMTMVTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/mtmv/AlterMTMVTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.mtmv;
+import org.apache.doris.alter.Alter;
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.MTMV;
@@ -24,6 +25,7 @@ import org.apache.doris.catalog.Table;
import org.apache.doris.catalog.info.TableNameInfo;
import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
+import org.apache.doris.common.util.DebugPointUtil;
import org.apache.doris.mtmv.MTMVRefreshEnum.RefreshMethod;
import org.apache.doris.mtmv.ivm.IvmInfo;
import org.apache.doris.mtmv.ivm.IvmUtil;
@@ -624,4 +626,107 @@ public class AlterMTMVTest extends TestWithFeService {
alterMv("ALTER MATERIALIZED VIEW owner_mv SET
('excluded_trigger_tables' = 'owner_base1')");
Assertions.assertSame(conflictingStream,
db.getTableOrMetaException(streamName));
}
+
+ @Test
+ public void
testAlterIvmExcludedTriggerTablesCreateStreamFailureCompensatesAndFails()
throws Exception {
+ createDatabaseAndUse("alter_ivm_excl_create_fail_test");
+ createTable("CREATE TABLE excl_fail_base1 (k1 int, v1 int) UNIQUE
KEY(k1) "
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1 "
+ + "PROPERTIES ('replication_num' = '1',
'enable_unique_key_merge_on_write' = 'true', "
+ + "'binlog.enable' = 'true', 'binlog.format' = 'ROW', "
+ + "'binlog.need_historical_value' = 'true')");
+ createTable("CREATE TABLE excl_fail_base2 (k1 int, v1 int) UNIQUE
KEY(k1) "
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1 "
+ + "PROPERTIES ('replication_num' = '1',
'enable_unique_key_merge_on_write' = 'true', "
+ + "'binlog.enable' = 'true', 'binlog.format' = 'ROW', "
+ + "'binlog.need_historical_value' = 'true')");
+ createMvByNereids("CREATE MATERIALIZED VIEW excl_create_fail_mv\n"
+ + " BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL\n"
+ + " DISTRIBUTED BY RANDOM BUCKETS 2\n"
+ + " PROPERTIES ('replication_num' = '1',\n"
+ + " 'excluded_trigger_tables' = 'excl_fail_base1,
excl_fail_base2')\n"
+ + " AS SELECT k1, v1 FROM excl_fail_base1 UNION ALL SELECT k1,
v1 FROM excl_fail_base2");
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("alter_ivm_excl_create_fail_test");
+ MTMV mtmv = (MTMV) db.getTableOrMetaException("excl_create_fail_mv");
+ String stream1 = ivmStreamName(db, mtmv.getId(), "excl_fail_base1");
+ String stream2 = ivmStreamName(db, mtmv.getId(), "excl_fail_base2");
+ Assertions.assertFalse(db.getTable(stream1).isPresent());
+ Assertions.assertFalse(db.getTable(stream2).isPresent());
+
+ boolean originEnableDebugPoints = Config.enable_debug_points;
+ try {
+ Config.enable_debug_points = true;
+ DebugPointUtil.clearDebugPoints();
+ // Allow the first stream create to succeed and fail the second
one (the count
+ // makes this independent of the base-table iteration order), so
the
+ // compensation must drop the first stream again.
+ DebugPointUtil.addDebugPointWithValue(
+ Alter.DEBUG_POINT_CREATE_EXCLUDED_STREAM_FAIL, "1");
+ Exception exception = Assertions.assertThrows(Exception.class,
+ () -> alterMv("ALTER MATERIALIZED VIEW
excl_create_fail_mv\n"
+ + " SET ('excluded_trigger_tables' = '')"));
+ Assertions.assertTrue(exception.getMessage().contains("debug
point"),
+ "unexpected error message: " + exception.getMessage());
+ } finally {
+ DebugPointUtil.clearDebugPoints();
+ Config.enable_debug_points = originEnableDebugPoints;
+ }
+
+ // Compensated: no stream remains and the property still excludes both
tables.
+ Assertions.assertFalse(db.getTable(stream1).isPresent(),
+ "compensation must drop the stream created before the failing
one");
+ Assertions.assertFalse(db.getTable(stream2).isPresent());
+ Assertions.assertEquals(2, mtmv.getExcludedTriggerTables().size());
+ }
+
+ @Test
+ public void
testAlterIvmExcludedTriggerTablesDropStreamFailureIsBestEffort() throws
Exception {
+ createDatabaseAndUse("alter_ivm_excl_drop_fail_test");
+ createTable("CREATE TABLE excl_drop_base1 (k1 int, v1 int) UNIQUE
KEY(k1) "
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1 "
+ + "PROPERTIES ('replication_num' = '1',
'enable_unique_key_merge_on_write' = 'true', "
+ + "'binlog.enable' = 'true', 'binlog.format' = 'ROW', "
+ + "'binlog.need_historical_value' = 'true')");
+ createTable("CREATE TABLE excl_drop_base2 (k1 int, v1 int) UNIQUE
KEY(k1) "
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1 "
+ + "PROPERTIES ('replication_num' = '1',
'enable_unique_key_merge_on_write' = 'true', "
+ + "'binlog.enable' = 'true', 'binlog.format' = 'ROW', "
+ + "'binlog.need_historical_value' = 'true')");
+ createMvByNereids("CREATE MATERIALIZED VIEW excl_drop_fail_mv\n"
+ + " BUILD DEFERRED REFRESH INCREMENTAL ON MANUAL\n"
+ + " DISTRIBUTED BY RANDOM BUCKETS 2\n"
+ + " PROPERTIES ('replication_num' = '1')\n"
+ + " AS SELECT k1, v1 FROM excl_drop_base1 UNION ALL SELECT k1,
v1 FROM excl_drop_base2");
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("alter_ivm_excl_drop_fail_test");
+ MTMV mtmv = (MTMV) db.getTableOrMetaException("excl_drop_fail_mv");
+ String stream1 = ivmStreamName(db, mtmv.getId(), "excl_drop_base1");
+ String stream2 = ivmStreamName(db, mtmv.getId(), "excl_drop_base2");
+ Assertions.assertTrue(db.getTable(stream1).isPresent());
+ Assertions.assertTrue(db.getTable(stream2).isPresent());
+
+ boolean originEnableDebugPoints = Config.enable_debug_points;
+ try {
+ Config.enable_debug_points = true;
+ DebugPointUtil.clearDebugPoints();
+ // Both bases join the excluded set; allow one stream drop to
succeed and fail
+ // the next one (the count makes this independent of the
base-table iteration
+ // order). The property is already applied and the ALTER must
still succeed:
+ // exactly one of the two now-unused streams leaks.
+ DebugPointUtil.addDebugPointWithValue(
+ Alter.DEBUG_POINT_DROP_EXCLUDED_STREAM_FAIL, "1");
+ alterMv("ALTER MATERIALIZED VIEW excl_drop_fail_mv\n"
+ + " SET ('excluded_trigger_tables' = 'excl_drop_base1,
excl_drop_base2')");
+ } finally {
+ DebugPointUtil.clearDebugPoints();
+ Config.enable_debug_points = originEnableDebugPoints;
+ }
+
+ Assertions.assertEquals(2, mtmv.getExcludedTriggerTables().size());
+ boolean stream1Present = db.getTable(stream1).isPresent();
+ boolean stream2Present = db.getTable(stream2).isPresent();
+ Assertions.assertEquals(1, (stream1Present ? 1 : 0) + (stream2Present
? 1 : 0),
+ "exactly one stream drop must have failed and leaked");
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]