This is an automated email from the ASF dual-hosted git repository.
tkhurana pushed a commit to branch PHOENIX-7562-feature-new
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/PHOENIX-7562-feature-new by
this push:
new a45c9a8b17 PHOENIX-8013 Tests for tenant writes to SYSTEM.CATALOG and
SYSTEM.CHILD_LINK tables
a45c9a8b17 is described below
commit a45c9a8b170a6b4880a427c0d959db8fc414a540
Author: tkhurana <[email protected]>
AuthorDate: Thu Oct 1 12:03:16 2026 -0700
PHOENIX-8013 Tests for tenant writes to SYSTEM.CATALOG and
SYSTEM.CHILD_LINK tables
---
.../phoenix/replication/ReplicationLogGroupIT.java | 236 ++++++++++++++++++---
.../replication/ReplicationLogBaseTest.java | 11 +
.../replication/ReplicationLogGroupTest.java | 57 +++--
3 files changed, 255 insertions(+), 49 deletions(-)
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
index a512b79389..ebc5a1573f 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/replication/ReplicationLogGroupIT.java
@@ -37,6 +37,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Properties;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -61,13 +62,13 @@ import
org.apache.phoenix.hbase.index.metrics.MetricsHaBypassSourceImpl;
import org.apache.phoenix.jdbc.ClusterRoleRecord;
import org.apache.phoenix.jdbc.FailoverPhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixResultSet;
-import org.apache.phoenix.query.PhoenixTestBuilder;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.replication.reader.ReplicationLogProcessor;
import org.apache.phoenix.util.CDCUtil;
+import org.apache.phoenix.util.MetaDataUtil;
+import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.QueryUtil;
import org.junit.BeforeClass;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@@ -1024,20 +1025,213 @@ public class ReplicationLogGroupIT extends
ReplicationLogGroupBaseIT {
indexName);
}
- @Ignore("Mutations on SYSTEM.CATALOG and SYSTEM.CHILD_LINK are generated on
the server side and don't have the HAGroup attribute set")
+ /**
+ * End-to-end replication of the tenant-owned SYSTEM.CATALOG and
SYSTEM.CHILD_LINK rows that back
+ * a tenant view. Models the intended HA topology: the base table and the
global view are global
+ * objects created independently on both clusters (their catalog/child-link
rows are non-tenant
+ * and never replicate), while the tenant view is created only on the active
over an HA connection
+ * so its tenant-owned metadata and data replicate to the standby. After
replaying the log onto
+ * cluster 2, the tenant view must materialize there purely from the
replicated metadata, and the
+ * base physical table must match cluster 1 cell-for-cell.
+ * <p>
+ * This exercises the production path in {@code IndexRegionObserver} that
ships tenant-owned
+ * SYSTEM.CATALOG / SYSTEM.CHILD_LINK rows -- the DDL mutations carry {@code
_HAGroupName} because
+ * they are built through the client {@code MutationState} of a
FailoverPhoenix (HA) connection.
+ */
+ @Test
public void testSystemTables() throws Exception {
- createViewHierarchy();
+ final String baseTable = "T_" + generateUniqueName();
+ final String globalView = "GV_" + generateUniqueName();
+ final String tenantView = "TV_" + generateUniqueName();
+ final String tenantIndex = "TVI_" + generateUniqueName();
+ final String tenantId = "TENANT_" + name.getMethodName();
+ final int rowCount = 5;
+
+ final String baseTableDdl = "CREATE TABLE IF NOT EXISTS " + baseTable
+ + " (TENANT_ID VARCHAR NOT NULL, KP CHAR(3) NOT NULL, ID CHAR(10) NOT
NULL, NUM BIGINT "
+ + "CONSTRAINT PK PRIMARY KEY (TENANT_ID, KP, ID)) MULTI_TENANT=true";
+ // Equality predicate keeps the view (and the tenant view derived from it)
updatable.
+ final String globalViewDdl = "CREATE VIEW IF NOT EXISTS " + globalView
+ + " (COL1 VARCHAR) AS SELECT * FROM " + baseTable + " WHERE KP = 'abc'";
+ final String tenantViewDdl = "CREATE VIEW " + tenantView + " AS SELECT *
FROM " + globalView;
+
+ // Global objects on the active: base table + global view. Created over
the HA connection, but
+ // their catalog / child-link rows are non-tenant and are filtered out of
replication.
+ try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection)
DriverManager
+ .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+ conn.createStatement().execute(baseTableDdl);
+ conn.createStatement().execute(globalViewDdl);
+ conn.commit();
+ }
+
+ // Tenant view + tenant data on the active over a tenant-scoped HA
connection: these carry
+ // _HAGroupName and are the only rows that replicate.
+ Properties tenantProps = (Properties) clientProps.clone();
+ tenantProps.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+ try (
+ Connection tenantConn =
DriverManager.getConnection(CLUSTERS.getJdbcHAUrl(), tenantProps)) {
+ tenantConn.createStatement().execute(tenantViewDdl);
+ tenantConn.createStatement()
+ .execute("CREATE INDEX " + tenantIndex + " ON " + tenantView + "
(COL1)");
+ tenantConn.commit();
+ PreparedStatement stmt = tenantConn
+ .prepareStatement("UPSERT INTO " + tenantView + " (ID, NUM, COL1)
VALUES (?, ?, ?)");
+ for (int i = 0; i < rowCount; i++) {
+ stmt.setString(1, "row" + i);
+ stmt.setLong(2, i + 1);
+ stmt.setString(3, "val_" + i);
+ stmt.executeUpdate();
+ }
+ tenantConn.commit();
+ }
+
+ // The only SYSTEM.* rows shipped are the tenant view's catalog rows and
its single child link.
+ logGroup.close();
Map<String, List<Mutation>> logsByTable = groupLogsByTable();
dumpTableLogCount(logsByTable);
- // find all the log entries for system tables
Map<String,
List<Mutation>> systemTables = logsByTable.entrySet().stream()
.filter(entry ->
entry.getKey().startsWith(QueryConstants.SYSTEM_SCHEMA_NAME))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- // there should be only 2 entries CATALOG, CHILD_LINK
- assertEquals(2, systemTables.size());
- assertEquals(1, getCountForTable(systemTables, SYSTEM_CHILD_LINK_NAME));
- assertTrue(getCountForTable(systemTables, SYSTEM_CATALOG_NAME) > 0);
+ assertEquals("Only SYSTEM.CATALOG and SYSTEM.CHILD_LINK should replicate",
2,
+ systemTables.size());
+ assertEquals("One tenant view => one tenant-owned child link", 1,
+ getCountForTable(systemTables, SYSTEM_CHILD_LINK_NAME));
+ assertTrue("Tenant view catalog rows should replicate",
+ getCountForTable(systemTables, SYSTEM_CATALOG_NAME) > 0);
+ assertTrue("Tenant data written through the base physical table should
replicate",
+ getCountForTable(logsByTable, baseTable) > 0);
+
+ // Recreate the global objects on the standby and replay. The MULTI_TENANT
base table auto-
+ // creates the shared view-index physical table, so the standby IRO
regenerates the tenant view
+ // index entries from the replayed base data; both the base table and that
index physical table
+ // must match cluster 1 cell-for-cell.
+ String viewIndexPhysicalName =
MetaDataUtil.getViewIndexPhysicalName(baseTable);
+ replayAndVerifyAcrossClusters(Arrays.asList(baseTableDdl, globalViewDdl),
baseTable,
+ viewIndexPhysicalName);
+
+ // The tenant view materializes on the standby from the replayed metadata
alone, and the
+ // COUNT(*) is served by the (regenerated) tenant view index -- the
optimizer prefers it over a
+ // full base-table scan.
+ Properties tenantProps2 = new Properties();
+ tenantProps2.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+ try (
+ Connection tenantConn2 =
+ DriverManager.getConnection(CLUSTERS.getJdbcUrl2(haGroup),
tenantProps2);
+ Statement stmt = tenantConn2.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + tenantView)) {
+ String explainPlan =
+
QueryUtil.getExplainPlan(rs.unwrap(PhoenixResultSet.class).getUnderlyingIterator());
+ assertTrue("COUNT(*) on the tenant view should be served by the tenant
view index, plan was: "
+ + explainPlan, explainPlan.contains(viewIndexPhysicalName));
+ assertTrue(rs.next());
+ assertEquals("Tenant view should be queryable on the standby after
replay", rowCount,
+ rs.getInt(1));
+ }
+ }
+
+ /**
+ * Production variant of {@link #testSystemTables()} where the index lives
on the GLOBAL view and
+ * there is no tenant view index: base table, global view, and a global view
index are global
+ * objects created independently on both clusters (none of their metadata
replicates), while only
+ * the tenant view and its upserts are created on the active over an HA
connection. Tenant upserts
+ * still populate the shared view-index physical table with tenant-scoped
entries, so on the
+ * standby the IRO regenerates those global-view-index entries from the
replayed base data even
+ * though no index metadata was replicated. Both the base table and the
shared index physical
+ * table must match cluster 1, and the tenant view COUNT(*) on the standby
must be served by the
+ * global view index.
+ */
+ @Test
+ public void testSystemTablesWithGlobalViewIndex() throws Exception {
+ final String baseTable = "T_" + generateUniqueName();
+ final String globalView = "GV_" + generateUniqueName();
+ final String globalIndex = "GVI_" + generateUniqueName();
+ final String tenantView = "TV_" + generateUniqueName();
+ final String tenantId = "TENANT_" + name.getMethodName();
+ final int rowCount = 5;
+
+ final String baseTableDdl = "CREATE TABLE IF NOT EXISTS " + baseTable
+ + " (TENANT_ID VARCHAR NOT NULL, KP CHAR(3) NOT NULL, ID CHAR(10) NOT
NULL, NUM BIGINT "
+ + "CONSTRAINT PK PRIMARY KEY (TENANT_ID, KP, ID)) MULTI_TENANT=true";
+ // Equality predicate keeps the view (and the tenant view derived from it)
updatable.
+ final String globalViewDdl = "CREATE VIEW IF NOT EXISTS " + globalView
+ + " (COL1 VARCHAR) AS SELECT * FROM " + baseTable + " WHERE KP = 'abc'";
+ final String globalIndexDdl =
+ "CREATE INDEX IF NOT EXISTS " + globalIndex + " ON " + globalView + "
(COL1)";
+ final String tenantViewDdl = "CREATE VIEW " + tenantView + " AS SELECT *
FROM " + globalView;
+
+ // Global objects on the active: base table + global view + global view
index. All are
+ // non-tenant
+ // and their catalog / child-link rows are filtered out of replication.
+ try (FailoverPhoenixConnection conn = (FailoverPhoenixConnection)
DriverManager
+ .getConnection(CLUSTERS.getJdbcHAUrl(), clientProps)) {
+ conn.createStatement().execute(baseTableDdl);
+ conn.createStatement().execute(globalViewDdl);
+ conn.createStatement().execute(globalIndexDdl);
+ conn.commit();
+ }
+
+ // Tenant view + tenant data on the active over a tenant-scoped HA
connection: these carry
+ // _HAGroupName and are the only rows that replicate. No tenant view index
here.
+ Properties tenantProps = (Properties) clientProps.clone();
+ tenantProps.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+ try (
+ Connection tenantConn =
DriverManager.getConnection(CLUSTERS.getJdbcHAUrl(), tenantProps)) {
+ tenantConn.createStatement().execute(tenantViewDdl);
+ tenantConn.commit();
+ PreparedStatement stmt = tenantConn
+ .prepareStatement("UPSERT INTO " + tenantView + " (ID, NUM, COL1)
VALUES (?, ?, ?)");
+ for (int i = 0; i < rowCount; i++) {
+ stmt.setString(1, "row" + i);
+ stmt.setLong(2, i + 1);
+ stmt.setString(3, "val_" + i);
+ stmt.executeUpdate();
+ }
+ tenantConn.commit();
+ }
+
+ // The only SYSTEM.* rows shipped are the tenant view's catalog rows and
its single child link;
+ // the global view index contributes no replicated metadata.
+ logGroup.close();
+ Map<String, List<Mutation>> logsByTable = groupLogsByTable();
+ dumpTableLogCount(logsByTable);
+ Map<String,
+ List<Mutation>> systemTables = logsByTable.entrySet().stream()
+ .filter(entry ->
entry.getKey().startsWith(QueryConstants.SYSTEM_SCHEMA_NAME))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ assertEquals("Only SYSTEM.CATALOG and SYSTEM.CHILD_LINK should replicate",
2,
+ systemTables.size());
+ assertEquals("One tenant view => one tenant-owned child link", 1,
+ getCountForTable(systemTables, SYSTEM_CHILD_LINK_NAME));
+ assertTrue("Tenant view catalog rows should replicate",
+ getCountForTable(systemTables, SYSTEM_CATALOG_NAME) > 0);
+ assertTrue("Tenant data written through the base physical table should
replicate",
+ getCountForTable(logsByTable, baseTable) > 0);
+
+ // Recreate the global objects (including the global view index) on the
standby and replay. The
+ // standby IRO regenerates the global-view-index entries for the tenant
rows from the replayed
+ // base data; both the base table and the shared index physical table must
match cluster 1.
+ String viewIndexPhysicalName =
MetaDataUtil.getViewIndexPhysicalName(baseTable);
+ replayAndVerifyAcrossClusters(Arrays.asList(baseTableDdl, globalViewDdl,
globalIndexDdl),
+ baseTable, viewIndexPhysicalName);
+
+ // The tenant view materializes on the standby from the replayed metadata
alone, and the
+ // COUNT(*) is served by the (regenerated) global view index.
+ Properties tenantProps2 = new Properties();
+ tenantProps2.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+ try (
+ Connection tenantConn2 =
+ DriverManager.getConnection(CLUSTERS.getJdbcUrl2(haGroup),
tenantProps2);
+ Statement stmt = tenantConn2.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + tenantView)) {
+ String explainPlan =
+
QueryUtil.getExplainPlan(rs.unwrap(PhoenixResultSet.class).getUnderlyingIterator());
+ assertTrue("COUNT(*) on the tenant view should be served by the global
view index, plan was: "
+ + explainPlan, explainPlan.contains(viewIndexPhysicalName));
+ assertTrue(rs.next());
+ assertEquals("Tenant view should be queryable on the standby after
replay", rowCount,
+ rs.getInt(1));
+ }
}
/**
@@ -1430,28 +1624,4 @@ public class ReplicationLogGroupIT extends
ReplicationLogGroupBaseIT {
assertTrue("Rotation must resume after the abort clears failover-pending",
findLogFiles(peerDir, peerFs).size() > filesAtCutover);
}
-
- private PhoenixTestBuilder.SchemaBuilder createViewHierarchy() throws
Exception {
- // Define the test schema.
- // 1. Table with columns => (ORG_ID, KP, COL1, COL2, COL3), PK => (ORG_ID,
KP)
- // 2. GlobalView with columns => (ID, COL4, COL5, COL6), PK => (ID)
- // 3. Tenant with columns => (ZID, COL7, COL8, COL9), PK => (ZID)
- final PhoenixTestBuilder.SchemaBuilder schemaBuilder =
- new PhoenixTestBuilder.SchemaBuilder(CLUSTERS.getJdbcHAUrl());
- PhoenixTestBuilder.SchemaBuilder.ConnectOptions connectOptions =
- new PhoenixTestBuilder.SchemaBuilder.ConnectOptions();
- connectOptions.setConnectProps(clientProps);
- PhoenixTestBuilder.SchemaBuilder.TableOptions tableOptions =
- PhoenixTestBuilder.SchemaBuilder.TableOptions.withDefaults();
- PhoenixTestBuilder.SchemaBuilder.GlobalViewOptions globalViewOptions =
- PhoenixTestBuilder.SchemaBuilder.GlobalViewOptions.withDefaults();
- PhoenixTestBuilder.SchemaBuilder.TenantViewOptions
tenantViewWithOverrideOptions =
- PhoenixTestBuilder.SchemaBuilder.TenantViewOptions.withDefaults();
- PhoenixTestBuilder.SchemaBuilder.TenantViewIndexOptions
tenantViewIndexOverrideOptions =
- PhoenixTestBuilder.SchemaBuilder.TenantViewIndexOptions.withDefaults();
-
schemaBuilder.withConnectOptions(connectOptions).withTableOptions(tableOptions)
-
.withGlobalViewOptions(globalViewOptions).withTenantViewOptions(tenantViewWithOverrideOptions)
-
.withTenantViewIndexOptions(tenantViewIndexOverrideOptions).buildWithNewTenant();
- return schemaBuilder;
- }
}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogBaseTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogBaseTest.java
index eb05d963c8..1434996345 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogBaseTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogBaseTest.java
@@ -36,6 +36,7 @@ import org.apache.phoenix.jdbc.HAGroupStoreRecord;
import org.apache.phoenix.jdbc.HAGroupStoreRecord.HAGroupState;
import org.apache.phoenix.jdbc.HighAvailabilityPolicy;
import org.apache.phoenix.replication.log.LogFileWriter;
+import org.apache.phoenix.replication.metrics.MetricsReplicationLogGroupSource;
import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.junit.After;
import org.junit.Before;
@@ -176,6 +177,16 @@ public class ReplicationLogBaseTest {
return spy(new TestableLog(this, shardManager, useAlignedRotation));
}
+ /**
+ * Wrap the real metrics source in a spy so tests can assert on the values
passed to
+ * {@code update*} at emit time. Asserting on the source's live histograms
is racy: they are
+ * registered with the JVM-global metrics2 system, whose periodic sampler
resets each histogram
+ * via {@code snapshotAndReset()} and can zero it between emit and read.
+ */
+ @Override
+ protected MetricsReplicationLogGroupSource createMetricsSource() {
+ return spy(super.createMetricsSource());
+ }
}
/**
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
index 2b337cbd6a..bdbfa65706 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/replication/ReplicationLogGroupTest.java
@@ -33,6 +33,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -2906,26 +2907,50 @@ public class ReplicationLogGroupTest extends
ReplicationLogBaseTest {
// before we read. close() is idempotent, so tearDown's close() is a no-op.
logGroup.close();
- // getCurrentMetricValues() snapshots and resets the histogram bins, so
call it exactly once.
- ReplicationLogMetricValues values =
logGroup.getMetrics().getCurrentMetricValues();
+ // Assert on the values passed to the metrics source at emit time,
captured from the spy. The
+ // source's histograms are registered with the JVM-global metrics2 system,
whose periodic
+ // sampler resets them via snapshotAndReset() and can zero getMax()
between emit and read -- so
+ // reading the live histogram (getCurrentMetricValues()) is racy and
flaked on "batchSize == 0".
+ MetricsReplicationLogGroupSource metrics = logGroup.getMetrics();
+ ArgumentCaptor<Long> appendTime = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> ringBufferTime = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> pendingSyncWaitTime =
ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> pendingSyncCount =
ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> syncTime = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> fsSyncTime = ArgumentCaptor.forClass(Long.class);
+ verify(metrics, atLeastOnce()).updateAppendTime(appendTime.capture());
+ verify(metrics,
atLeastOnce()).updateRingBufferTime(ringBufferTime.capture());
+ verify(metrics,
atLeastOnce()).updatePendingSyncWaitTime(pendingSyncWaitTime.capture());
+ verify(metrics,
atLeastOnce()).updatePendingSyncCount(pendingSyncCount.capture());
+ verify(metrics, atLeastOnce()).updateSyncTime(syncTime.capture());
+ verify(metrics, atLeastOnce()).updateFsSyncTime(fsSyncTime.capture());
// appendTime brackets only the uncontended ring-buffer publish -- the
shortest interval of all
// these metrics. Nanosecond units are not nanosecond resolution: on a
fast machine every
// publish can floor to a 0ns delta, so assert presence (>= 0) rather than
strict positivity.
- assertTrue("appendTime should be >= 0, got " + values.getAppendTimeMax(),
- values.getAppendTimeMax() >= 0);
- assertTrue("ringBufferTime should be >= 0, got " +
values.getRingBufferTimeMax(),
- values.getRingBufferTimeMax() >= 0);
- assertTrue("pendingSyncWaitTime should be >= 0, got " +
values.getPendingSyncWaitTimeMax(),
- values.getPendingSyncWaitTimeMax() >= 0);
- // Counts.
- assertTrue("pendingSyncCount should be > 0, got " +
values.getPendingSyncCountMax(),
- values.getPendingSyncCountMax() > 0);
- // Millisecond-resolution timers: the injected fsync delay clears the
truncation floor.
- assertTrue("syncTime should be > 0, got " + values.getSyncTimeMax(),
- values.getSyncTimeMax() > 0);
- assertTrue("fsSyncTime should be > 0, got " + values.getFsSyncTimeMax(),
- values.getFsSyncTimeMax() > 0);
+ assertTrue("appendTime should be >= 0, got " +
max(appendTime.getAllValues()),
+ max(appendTime.getAllValues()) >= 0);
+ assertTrue("ringBufferTime should be >= 0, got " +
max(ringBufferTime.getAllValues()),
+ max(ringBufferTime.getAllValues()) >= 0);
+ assertTrue("pendingSyncWaitTime should be >= 0, got " +
max(pendingSyncWaitTime.getAllValues()),
+ max(pendingSyncWaitTime.getAllValues()) >= 0);
+ assertTrue("pendingSyncCount should be > 0, got " +
max(pendingSyncCount.getAllValues()),
+ max(pendingSyncCount.getAllValues()) > 0);
+ // Millisecond-resolution timers: the injected fsync delay clears the
truncation floor. syncTime
+ // and fsSyncTime are recorded in ns here; the source truncates to ms
internally, but we assert
+ // on the raw ns argument, which the 2ms injected delay keeps comfortably
positive.
+ assertTrue("syncTime should be > 0, got " + max(syncTime.getAllValues()),
+ max(syncTime.getAllValues()) > 0);
+ assertTrue("fsSyncTime should be > 0, got " +
max(fsSyncTime.getAllValues()),
+ max(fsSyncTime.getAllValues()) > 0);
+ }
+
+ private static long max(List<Long> values) {
+ long result = Long.MIN_VALUE;
+ for (Long v : values) {
+ result = Math.max(result, v);
+ }
+ return result;
}
/**