voonhous commented on code in PR #18942:
URL: https://github.com/apache/hudi/pull/18942#discussion_r3829506189
##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metrics/HoodieMetrics.java:
##########
@@ -456,8 +459,12 @@ public void updateClusteringFileCreationMetrics(long
durationInMs) {
reportMetrics(HoodieTimeline.CLUSTERING_ACTION, "fileCreationTime",
durationInMs);
}
- public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline) {
- updateEarliestPendingInstant(activeTimeline,
EARLIEST_PENDING_CLUSTERING_INSTANT_STR, HoodieTimeline.CLUSTERING_ACTION);
+ public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline, final InstantGenerator instantGenerator) {
Review Comment:
Selection logic is fine, but this method now does storage IO on every commit
even when metrics are disabled -- and disabled is the default.
Before this PR `updateTableServiceInstantMetrics` was pure in-memory
action-name matching, so the missing guard cost nothing. Now the predicate
reaches `timeline.readRequestedReplaceMetadata(...)` ->
`readNonEmptyInstantContent` -> `ActiveTimelineV2.getContentStream` ->
`metaClient.getStorage().open(...)` for every pending replacecommit, inside
`postCommit`. `hoodie.metrics.on` defaults to `false`
(`HoodieMetricsConfig.java:58-62`), and the only guard is inside `updateMetric`
at `HoodieMetrics.java:583`, i.e. after the reads have already happened.
This is also the one entry point that `1fd2c3671a80` ("fix(metrics): NPE
handling when hudi metrics is disabled", #18947) missed:
`testUpdateMethodsAreNoOpsWhenMetricsOff` (`TestHoodieMetrics.java:411`)
enumerates 19 update methods and this is not one of them.
Please add the guard before merge, and add
`updateTableServiceInstantMetrics` to that test:
```suggestion
public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline, final InstantGenerator instantGenerator) {
if (!config.isMetricsOn()) {
return;
}
```
##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metrics/HoodieMetrics.java:
##########
@@ -484,13 +491,35 @@ private void updateEarliestPendingInstant(final
HoodieActiveTimeline activeTimel
final String metricName,
final String action) {
Set<String> validActions = CollectionUtils.createSet(action);
- HoodieTimeline filteredInstants =
activeTimeline.filterInflightsAndRequested().filter(instant ->
validActions.contains(instant.getAction()));
+ updateEarliestPendingInstant(activeTimeline, metricName, action, instant
-> validActions.contains(instant.getAction()));
+ }
+
+ private void updateEarliestPendingInstant(final HoodieActiveTimeline
activeTimeline,
+ final String metricName,
+ final String action,
+ final Predicate<HoodieInstant>
pendingFilter) {
+ HoodieTimeline filteredInstants =
activeTimeline.filterInflightsAndRequested().filter(pendingFilter);
Option<HoodieInstant> hoodieInstantOption =
filteredInstants.firstInstant();
if (hoodieInstantOption.isPresent()) {
updateTimestampMetric(metricName, action, hoodieInstantOption);
}
}
+ /**
+ * Whether a pending instant belongs to a clustering operation, on any table
version.
+ * A plan that cannot be read is treated as non-clustering so that metrics
never fail the commit.
+ */
+ private boolean isPendingClusteringInstant(final HoodieActiveTimeline
activeTimeline,
+ final HoodieInstant instant,
+ final InstantGenerator
instantGenerator) {
+ try {
+ return ClusteringUtils.isClusteringInstant(activeTimeline, instant,
instantGenerator);
+ } catch (Exception e) {
+ log.warn("Failed to read the replace metadata of {} while updating
clustering instant metrics", instant, e);
+ return false;
+ }
Review Comment:
The catch itself is right and load-bearing -- `postCommit`
(`BaseHoodieWriteClient.java:663-680`) has a `finally` but no `catch`, and
`hoodie.write.can.ignore.post.commit.failures` defaults to `false`
(`HoodieWriteConfig.java:747-748`), so an escaping exception would fail the
commit. That was impossible before this PR because the method did no IO. Worth
keeping.
Two follow-ups:
1. **Nothing exercises it.** Add a case whose timeline throws from the
metadata read, asserting `assertDoesNotThrow(...)` and
`pendingClusteringInstantCount == 0`. Without it the fallback degrades silently
to exactly the bug being fixed (count 0) and no test would notice.
2. **`warn` with a stacktrace is too loud for the case that will actually
hit this.** `ClusteringUtils.isClusteringInstant` calls the 3-arg
`getClusteringPlan`, which passes `Option.empty()` for the metaClient
(`ClusteringUtils.java:271-272`) and so deliberately opts out of the
concurrent-rollback recovery added by `f64c93ee899c` (#18288, "do not fail if
replacecommit was rolled back already (by a concurrent writer)"). A pending
replacecommit rolled back between timeline load and `postCommit` therefore logs
a full stacktrace once per instant per commit until the timeline is reloaded.
Please log at `debug`, or drop the exception argument.
##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metrics/HoodieMetrics.java:
##########
@@ -456,8 +459,12 @@ public void updateClusteringFileCreationMetrics(long
durationInMs) {
reportMetrics(HoodieTimeline.CLUSTERING_ACTION, "fileCreationTime",
durationInMs);
}
- public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline) {
- updateEarliestPendingInstant(activeTimeline,
EARLIEST_PENDING_CLUSTERING_INSTANT_STR, HoodieTimeline.CLUSTERING_ACTION);
+ public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline, final InstantGenerator instantGenerator) {
+ // Clustering is scheduled as CLUSTERING_ACTION only on timeline layout 2.
On table version six it is scheduled as
+ // REPLACE_COMMIT_ACTION, which insert_overwrite and delete_partition
share, so the clustering plan is what
+ // identifies it rather than the action name.
Review Comment:
nit, feel free to ignore: this names the wrong field. Selection is on the
requested replace metadata's `operationType`, not on the clustering plan --
`ClusteringUtils.java:285` is
`WriteOperationType.CLUSTER.name().equals(requestedReplaceMetadata.get().getOperationType())`,
and the plan is only read once that check has passed. The test's own fixture
proves it: both instants carry a non-null plan and only the operationType
differs. Same wording is in the commit message and the PR body.
```suggestion
// Clustering is scheduled as CLUSTERING_ACTION only on timeline layout
2. On table version six it is scheduled as
// REPLACE_COMMIT_ACTION, which insert_overwrite and delete_partition
share, so the requested replace metadata's
// operationType is what identifies it rather than the action name.
```
##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metrics/HoodieMetrics.java:
##########
@@ -456,8 +459,12 @@ public void updateClusteringFileCreationMetrics(long
durationInMs) {
reportMetrics(HoodieTimeline.CLUSTERING_ACTION, "fileCreationTime",
durationInMs);
}
- public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline) {
- updateEarliestPendingInstant(activeTimeline,
EARLIEST_PENDING_CLUSTERING_INSTANT_STR, HoodieTimeline.CLUSTERING_ACTION);
+ public void updateTableServiceInstantMetrics(final HoodieActiveTimeline
activeTimeline, final InstantGenerator instantGenerator) {
+ // Clustering is scheduled as CLUSTERING_ACTION only on timeline layout 2.
On table version six it is scheduled as
+ // REPLACE_COMMIT_ACTION, which insert_overwrite and delete_partition
share, so the clustering plan is what
+ // identifies it rather than the action name.
+ Predicate<HoodieInstant> pendingClustering = instant ->
isPendingClusteringInstant(activeTimeline, instant, instantGenerator);
+ updateEarliestPendingInstant(activeTimeline,
EARLIEST_PENDING_CLUSTERING_INSTANT_STR, HoodieTimeline.CLUSTERING_ACTION,
pendingClustering);
Review Comment:
This predicate re-implements a timeline API that already exists and is
already layout-aware, and it costs twice the IO it needs to.
`HoodieTimeline.filterPendingClusteringTimeline()` (declared
`HoodieTimeline.java:441`) is implemented per layout:
- `BaseTimelineV1.java:62-68` -- `REPLACE_COMMIT_ACTION && !isCompleted() &&
ClusteringUtils.isClusteringInstant(this, i, instantGenerator)`, identical to
this predicate but using the timeline's own generator.
- `BaseTimelineV2.java:67-71` -- `CLUSTERING_ACTION && !isCompleted()`, zero
IO.
Two consequences of hand-rolling it here:
1. **The plan read is paid on every layout.** On tv8/9/10, the common case,
the layout already guarantees clustering has its own action, yet this now opens
and avro-parses `<ts>.replacecommit.requested` for every pending
insert_overwrite / delete_partition / bucket_rescale on every commit, only ever
to return false.
2. **It runs twice.** `filter()` is eager (`BaseHoodieTimeline.java:358` ->
`:96`, `setInstants(instants.collect(toList()))`), so `firstInstant()` at
`:502` does not short-circuit, and the same predicate object is filtered again
independently at `:578`. That is 2N `storage.open()` round trips per commit for
N pending replacecommits, and 4N for any instant whose requested file is
zero-length, since `ClusteringUtils.java:196-201` falls back to a second full
read via `isEmptyReplaceOrClusteringInstant`.
Concretely: drop the `Predicate` overloads, the `InstantGenerator` parameter
and both `BaseHoodieWriteClient` hunks, and materialize the timeline once:
```java
HoodieTimeline pendingClustering =
activeTimeline.filterPendingClusteringTimeline();
```
then feed `pendingClustering.firstInstant()` and
`pendingClustering.countInstants()` to the two clustering metrics. The PR
shrinks to a single production file.
One semantic difference to call out in the description if you take this: on
layout 2 it will not count a pending `replacecommit` clustering left over from
a v6 table. `UpgradeDowngrade` rolls back failed writes before upgrading so it
should not arise, but state it explicitly rather than paying the read
everywhere to cover it.
##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/TestHoodieMetrics.java:
##########
@@ -320,13 +324,61 @@ public void testTimerCtxandGauges() throws
InterruptedException {
assertEquals((long)metrics.getRegistry().getGauges().get(metricName).getValue(),
6L);
}
+ @Test
+ void testPendingClusteringInstantMetricsOnTableVersionSix() {
+ // Table version six schedules clustering as REPLACE_COMMIT_ACTION, which
insert_overwrite shares.
+ HoodieInstant pendingInsertOverwrite =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1001");
+ HoodieInstant pendingClustering =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1002");
+ Map<String, String> operationTypes = new HashMap<>();
+ operationTypes.put("1001", WriteOperationType.INSERT_OVERWRITE.name());
+ operationTypes.put("1002", WriteOperationType.CLUSTER.name());
Review Comment:
Two gaps in this fixture, both on branches the fix depends on.
**No INFLIGHT instant.** `filterInflightsAndRequested()`
(`BaseHoodieTimeline.java:151-155`) returns REQUESTED *and* INFLIGHT; both
fixtures here are REQUESTED. The inflight case takes a different branch --
`ClusteringUtils.java:182-186` re-derives the requested instant from the
inflight one, with the comment "inflight replacecommit files don't have
clustering plan. This is because replacecommit inflight can have workload
profile for 'insert_overwrite'." That branch exists because of #2389
([HUDI-1498], "Read clustering plan from requested file for inflight instant"),
and on tv6 it is live, since inflight clustering is
`<ts>.replacecommit.inflight`. Add a third instant in `State.INFLIGHT` with a
CLUSTER operationType and assert the count becomes 2.
**The negative case is a shape production never writes.**
`"INSERT_OVERWRITE"` is not what lands on disk. Every replacecommit goes
through `createRequestedCommitWithReplaceMetadata`, which writes a bare `new
HoodieRequestedReplaceMetadata()` (`ActiveTimelineV1.java:138`,
`ActiveTimelineV2.java:143`) -- `operationType == null`, `clusteringPlan ==
null`, all fields being nullable in `HoodieRequestedReplaceMetadata.avsc`. So
the real-world negative case is a null operationType, and a future tightening
to `metadata.getOperationType().equals(CLUSTER.name())` would NPE on every
pending insert_overwrite, be swallowed by the new `catch`, and leave this test
green. Add an instant with a null operationType and assert the count is
unchanged. `DELETE_PARTITION` (`BaseHoodieTableServiceClient.java:789`) is the
one that genuinely writes a non-null operationType, and the description names
it, so it is worth a case too.
##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/TestHoodieMetrics.java:
##########
@@ -320,13 +324,61 @@ public void testTimerCtxandGauges() throws
InterruptedException {
assertEquals((long)metrics.getRegistry().getGauges().get(metricName).getValue(),
6L);
}
+ @Test
+ void testPendingClusteringInstantMetricsOnTableVersionSix() {
Review Comment:
This test is named for table version six, but nothing in it is table version
six -- so the PR's headline claim has no coverage behind it.
`MockHoodieActiveTimeline extends ActiveTimelineV2`
(`TestHoodieMetrics.java:349`), whose `getTimelineLayoutVersion()` is hardcoded
to `LAYOUT_VERSION_2` (`BaseTimelineV2.java:61-64`), and `INSTANT_GENERATOR` is
`DefaultInstantGenerator extends InstantGeneratorV2`
(`HoodieTestUtils.java:110`, `DefaultInstantGenerator.java:23`). A real tv6
table is `LAYOUT_VERSION_1` (`HoodieTableVersion.java:54`) and gets
`ActiveTimelineV1` + `InstantGeneratorV1` (`TimelineLayout.java:87-93`). The
assertion runs an entirely tv8 stack.
The override of `readRequestedReplaceMetadata` at `:369` also stubs out the
only method in the chain that touches storage or the layout-specific serde, so
`CommitMetadataSerDeV1`, `readNonEmptyInstantContent` and the
empty-requested-file fallback are all bypassed. It has to be stubbed, in fact:
`MockHoodieActiveTimeline` calls the no-arg `ActiveTimelineV2()`, which leaves
`metaClient` null (`ActiveTimelineV2.java:88`), so any real read would NPE --
and that NPE would then be swallowed by the new `catch (Exception)` and turned
back into count 0.
There is a ready-made tv6 harness in this same module.
`TestBaseHoodieTableServiceClient` extends `HoodieCommonTestHarness`, where
`initMetaClient(preTableVersion8)` yields `HoodieTableVersion.SIX`
(`HoodieCommonTestHarness.java:191-197`), and
`createClusteringInstant(instantTime, REPLACE_COMMIT_ACTION)` at
`TestBaseHoodieTableServiceClient.java:429-449` writes a genuine pending
replacecommit clustering instant via `saveToPendingClusterCommit` and
transitions it to inflight.
`isClusteringInstantEligibleForRollback_returnsTrueWhenEligible` at `:299-315`
is the exact pattern to copy:
```java
@ParameterizedTest
@ValueSource(booleans = {true, false})
void pendingClusteringInstantMetrics(boolean preTableVersion8) throws
IOException {
initMetaClient(preTableVersion8);
String clusteringAction = preTableVersion8 ? REPLACE_COMMIT_ACTION :
CLUSTERING_ACTION;
...
}
```
Please rebuild the test on that harness so both layouts run against real
instant files. If you would rather keep the mock, rename this to something
honest such as `testPendingClusteringMetricsSelectByOperationType`, so it does
not claim tv6 coverage it does not have.
##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/TestHoodieMetrics.java:
##########
@@ -320,13 +324,61 @@ public void testTimerCtxandGauges() throws
InterruptedException {
assertEquals((long)metrics.getRegistry().getGauges().get(metricName).getValue(),
6L);
}
+ @Test
+ void testPendingClusteringInstantMetricsOnTableVersionSix() {
+ // Table version six schedules clustering as REPLACE_COMMIT_ACTION, which
insert_overwrite shares.
+ HoodieInstant pendingInsertOverwrite =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1001");
+ HoodieInstant pendingClustering =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1002");
+ Map<String, String> operationTypes = new HashMap<>();
+ operationTypes.put("1001", WriteOperationType.INSERT_OVERWRITE.name());
+ operationTypes.put("1002", WriteOperationType.CLUSTER.name());
+
+ hoodieMetrics.updateTableServiceInstantMetrics(
+ new MockClusteringPlanTimeline(operationTypes, pendingInsertOverwrite,
pendingClustering), INSTANT_GENERATOR);
+
+ // Only the instant carrying a clustering plan counts. insert_overwrite is
the earlier of the two, so
+ // both assertions fail if the action name alone is used to select it.
Review Comment:
nit, feel free to ignore: same wrong field as the production comment. Both
fixtures carry a non-null clustering plan; what separates them is
`operationType` (`ClusteringUtils.java:285`). The non-null plan on the
insert_overwrite instant is actually the valuable part of this fixture -- it
pins that selection must not key on plan presence -- so it is worth saying that
rather than describing it backwards.
```suggestion
// Only the instant whose requested replace metadata carries
operationType CLUSTER counts. Both fixtures
// deliberately carry a plan, so this also pins that selection must not
key on plan presence.
// insert_overwrite is the earlier of the two, so both assertions fail
if the action name alone selects it.
```
Separately, both assertions below dereference `...getGauges().get(name)`
with no null check. If the fix regresses, `updateEarliestPendingInstant` never
registers the gauge (`HoodieMetrics.java:501-505` writes only when an instant
matches), so line 346 fails with an NPE rather than a readable assertion. An
`assertNotNull(gauge, ...)` first would make that failure diagnosable.
##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/TestHoodieMetrics.java:
##########
@@ -320,13 +324,61 @@ public void testTimerCtxandGauges() throws
InterruptedException {
assertEquals((long)metrics.getRegistry().getGauges().get(metricName).getValue(),
6L);
}
+ @Test
+ void testPendingClusteringInstantMetricsOnTableVersionSix() {
+ // Table version six schedules clustering as REPLACE_COMMIT_ACTION, which
insert_overwrite shares.
+ HoodieInstant pendingInsertOverwrite =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1001");
+ HoodieInstant pendingClustering =
+ INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED,
HoodieTimeline.REPLACE_COMMIT_ACTION, "1002");
+ Map<String, String> operationTypes = new HashMap<>();
+ operationTypes.put("1001", WriteOperationType.INSERT_OVERWRITE.name());
+ operationTypes.put("1002", WriteOperationType.CLUSTER.name());
+
+ hoodieMetrics.updateTableServiceInstantMetrics(
+ new MockClusteringPlanTimeline(operationTypes, pendingInsertOverwrite,
pendingClustering), INSTANT_GENERATOR);
+
+ // Only the instant carrying a clustering plan counts. insert_overwrite is
the earlier of the two, so
+ // both assertions fail if the action name alone is used to select it.
+ String countMetric =
hoodieMetrics.getMetricsName(HoodieTimeline.CLUSTERING_ACTION,
HoodieMetrics.PENDING_CLUSTERING_INSTANT_COUNT_STR);
+ assertEquals(1L, (long)
metrics.getRegistry().getGauges().get(countMetric).getValue());
+ String earliestMetric =
hoodieMetrics.getMetricsName(HoodieTimeline.CLUSTERING_ACTION,
HoodieMetrics.EARLIEST_PENDING_CLUSTERING_INSTANT_STR);
+ assertEquals(1002L, (long)
metrics.getRegistry().getGauges().get(earliestMetric).getValue());
+ }
+
private static class MockHoodieActiveTimeline extends ActiveTimelineV2 {
public MockHoodieActiveTimeline(HoodieInstant... instants) {
super();
this.setInstants(Arrays.asList(instants));
}
}
+ /**
+ * Serves a requested replace metadata per instant time so pending
replacecommits can be told apart
+ * by their write operation type without a backing timeline on storage.
+ */
+ private static class MockClusteringPlanTimeline extends
MockHoodieActiveTimeline {
Review Comment:
nit, feel free to ignore: this `HoodieRequestedReplaceMetadata` builder
chain is byte-for-byte identical to
`TestBaseHoodieTableServiceClient.java:434-441` in this same module. If the
test moves onto that harness it disappears entirely; if the mock stays,
consider factoring the metadata literal into a shared test helper rather than
keeping a third copy.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]