This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 5c27559f02f Add subscription stall diagnostics and reduce WAL prefetch
overhead (#18808)
5c27559f02f is described below
commit 5c27559f02f525e75ee07a58d06ff8ee54578527
Author: Caideyipi <[email protected]>
AuthorDate: Mon Oct 12 14:01:18 2026 +0800
Add subscription stall diagnostics and reduce WAL prefetch overhead (#18808)
* Add subscription delivery stall and memory overcommit diagnostics
* Add debug tracing for consensus subscription prefetch rounds
* Avoid redundant WAL work during subscription prefetch
* Make WAL replay diagnostics test locale independent
* Preserve subscription WAL requests across memory backpressure
* Add opt-in subscription WAL prefetch performance comparisons
* Clarify subscription prefetch tracker synchronization
---
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 42 ++-
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 42 ++-
.../consensus/ConsensusLogToTabletConverter.java | 4 +-
.../consensus/ConsensusPrefetchingQueue.java | 314 ++++++++++++-----
.../broker/consensus/ProgressWALIterator.java | 18 +
.../consensus/SubscriptionQueueTimeTracker.java | 119 +++++++
...nsensusSubscriptionPrefetchingQueueMetrics.java | 60 ++++
.../subscription/metric/SubscriptionMetrics.java | 34 ++
.../resource/SubscriptionMemoryManager.java | 45 ++-
.../task/subtask/ConsensusPrefetchSubtask.java | 11 +
.../ConsensusLogToTabletConverterTest.java | 45 +++
...onsensusPrefetchingQueueDataNodeMemoryTest.java | 168 ++++++++-
.../consensus/ConsensusPrefetchingQueueTest.java | 79 ++++-
...nsensusPrefetchingQueueWalBackpressureTest.java | 129 +++++++
.../broker/consensus/ProgressWALIteratorTest.java | 45 ++-
.../SubscriptionWalPrefetchPerformanceTest.java | 380 +++++++++++++++++++++
.../metric/SubscriptionMetricsRestartTest.java | 87 +++++
.../resource/SubscriptionMemoryManagerTest.java | 14 +
.../iotdb/commons/service/metric/enums/Metric.java | 13 +
19 files changed, 1550 insertions(+), 99 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index e6268fbe01e..836ac88d84f 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -21,6 +21,46 @@ package org.apache.iotdb.db.i18n;
public final class DataNodePipeMessages {
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
=
+ "Subscription prefetch round starting for {}: active={},
initialized={}, closed={}, pendingEntries={}, prefetchedEvents={},
inFlightEvents={}, nextReadSearchIndex={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_COMPLETED_FOR_ARG_DURATIONMS_ARG_RESULT_ARG_DELAYMS_ARG_READSEARCHINDEX_ARG_ARG_PENDINGACCEPTED_ARG_WALACCEPTED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_MEMORYBLOCKREASON_ARG_ADMISSIONBLOCKREASON_ARG_0F0A3D57
=
+ "Subscription prefetch round completed for {}: durationMs={},
result={}, delayMs={}, readSearchIndex={} -> {}, pendingAccepted={},
walAccepted={}, pendingEntries={}, prefetchedEvents={}, inFlightEvents={},
memoryBlockReason={}, admissionBlockReason={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ACQUIRED_QUEUE_READ_LOCK_FOR_ARG_AFTER_ARG_MS_425E9A20
=
+ "Subscription prefetch acquired queue read lock for {} after {} ms.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_SCANNING_WAL_FOR_ARG_NEXTREADSEARCHINDEX_ARG_MAXWALENTRIES_ARG_PREFETCHEDEVENTS_ARG_AA16EEA1
=
+ "Subscription prefetch scanning WAL for {}: nextReadSearchIndex={},
maxWalEntries={}, prefetchedEvents={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_PREPARING_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_EE485CC7
=
+ "Subscription prefetch preparing entry for {}: searchIndex={},
writerNodeId={}, localSeq={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_PREPARED_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_TABLETS_ARG_ESTIMATEDBYTES_ARG_DURATIONMS_ARG_E91BA0CD
=
+ "Subscription prefetch prepared entry for {}: searchIndex={},
writerNodeId={}, localSeq={}, tablets={}, estimatedBytes={}, durationMs={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_MEMORY_RESERVATION_REJECTED_FOR_ARG_ENTRYBYTES_ARG_REASON_ARG_QUOTABYTES_ARG_FREEBYTES_ARG_98903D3B
=
+ "Subscription prefetch memory reservation rejected for {}:
entryBytes={}, reason={}, quotaBytes={}, freeBytes={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_TASK_ARG_SCHEDULING_AFTER_ROUND_RESULT_ARG_WAKEUPPENDING_ARG_ENQUEUENOW_ARG_DELAYMS_ARG_602EA4A4
=
+ "Subscription prefetch task {} scheduling after round: result={},
wakeupPending={}, enqueueNow={}, delayMs={}.";
+
+ public static final String
+
LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E
=
+ "Subscription memory overcommit: entryBytes={}, budgetBytes={},
overcommitBytes={}; other queues may be blocked until release.";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD
=
+ "Subscription prefetch round took {} ms for {}.
deliveryIdleTimeMs={}, memoryUsedBytes={}, memoryLimitBytes={}; inspect worker
stacks, WAL I/O and JVM pauses.";
+
public static final String LOG_FAILED_TO_RESOLVE_TRANSFER_EXCEPTION_A4F5397A
=
"Failed to resolve transfer exception.";
@@ -2124,7 +2164,7 @@ public final class DataNodePipeMessages {
"ConsensusPrefetchingQueue {}: periodic stats, lag={}, pendingDelta={},
walDelta={}, "
+ "pendingTotal={}, walTotal={}, walGapSkippedEntries={},
pendingQueueSize={}, "
+ "prefetchingQueueSize={}, inFlightEventsSize={},
realtimeWriterCount={}, "
- + "walHasNext={}, isActive={}, subtaskScheduled={}";
+ + "walNextBuffered={}, isActive={}, subtaskScheduled={}";
public static final String
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAITING_MS_FOR_WAL_GAP_TO_BECOME_7D91C6C5 =
"ConsensusPrefetchingQueue {}: waiting {}ms for WAL gap [{}, {}) to
become visible, "
+ "currentNextExpected={}, currentWalIndex={}, seekGeneration={}";
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 64cd9c7a2d6..5840da12daf 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -21,6 +21,46 @@ package org.apache.iotdb.db.i18n;
public final class DataNodePipeMessages {
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
=
+ "订阅预取轮次开始,队列
{}:active={},initialized={},closed={},pendingEntries={},prefetchedEvents={},inFlightEvents={},nextReadSearchIndex={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_COMPLETED_FOR_ARG_DURATIONMS_ARG_RESULT_ARG_DELAYMS_ARG_READSEARCHINDEX_ARG_ARG_PENDINGACCEPTED_ARG_WALACCEPTED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_MEMORYBLOCKREASON_ARG_ADMISSIONBLOCKREASON_ARG_0F0A3D57
=
+ "订阅预取轮次结束,队列
{}:durationMs={},result={},delayMs={},readSearchIndex={} ->
{},pendingAccepted={},walAccepted={},pendingEntries={},prefetchedEvents={},inFlightEvents={},memoryBlockReason={},admissionBlockReason={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ACQUIRED_QUEUE_READ_LOCK_FOR_ARG_AFTER_ARG_MS_425E9A20
=
+ "订阅预取已获取队列 {} 的读锁,距离轮次开始已过 {} ms。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_SCANNING_WAL_FOR_ARG_NEXTREADSEARCHINDEX_ARG_MAXWALENTRIES_ARG_PREFETCHEDEVENTS_ARG_AA16EEA1
=
+ "订阅预取开始扫描队列 {} 的
WAL:nextReadSearchIndex={},maxWalEntries={},prefetchedEvents={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_PREPARING_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_EE485CC7
=
+ "订阅预取开始解析和转换队列 {} 的
entry:searchIndex={},writerNodeId={},localSeq={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_PREPARED_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_TABLETS_ARG_ESTIMATEDBYTES_ARG_DURATIONMS_ARG_E91BA0CD
=
+ "订阅预取完成队列 {} 的 entry
解析和转换:searchIndex={},writerNodeId={},localSeq={},tablets={},estimatedBytes={},durationMs={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_MEMORY_RESERVATION_REJECTED_FOR_ARG_ENTRYBYTES_ARG_REASON_ARG_QUOTABYTES_ARG_FREEBYTES_ARG_98903D3B
=
+ "订阅预取队列 {}
的内存预留被拒绝:entryBytes={},reason={},quotaBytes={},freeBytes={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_TASK_ARG_SCHEDULING_AFTER_ROUND_RESULT_ARG_WAKEUPPENDING_ARG_ENQUEUENOW_ARG_DELAYMS_ARG_602EA4A4
=
+ "订阅预取任务 {}
的轮次后调度:result={},wakeupPending={},enqueueNow={},delayMs={}。";
+
+ public static final String
+
LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E
=
+
"订阅内存超出预算:entryBytes={},budgetBytes={},overcommitBytes={};其它队列可能阻塞,直到该 entry
释放。";
+
+ public static final String
+
LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD
=
+ "订阅预取轮次耗时 {} ms,队列
{}。deliveryIdleTimeMs={},memoryUsedBytes={},memoryLimitBytes={};请检查工作线程栈、WAL
I/O 和 JVM 停顿。";
+
public static final String LOG_FAILED_TO_RESOLVE_TRANSFER_EXCEPTION_A4F5397A
=
"解析 transfer exception 失败。";
@@ -1970,7 +2010,7 @@ public final class DataNodePipeMessages {
public static final String
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_PERIODIC_STATS_LAG_PENDINGDELTA_WALGAPSKIPPEDENTRIES_9A4E6608
=
"ConsensusPrefetchingQueue
{}:周期统计,lag={},pendingDelta={},walDelta={},pendingTotal={},"
+
"walTotal={},walGapSkippedEntries={},pendingQueueSize={},prefetchingQueueSize={},"
- +
"inFlightEventsSize={},realtimeWriterCount={},walHasNext={},isActive={},"
+ +
"inFlightEventsSize={},realtimeWriterCount={},walNextBuffered={},isActive={},"
+ "subtaskScheduled={}";
public static final String
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAITING_MS_FOR_WAL_GAP_TO_BECOME_7D91C6C5 =
"ConsensusPrefetchingQueue {}:等待 {}ms,使 WAL 缺口 [{}, {})
可见,currentNextExpected={},"
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverter.java
index abd4c5cc381..882860cebbc 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverter.java
@@ -134,7 +134,9 @@ public class ConsensusLogToTabletConverter {
try {
if (req instanceof IoTConsensusRequest) {
// WAL entries read from file are wrapped as IoTConsensusRequest
(ByteBuffer).
- planNode =
WALEntry.deserializeForConsensus(req.serializeToByteBuffer());
+ // Preserve the request's buffer position so memory backpressure can
retry every
+ // fragment. duplicate() shares the bytes without retaining a second
payload copy.
+ planNode =
WALEntry.deserializeForConsensus(req.serializeToByteBuffer().duplicate());
} else if (req instanceof InsertNode) {
// In-memory entries that are not yet flushed to WAL file may
already be PlanNode.
planNode = (PlanNode) req;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index e24748edc0b..dd5d2040009 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -319,6 +319,8 @@ public class ConsensusPrefetchingQueue {
private volatile long lastProgressTimeMs = 0L;
+ private final SubscriptionQueueTimeTracker timeTracker = new
SubscriptionQueueTimeTracker();
+
private volatile String lastConsumerId = "";
private volatile long lastPendingAcceptedEntries = 0L;
@@ -954,6 +956,7 @@ public class ConsensusPrefetchingQueue {
this.subscriptionWALIterator =
createSubscriptionWALIterator(resolvedStart.getStartSearchIndex());
this.prefetchInitialized = true;
+ timeTracker.start();
this.observedSeekGeneration = seekGeneration.get();
discardBatch(this.lingerBatch);
resetBatchWriterProgress();
@@ -1443,6 +1446,7 @@ public class ConsensusPrefetchingQueue {
// Mark as polled before updating inFlightEvents
event.recordLastPolledTimestamp();
+ timeTracker.recordDelivery();
lastPollTimeMs = System.currentTimeMillis();
lastConsumerId = consumerId;
inFlightEvents.put(new InFlightEventKey(consumerId,
event.getCommitContext()), event);
@@ -1528,7 +1532,69 @@ public class ConsensusPrefetchingQueue {
private static final long PREFETCH_STATS_LOG_INTERVAL_MS = 5_000L;
+ private static final long SLOW_PREFETCH_ROUND_THRESHOLD_MS = 10_000L;
+
+ private String getPrefetchDebugId() {
+ return getPrefetchingQueueId() + "/" + consensusGroupId;
+ }
+
public PrefetchRoundResult drivePrefetchOnce() {
+ timeTracker.beginPrefetch();
+ final boolean debugEnabled = LOGGER.isDebugEnabled();
+ final long startReadSearchIndex = debugEnabled ?
nextExpectedSearchIndex.get() : 0L;
+ final long startPendingAccepted = debugEnabled ?
pendingPathAcceptedEntries.get() : 0L;
+ final long startWalAccepted = debugEnabled ? walPathAcceptedEntries.get()
: 0L;
+ PrefetchRoundResult result = null;
+ try {
+ if (debugEnabled) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD,
+ getPrefetchDebugId(),
+ isActive,
+ prefetchInitialized,
+ closeRequested || isClosed,
+ pendingEntries.size(),
+ prefetchingQueue.size(),
+ inFlightEvents.size(),
+ startReadSearchIndex);
+ }
+ result = drivePrefetchOnceInternal();
+ return result;
+ } finally {
+ final long durationMs = timeTracker.endPrefetch();
+ if (debugEnabled) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_ROUND_COMPLETED_FOR_ARG_DURATIONMS_ARG_RESULT_ARG_DELAYMS_ARG_READSEARCHINDEX_ARG_ARG_PENDINGACCEPTED_ARG_WALACCEPTED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_MEMORYBLOCKREASON_ARG_ADMISSIONBLOCKREASON_ARG_0F0A3D57,
+ getPrefetchDebugId(),
+ durationMs,
+ Objects.nonNull(result) ? result.getType() : null,
+ Objects.nonNull(result) ? result.getDelayMs() : null,
+ startReadSearchIndex,
+ nextExpectedSearchIndex.get(),
+ pendingPathAcceptedEntries.get() - startPendingAccepted,
+ walPathAcceptedEntries.get() - startWalAccepted,
+ pendingEntries.size(),
+ prefetchingQueue.size(),
+ inFlightEvents.size(),
+ memoryBlockReason,
+ realtimeAdmissionBlockReason);
+ }
+ if (durationMs >= SLOW_PREFETCH_ROUND_THRESHOLD_MS) {
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD,
+ durationMs,
+ this,
+ getDeliveryIdleTimeMs(),
+ subscriptionMemoryManager.getUsedMemorySizeInBytes(),
+ subscriptionMemoryManager.getTotalMemorySizeInBytes());
+ }
+ }
+ }
+
+ private PrefetchRoundResult drivePrefetchOnceInternal() {
if (applyPendingSeekRequestIfNecessary()) {
return closeRequested ? PrefetchRoundResult.dormant() :
PrefetchRoundResult.rescheduleNow();
}
@@ -1539,6 +1605,13 @@ public class ConsensusPrefetchingQueue {
return PrefetchRoundResult.dormant();
}
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_ACQUIRED_QUEUE_READ_LOCK_FOR_ARG_AFTER_ARG_MS_425E9A20,
+ getPrefetchDebugId(),
+ getPrefetchDurationMs());
+ }
logPeriodicStatsIfNecessary();
final long currentSeekGeneration = seekGeneration.get();
@@ -1714,7 +1787,7 @@ public class ConsensusPrefetchingQueue {
prefetchingQueue.size(),
inFlightEvents.size(),
realtimeEntriesByWriter.size(),
- hasReadableWalEntries(),
+ Objects.nonNull(subscriptionWALIterator) &&
subscriptionWALIterator.hasBufferedNext(),
isActive,
Objects.nonNull(prefetchSubtask) &&
prefetchSubtask.isScheduledOrRunning());
lastStatsLogTimeMs = nowMs;
@@ -1819,24 +1892,9 @@ public class ConsensusPrefetchingQueue {
return hasLocalSearchIndex(request) && request.getSearchIndex() <
nextExpectedSearchIndex.get();
}
- private boolean advanceLocalCursorIfPresent(final IndexedConsensusRequest
request) {
+ private void advanceLocalCursorIfPresent(final IndexedConsensusRequest
request) {
if (hasLocalSearchIndex(request)) {
nextExpectedSearchIndex.set(request.getSearchIndex() + 1);
- return true;
- }
- return false;
- }
-
- private void advanceLocalCursorFromPendingIfPresent(
- final IndexedConsensusRequest request, final long
expectedSeekGeneration) {
- if (advanceLocalCursorIfPresent(request)) {
- // Pending delivery advances independently of the WAL reader. Raise its
local lower bound in
- // place so stale local requests are filtered without rebuilding and
rescanning retained WAL.
- final ProgressWALIterator iterator = subscriptionWALIterator;
- if (Objects.nonNull(iterator) && seekGeneration.get() ==
expectedSeekGeneration) {
- iterator.advanceTo(
- nextExpectedSearchIndex.get(),
this::isWriterProgressCoveredForWalFastForward);
- }
}
}
@@ -1895,76 +1953,90 @@ public class ConsensusPrefetchingQueue {
int processedCount = 0;
int skippedCount = 0;
+ final long startSearchIndex = nextExpectedSearchIndex.get();
- for (int index = 0; index < batch.size(); index++) {
- final IndexedConsensusRequest request = batch.get(index);
- final long searchIndex = request.getSearchIndex();
+ try {
+ for (int index = 0; index < batch.size(); index++) {
+ final IndexedConsensusRequest request = batch.get(index);
+ final long searchIndex = request.getSearchIndex();
- // Only local-indexed requests participate in the internal WAL read
cursor.
- final long expected = nextExpectedSearchIndex.get();
- if (hasLocalSearchIndex(request) && searchIndex > expected) {
- LOGGER.debug(
- DataNodePipeMessages
-
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_GAP_DETECTED_EXPECTED_GOT_FILLING_70DD08B3,
- this,
- expected,
- searchIndex,
- searchIndex - expected);
- final MaterializationResult gapFillResult =
- fillGapFromWAL(
- expected,
- searchIndex,
- lingerBatch,
- expectedSeekGeneration,
- maxWalEntries,
- maxTablets,
- maxBatchBytes);
- if (gapFillResult != MaterializationResult.SUCCESS) {
- return gapFillResult;
+ // Only local-indexed requests participate in the internal WAL read
cursor.
+ final long expected = nextExpectedSearchIndex.get();
+ if (hasLocalSearchIndex(request) && searchIndex > expected) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_GAP_DETECTED_EXPECTED_GOT_FILLING_70DD08B3,
+ this,
+ expected,
+ searchIndex,
+ searchIndex - expected);
+ final MaterializationResult gapFillResult =
+ fillGapFromWAL(
+ expected,
+ searchIndex,
+ lingerBatch,
+ expectedSeekGeneration,
+ maxWalEntries,
+ maxTablets,
+ maxBatchBytes);
+ if (gapFillResult != MaterializationResult.SUCCESS) {
+ return gapFillResult;
+ }
}
- }
- if (isBeforeLocalCursor(request)) {
- skippedCount++;
- continue;
- }
+ if (isBeforeLocalCursor(request)) {
+ skippedCount++;
+ continue;
+ }
- if (shouldSkipForRecoveryProgress(request)) {
- skippedCount++;
- advanceLocalCursorFromPendingIfPresent(request,
expectedSeekGeneration);
- continue;
- }
- if (shouldSkipForMaterializedProgress(request)) {
- skippedCount++;
- advanceLocalCursorFromPendingIfPresent(request,
expectedSeekGeneration);
- continue;
- }
+ if (shouldSkipForRecoveryProgress(request)) {
+ skippedCount++;
+ advanceLocalCursorIfPresent(request);
+ continue;
+ }
+ if (shouldSkipForMaterializedProgress(request)) {
+ skippedCount++;
+ advanceLocalCursorIfPresent(request);
+ continue;
+ }
- final MaterializationResult appendResult =
- appendRealtimeRequest(
- request, lingerBatch, expectedSeekGeneration, maxTablets,
maxBatchBytes, true);
- if (appendResult != MaterializationResult.SUCCESS) {
- return appendResult;
- }
- markMaterializedProgress(request);
- processedCount++;
- advanceLocalCursorFromPendingIfPresent(request, expectedSeekGeneration);
- if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
- break;
+ final MaterializationResult appendResult =
+ appendRealtimeRequest(
+ request, lingerBatch, expectedSeekGeneration, maxTablets,
maxBatchBytes, true);
+ if (appendResult != MaterializationResult.SUCCESS) {
+ return appendResult;
+ }
+ markMaterializedProgress(request);
+ processedCount++;
+ advanceLocalCursorIfPresent(request);
+ if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
+ break;
+ }
}
- }
- LOGGER.debug(
- DataNodePipeMessages
-
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ACCUMULATE_COMPLETE_BATCHSIZE_FA3F3B41,
- this,
- batch.size(),
- processedCount,
- skippedCount,
- lingerBatch.tablets.size(),
- nextExpectedSearchIndex.get());
+ LOGGER.debug(
+ DataNodePipeMessages
+
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_ACCUMULATE_COMPLETE_BATCHSIZE_FA3F3B41,
+ this,
+ batch.size(),
+ processedCount,
+ skippedCount,
+ lingerBatch.tablets.size(),
+ nextExpectedSearchIndex.get());
- return MaterializationResult.SUCCESS;
+ return MaterializationResult.SUCCESS;
+ } finally {
+ // Advance once for the consumed prefix, including when a gap or memory
block stops the
+ // batch. Per-entry fast-forward can repeatedly inspect the same
uncovered WAL file footer.
+ // Gap recovery creates its own iterator at the current cursor before
reading WAL.
+ final ProgressWALIterator iterator = subscriptionWALIterator;
+ if (nextExpectedSearchIndex.get() > startSearchIndex
+ && Objects.nonNull(iterator)
+ && seekGeneration.get() == expectedSeekGeneration) {
+ iterator.advanceTo(
+ nextExpectedSearchIndex.get(),
this::isWriterProgressCoveredForWalFastForward);
+ }
+ }
}
/**
@@ -2080,6 +2152,15 @@ public class ConsensusPrefetchingQueue {
return MaterializationResult.SUCCESS;
}
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_SCANNING_WAL_FOR_ARG_NEXTREADSEARCHINDEX_ARG_MAXWALENTRIES_ARG_PREFETCHEDEVENTS_ARG_AA16EEA1,
+ getPrefetchDebugId(),
+ nextExpectedSearchIndex.get(),
+ maxWalEntries,
+ prefetchingQueue.size());
+ }
ensureSubscriptionWalReadable();
reportUnreadableWalReplayIfNecessary();
@@ -2109,7 +2190,10 @@ public class ConsensusPrefetchingQueue {
walEntry, batchState, expectedSeekGeneration, maxTablets,
maxBatchBytes, false);
if (appendResult != MaterializationResult.SUCCESS) {
if (appendResult == MaterializationResult.MEMORY_BLOCKED) {
- resetSubscriptionWALPosition(nextExpectedSearchIndex.get());
+ // next() has removed this request from the iterator, but replay
progress has not
+ // advanced. Keep it buffered so the next round retries it without
reopening the WAL
+ // and rescanning the prefix before the current reader position.
+ subscriptionWALIterator.retry(walEntry);
}
return appendResult;
}
@@ -2304,6 +2388,17 @@ public class ConsensusPrefetchingQueue {
}
private PreparedEntry prepareEntry(final IndexedConsensusRequest
indexedRequest) {
+ final boolean debugEnabled = LOGGER.isDebugEnabled();
+ final long prepareStartNs = debugEnabled ? System.nanoTime() : 0L;
+ if (debugEnabled) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_PREPARING_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_EE485CC7,
+ getPrefetchDebugId(),
+ indexedRequest.getSearchIndex(),
+ indexedRequest.getNodeId(),
+ indexedRequest.getProgressLocalSeq());
+ }
final InsertNode insertNode =
ConsensusLogToTabletConverter.deserializeToInsertNode(indexedRequest);
if (Objects.isNull(insertNode)) {
@@ -2330,6 +2425,18 @@ public class ConsensusPrefetchingQueue {
}
final List<Tablet> tablets = converter.convert(insertNode);
final long estimatedBytes = estimateTabletsBytes(tablets);
+ if (debugEnabled) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_PREPARED_ENTRY_FOR_ARG_SEARCHINDEX_ARG_WRITERNODEID_ARG_LOCALSEQ_ARG_TABLETS_ARG_ESTIMATEDBYTES_ARG_DURATIONMS_ARG_E91BA0CD,
+ getPrefetchDebugId(),
+ searchIndex,
+ writerNodeId,
+ localSeq,
+ tablets.size(),
+ estimatedBytes,
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - prepareStartNs));
+ }
if (!tryReserveTabletMemory(estimatedBytes)) {
return null;
}
@@ -2472,6 +2579,16 @@ public class ConsensusPrefetchingQueue {
final SubscriptionMemoryManager.AllocationResult allocationResult =
subscriptionMemoryHandle.tryAllocate(bytes);
if (!allocationResult.isAccepted()) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_MEMORY_RESERVATION_REJECTED_FOR_ARG_ENTRYBYTES_ARG_REASON_ARG_QUOTABYTES_ARG_FREEBYTES_ARG_98903D3B,
+ getPrefetchDebugId(),
+ bytes,
+ allocationResult.getRejectionReason(),
+ allocationResult.getQuotaInBytes(),
+ allocationResult.getFreeMemoryInBytes());
+ }
memoryBlockedEntryBytes = bytes;
final SubscriptionQueueRejectionReason rejectionReason =
toQueueRejectionReason(allocationResult.getRejectionReason());
@@ -4378,6 +4495,30 @@ public class ConsensusPrefetchingQueue {
return lastProgressTimeMs;
}
+ public long getLastDeliveryIntervalMs() {
+ return timeTracker.getLastDeliveryIntervalMs();
+ }
+
+ public long getMaxDeliveryIntervalMs() {
+ return timeTracker.getMaxDeliveryIntervalMs();
+ }
+
+ public long getDeliveryIdleTimeMs() {
+ return timeTracker.getDeliveryIdleTimeMs();
+ }
+
+ public long getPrefetchDurationMs() {
+ return timeTracker.getPrefetchDurationMs();
+ }
+
+ public long getMaxPrefetchDurationMs() {
+ return timeTracker.getMaxPrefetchDurationMs();
+ }
+
+ public long getPrefetchIdleTimeMs() {
+ return timeTracker.getPrefetchIdleTimeMs();
+ }
+
/** Returns 0=uninitialized, 1=inactive, 2=caught up, 3=catching up,
4=stalled. */
public long getProgressStatus() {
switch (getProgressStatusName()) {
@@ -4743,6 +4884,21 @@ public class ConsensusPrefetchingQueue {
result.put("retainedRequestBytes",
String.valueOf(getRetainedRequestBytes()));
result.put("retainedTabletBytes",
String.valueOf(retainedTabletBytes.get()));
result.put("memoryBlockedEntryBytes",
String.valueOf(memoryBlockedEntryBytes));
+ result.put(
+ "dataNodeSubscriptionMemoryUsedInBytes",
+ String.valueOf(subscriptionMemoryManager.getUsedMemorySizeInBytes()));
+ result.put(
+ "subscriptionMemoryOvercommitBytes",
+ String.valueOf(subscriptionMemoryManager.getOvercommitSizeInBytes()));
+ result.put(
+ "subscriptionMemoryOversizedEntryCount",
+ String.valueOf(subscriptionMemoryManager.getOversizedEntryCount()));
+ result.put("lastDeliveryIntervalMs",
String.valueOf(getLastDeliveryIntervalMs()));
+ result.put("maxDeliveryIntervalMs",
String.valueOf(getMaxDeliveryIntervalMs()));
+ result.put("deliveryIdleTimeMs", String.valueOf(getDeliveryIdleTimeMs()));
+ result.put("prefetchDurationMs", String.valueOf(getPrefetchDurationMs()));
+ result.put("maxPrefetchDurationMs",
String.valueOf(getMaxPrefetchDurationMs()));
+ result.put("prefetchIdleTimeMs", String.valueOf(getPrefetchIdleTimeMs()));
result.put("realtimeAdmissionBlocked",
String.valueOf(realtimeAdmissionBlocked.get()));
result.put(
"subscriptionMemoryLimitInBytes",
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
index cbf0649f224..3636829274f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
@@ -49,6 +49,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.Set;
/**
@@ -205,6 +206,23 @@ public class ProgressWALIterator implements Closeable,
Iterator<IndexedConsensus
}
}
+ /**
+ * Returns whether a usable request is already buffered, without reading or
advancing WAL. A false
+ * result does not imply that the iterator is exhausted.
+ */
+ boolean hasBufferedNext() {
+ return nextReady != null && !shouldSkip(nextReady);
+ }
+
+ /**
+ * Keeps a request returned by {@link #next()} available for a retry without
reopening the WAL.
+ * The caller uses this when materialization is temporarily blocked and
replay progress has not
+ * advanced yet.
+ */
+ void retry(final IndexedConsensusRequest request) {
+ nextReady = Objects.requireNonNull(request);
+ }
+
/**
* Advances the local search-index lower bound without rebuilding the
iterator. Whole WAL files
* are skipped only when every writer progress tuple in them is already
covered by queue state.
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
new file mode 100644
index 00000000000..0a8187e4e2e
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
@@ -0,0 +1,119 @@
+/*
+ * 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.iotdb.db.subscription.broker.consensus;
+
+import org.apache.iotdb.commons.utils.TestOnly;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongSupplier;
+
+/** Monotonic timing that remains readable while a queue's prefetch worker is
blocked. */
+class SubscriptionQueueTimeTracker {
+
+ private final LongSupplier nanoTime;
+
+ private volatile boolean started;
+ private boolean delivered;
+ private volatile long lastDeliveryTimeNs;
+ private volatile long lastDeliveryIntervalMs;
+ private volatile long maxDeliveryIntervalMs;
+ private volatile long lastPrefetchCompletionTimeNs;
+ private volatile long prefetchStartTimeNs;
+ private volatile boolean prefetchRunning;
+ private volatile long maxPrefetchDurationMs;
+
+ SubscriptionQueueTimeTracker() {
+ this(System::nanoTime);
+ }
+
+ @TestOnly
+ SubscriptionQueueTimeTracker(final LongSupplier nanoTime) {
+ this.nanoTime = nanoTime;
+ }
+
+ synchronized void start() {
+ if (!started) {
+ final long nowNs = nanoTime.getAsLong();
+ lastDeliveryTimeNs = nowNs;
+ lastPrefetchCompletionTimeNs = nowNs;
+ started = true;
+ }
+ }
+
+ // Multiple consumers may poll the queue concurrently under its read lock,
so delivery interval
+ // updates must remain atomic even though prefetch rounds are serialized.
+ synchronized void recordDelivery() {
+ start();
+ final long nowNs = nanoTime.getAsLong();
+ if (delivered) {
+ lastDeliveryIntervalMs = elapsedMs(nowNs, lastDeliveryTimeNs);
+ maxDeliveryIntervalMs = Math.max(maxDeliveryIntervalMs,
lastDeliveryIntervalMs);
+ }
+ lastDeliveryTimeNs = nowNs;
+ delivered = true;
+ }
+
+ // The prefetch subtask serializes rounds, so this path does not need
synchronization beyond the
+ // one-time initialization performed by start().
+ void beginPrefetch() {
+ start();
+ prefetchStartTimeNs = nanoTime.getAsLong();
+ prefetchRunning = true;
+ }
+
+ long endPrefetch() {
+ final long nowNs = nanoTime.getAsLong();
+ final long durationMs = elapsedMs(nowNs, prefetchStartTimeNs);
+ maxPrefetchDurationMs = Math.max(maxPrefetchDurationMs, durationMs);
+ lastPrefetchCompletionTimeNs = nowNs;
+ prefetchRunning = false;
+ return durationMs;
+ }
+
+ long getLastDeliveryIntervalMs() {
+ return lastDeliveryIntervalMs;
+ }
+
+ long getMaxDeliveryIntervalMs() {
+ return maxDeliveryIntervalMs;
+ }
+
+ long getDeliveryIdleTimeMs() {
+ return started ? elapsedMs(nanoTime.getAsLong(), lastDeliveryTimeNs) : 0L;
+ }
+
+ /** Elapsed time of an ongoing round, including waiting to acquire the queue
lock. */
+ long getPrefetchDurationMs() {
+ return prefetchRunning ? elapsedMs(nanoTime.getAsLong(),
prefetchStartTimeNs) : 0L;
+ }
+
+ long getMaxPrefetchDurationMs() {
+ return Math.max(maxPrefetchDurationMs, getPrefetchDurationMs());
+ }
+
+ /** Time since the last completed round, or activation before the first
completed round. */
+ long getPrefetchIdleTimeMs() {
+ return started ? elapsedMs(nanoTime.getAsLong(),
lastPrefetchCompletionTimeNs) : 0L;
+ }
+
+ private static long elapsedMs(final long nowNs, final long thenNs) {
+ return Math.max(0L, TimeUnit.NANOSECONDS.toMillis(nowNs - thenNs));
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
index 359da8f9239..f0c3cfed01d 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
@@ -206,6 +206,42 @@ public class ConsensusSubscriptionPrefetchingQueueMetrics
implements IMetricSet
queue,
ConsensusPrefetchingQueue::getLastProgressTimeMs,
key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getLastDeliveryIntervalMs,
+ key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getMaxDeliveryIntervalMs,
+ key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getDeliveryIdleTimeMs,
+ key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getPrefetchDurationMs,
+ key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getMaxPrefetchDurationMs,
+ key.getTags());
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS.toString(),
+ MetricLevel.IMPORTANT,
+ queue,
+ ConsensusPrefetchingQueue::getPrefetchIdleTimeMs,
+ key.getTags());
metricService.createAutoGauge(
Metric.SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS.toString(),
MetricLevel.IMPORTANT,
@@ -373,6 +409,30 @@ public class ConsensusSubscriptionPrefetchingQueueMetrics
implements IMetricSet
MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_CONSENSUS_LAST_PROGRESS_TIME.toString(),
key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS.toString(),
+ key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS.toString(),
+ key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS.toString(),
+ key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS.toString(),
+ key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS.toString(),
+ key.getTags());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
+ Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS.toString(),
+ key.getTags());
metricService.remove(
MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS.toString(),
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
index 29de59ddf32..8e8657bb3d5 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
@@ -19,8 +19,13 @@
package org.apache.iotdb.db.subscription.metric;
+import org.apache.iotdb.commons.service.metric.enums.Metric;
+import
org.apache.iotdb.db.subscription.resource.SubscriptionDataNodeResourceManager;
+import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
import org.apache.iotdb.metrics.AbstractMetricService;
import org.apache.iotdb.metrics.metricsets.IMetricSet;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.MetricType;
public class SubscriptionMetrics implements IMetricSet {
@@ -30,12 +35,41 @@ public class SubscriptionMetrics implements IMetricSet {
public void bindTo(final AbstractMetricService metricService) {
SubscriptionPrefetchingQueueMetrics.getInstance().bindTo(metricService);
ConsensusSubscriptionPrefetchingQueueMetrics.getInstance().bindTo(metricService);
+ // Read the shared allocator directly; queue snapshots taken at different
times cannot be
+ // summed to establish whether a DataNode exceeded its subscription memory
budget.
+ final SubscriptionMemoryManager memoryManager =
SubscriptionDataNodeResourceManager.memory();
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_MEMORY_USED_BYTES.toString(),
+ MetricLevel.IMPORTANT,
+ memoryManager,
+ SubscriptionMemoryManager::getUsedMemorySizeInBytes);
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES.toString(),
+ MetricLevel.IMPORTANT,
+ memoryManager,
+ SubscriptionMemoryManager::getTotalMemorySizeInBytes);
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES.toString(),
+ MetricLevel.IMPORTANT,
+ memoryManager,
+ SubscriptionMemoryManager::getOvercommitSizeInBytes);
+ metricService.createAutoGauge(
+ Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT.toString(),
+ MetricLevel.IMPORTANT,
+ memoryManager,
+ SubscriptionMemoryManager::getOversizedEntryCount);
}
@Override
public void unbindFrom(final AbstractMetricService metricService) {
SubscriptionPrefetchingQueueMetrics.getInstance().unbindFrom(metricService);
ConsensusSubscriptionPrefetchingQueueMetrics.getInstance().unbindFrom(metricService);
+ metricService.remove(MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_MEMORY_USED_BYTES.toString());
+ metricService.remove(MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES.toString());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES.toString());
+ metricService.remove(
+ MetricType.AUTO_GAUGE,
Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT.toString());
}
//////////////////////////// singleton ////////////////////////////
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
index d250cd0ba16..f13ef2b1329 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
@@ -24,9 +24,14 @@ import org.apache.iotdb.commons.memory.IMemoryBlock;
import org.apache.iotdb.commons.memory.MemoryBlockType;
import org.apache.iotdb.commons.utils.TestOnly;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
/**
* DataNode-wide memory manager for materialized subscription data.
@@ -39,8 +44,12 @@ import java.util.Map;
*/
public class SubscriptionMemoryManager {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SubscriptionMemoryManager.class);
+
private static final String MEMORY_BLOCK_NAME = "Subscription";
+ private static final long OVERCOMMIT_WARNING_INTERVAL_NS =
TimeUnit.SECONDS.toNanos(30);
+
private final IMemoryBlock memoryBlock;
private static final int PROTECTED_SHARE_DIVISOR = 2;
private final Map<Long, MemoryHandle> handles = new HashMap<>();
@@ -49,6 +58,10 @@ public class SubscriptionMemoryManager {
// Kept for callers of the original manager-level API. It is deliberately
not a fair-share owner.
private long legacyUsedMemoryInBytes;
+ private volatile long oversizedEntryCount = 0L;
+
+ private long lastOvercommitWarningTimeNs;
+
SubscriptionMemoryManager() {
memoryBlock =
IoTDBDescriptor.getInstance()
@@ -71,7 +84,17 @@ public class SubscriptionMemoryManager {
return handle;
}
- /** Reserves memory through the compatibility owner. New queues should use a
handle. */
+ /**
+ * Reserves memory through the compatibility owner. Consensus queues use a
handle, which rejects
+ * oversized entries instead of overcommitting the node budget.
+ *
+ * <p>A single entry larger than the whole budget is allowed only while the
budget is otherwise
+ * empty. This avoids permanently blocking progress while keeping the
overrun bounded by one
+ * entry.
+ *
+ * <p>This is a soft limit for such entries. Other queues can be blocked
until the oversized entry
+ * is released; the exception is exposed through metrics and a rate-limited
warning.
+ */
public synchronized boolean tryAllocate(final long sizeInBytes) {
return tryAllocateLegacy(sizeInBytes).isAccepted();
}
@@ -93,6 +116,18 @@ public class SubscriptionMemoryManager {
if (total > 0L && used == 0L && sizeInBytes > total) {
memoryBlock.forceAllocateWithoutLimitation(sizeInBytes);
legacyUsedMemoryInBytes += sizeInBytes;
+ oversizedEntryCount++;
+ final long nowNs = System.nanoTime();
+ if (oversizedEntryCount == 1L
+ || nowNs - lastOvercommitWarningTimeNs >=
OVERCOMMIT_WARNING_INTERVAL_NS) {
+ lastOvercommitWarningTimeNs = nowNs;
+ LOGGER.warn(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E,
+ sizeInBytes,
+ memoryBlock.getTotalMemorySizeInBytes(),
+ getOvercommitSizeInBytes());
+ }
return AllocationResult.accepted(total, total - sizeInBytes);
}
return AllocationResult.rejected(
@@ -126,6 +161,14 @@ public class SubscriptionMemoryManager {
return memoryBlock.getFreeMemoryInBytes();
}
+ public synchronized long getOvercommitSizeInBytes() {
+ return Math.max(0L, getUsedMemorySizeInBytes() -
getTotalMemorySizeInBytes());
+ }
+
+ public long getOversizedEntryCount() {
+ return oversizedEntryCount;
+ }
+
private synchronized AllocationResult tryAllocate(
final MemoryHandle handle, final long sizeInBytes) {
final AllocationResult decision = inspectAllocationUnderLock(handle,
sizeInBytes);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
index 703289b736b..5f91fecd3d1 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/task/subtask/ConsensusPrefetchSubtask.java
@@ -124,6 +124,7 @@ public class ConsensusPrefetchSubtask {
boolean shouldEnqueue = false;
Long delayedWakeMs = null;
long delayedToken = 0L;
+ boolean wakeupRequested;
synchronized (monitor) {
running = false;
if (closed) {
@@ -132,6 +133,7 @@ public class ConsensusPrefetchSubtask {
return;
}
+ wakeupRequested = wakeupPending;
if (wakeupPending) {
wakeupPending = false;
shouldEnqueue = true;
@@ -158,6 +160,15 @@ public class ConsensusPrefetchSubtask {
monitor.notifyAll();
}
+ LOGGER.debug(
+ DataNodePipeMessages
+
.LOG_SUBSCRIPTION_PREFETCH_TASK_ARG_SCHEDULING_AFTER_ROUND_RESULT_ARG_WAKEUPPENDING_ARG_ENQUEUENOW_ARG_DELAYMS_ARG_602EA4A4,
+ taskId,
+ result.getType(),
+ wakeupRequested,
+ shouldEnqueue,
+ delayedWakeMs);
+
final ConsensusSubscriptionPrefetchExecutor currentExecutor = executor;
if (currentExecutor == null) {
return;
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverterTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverterTest.java
index b06ccf453fe..c4af27f764c 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverterTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusLogToTabletConverterTest.java
@@ -24,7 +24,11 @@ import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.pipe.datastructure.pattern.IoTDBTreePattern;
import org.apache.iotdb.commons.pipe.datastructure.pattern.TablePattern;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.commons.request.IConsensusRequest;
import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
+import org.apache.iotdb.consensus.common.request.IoTConsensusRequest;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsOfOneDeviceNode;
@@ -32,6 +36,8 @@ import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTablet
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode;
import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALByteBufferForTest;
import org.apache.iotdb.db.subscription.columnfilter.ColumnFilterMatcher;
import org.apache.tsfile.enums.ColumnCategory;
@@ -44,6 +50,7 @@ import org.apache.tsfile.write.schema.MeasurementSchema;
import org.junit.Assert;
import org.junit.Test;
+import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -55,6 +62,44 @@ public class ConsensusLogToTabletConverterTest {
private static final String DATABASE_NAME = "db";
+ @Test
+ public void testRepeatedWalDeserializationPreservesAllFragments() {
+ final List<IConsensusRequest> fragments = new ArrayList<>();
+ for (int row = 1; row <= 2; row++) {
+ final RelationalInsertRowNode node =
StatementTestUtils.genInsertRowNode(row);
+ node.setSearchIndex(4L);
+ node.setLastFragment(row == 2);
+ final WALInfoEntry walEntry = new WALInfoEntry(1L, node);
+ final ByteBuffer buffer = ByteBuffer.allocate(walEntry.serializedSize());
+ walEntry.serialize(new WALByteBufferForTest(buffer));
+ buffer.flip();
+ fragments.add(new IoTConsensusRequest(buffer));
+ }
+ final IndexedConsensusRequest request =
+ new IndexedConsensusRequest(4L, 4L,
fragments).setPhysicalTime(1000L).setNodeId(7);
+ final ConsensusLogToTabletConverter converter = createConverter("id1",
"m1");
+
+ for (int attempt = 0; attempt < 3; attempt++) {
+ final InsertNode node =
ConsensusLogToTabletConverter.deserializeToInsertNode(request);
+ Assert.assertNotNull(node);
+ Assert.assertEquals(4L, node.getSearchIndex());
+ Assert.assertEquals(4L, node.getSyncIndex());
+ Assert.assertEquals(1000L, node.getPhysicalTime());
+ Assert.assertEquals(7, node.getNodeId());
+ final List<Tablet> tablets = converter.convert(node);
+ Assert.assertEquals(2, tablets.size());
+ for (int row = 1; row <= 2; row++) {
+ final Tablet tablet = tablets.get(row - 1);
+ Assert.assertEquals(1, tablet.getRowSize());
+ Assert.assertEquals("id:" + row, toUtf8(((Binary[])
tablet.getValues()[0])[0]));
+ Assert.assertEquals((double) row, ((double[])
tablet.getValues()[1])[0], 0.0);
+ final ByteBuffer buffer = fragments.get(row -
1).serializeToByteBuffer();
+ Assert.assertEquals(0, buffer.position());
+ Assert.assertEquals(buffer.capacity(), buffer.limit());
+ }
+ }
+ }
+
@Test
public void testDataModelIsDerivedFromImmutableConversionPattern() {
final ConsensusLogToTabletConverter tableConverter =
createConverter("id1");
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
index c7ebf06ab05..16f675a4da2 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
@@ -36,6 +36,7 @@ import
org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
import org.apache.iotdb.rpc.subscription.config.TopicConstant;
import org.apache.iotdb.rpc.subscription.payload.poll.ErrorPayload;
import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
+import
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
import org.apache.tsfile.enums.ColumnCategory;
import org.apache.tsfile.enums.TSDataType;
@@ -48,12 +49,19 @@ import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
+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;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -226,6 +234,12 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
assertEquals(1L, largeQueue.getOversizedEntryRejectionCount());
assertEquals(0L, largeQueue.getRealtimeAdmissionRejectionCount());
assertEquals(smallBytes, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(0L, memoryManager.getOversizedEntryCount());
+ assertEquals("0",
largeQueue.coreReportMessage().get("subscriptionMemoryUsedInBytes"));
+ assertEquals(
+ String.valueOf(smallBytes),
+
largeQueue.coreReportMessage().get("dataNodeSubscriptionMemoryUsedInBytes"));
final SubscriptionEvent error = largeQueue.poll("largeConsumer");
assertNotNull(error);
final ErrorPayload payload = (ErrorPayload)
error.getCurrentResponse().getPayload();
@@ -306,9 +320,10 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
assertEquals(oneTabletBytes, queueA.getRetainedTabletBytes());
assertEquals(0L, queueB.getRetainedTabletBytes());
assertEquals(oneTabletBytes, memoryManager.getUsedMemorySizeInBytes());
- assertTrue(
- queueA.getRetainedTabletBytes() + queueB.getRetainedTabletBytes()
- <= memoryManager.getTotalMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(0L, memoryManager.getOversizedEntryCount());
+ assertEquals(
+ oneTabletBytes, queueA.getRetainedTabletBytes() +
queueB.getRetainedTabletBytes());
readerB.currentSearchIndex = 1L;
assertTrue(pendingEntries(queueB).offer(createRequest(1L)));
@@ -323,6 +338,7 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
assertTrue(queueA.ack("consumerA", eventA.getCommitContext()));
assertEquals(0L, queueA.getRetainedTabletBytes());
assertEquals(oneTabletBytes, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
queueB.drivePrefetchOnce();
assertEquals("false",
queueB.coreReportMessage().get("realtimeAdmissionBlocked"));
@@ -331,6 +347,7 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
assertNotNull(eventB);
assertTrue(queueB.ack("consumerB", eventB.getCommitContext()));
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
queueB.drivePrefetchOnce();
assertEquals("false",
queueB.coreReportMessage().get("realtimeAdmissionBlocked"));
} finally {
@@ -347,6 +364,151 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
}
}
+ @Test
+ public void testWatermarkAndAckDoNotHideDataDeliveryGap() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final int originalBatchMaxDelay =
+
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxDelayInMs();
+ final File systemDir = temporaryFolder.newFolder("delivery-gap");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(0);
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ queue =
+ newQueue(
+ "consumerGroup",
+ new DataRegionId(1),
+ reader,
+ newConverter(new AtomicInteger()),
+ newCommitManager(systemDir),
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+ final AtomicLong clockNs = new AtomicLong(-1_000_000L);
+ setTimeTracker(queue, new SubscriptionQueueTimeTracker(clockNs::get));
+ queue.setSubscriptionMemoryManager(new
SubscriptionMemoryManager(1_000_000L));
+
+ assertNull(queue.poll("consumer"));
+ reader.currentSearchIndex = 1L;
+ assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+ queue.drivePrefetchOnce();
+ final SubscriptionEvent first = queue.poll("consumer");
+ assertNotNull(first);
+
+ clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(60_000L));
+ final Method injectWatermark =
+ ConsensusPrefetchingQueue.class.getDeclaredMethod("injectWatermark",
long.class);
+ injectWatermark.setAccessible(true);
+ injectWatermark.invoke(queue, 1L);
+ final SubscriptionEvent watermark = queue.poll("consumer");
+ assertNotNull(watermark);
+ assertEquals(
+ SubscriptionPollResponseType.WATERMARK.getType(),
+ watermark.getCurrentResponse().getResponseType());
+ assertEquals(60_000L, queue.getDeliveryIdleTimeMs());
+ assertTrue(queue.ack("consumer", first.getCommitContext()));
+ assertEquals(60_000L, queue.getDeliveryIdleTimeMs());
+ assertEquals(0L, queue.getLastDeliveryIntervalMs());
+
+ clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(1L));
+ reader.currentSearchIndex = 2L;
+ assertTrue(pendingEntries(queue).offer(createRequest(2L)));
+ queue.drivePrefetchOnce();
+ final SubscriptionEvent second = queue.poll("consumer");
+ assertNotNull(second);
+ assertEquals(60_001L, queue.getLastDeliveryIntervalMs());
+ assertEquals(60_001L, queue.getMaxDeliveryIntervalMs());
+ assertEquals(0L, queue.getDeliveryIdleTimeMs());
+ assertTrue(queue.ack("consumer", second.getCommitContext()));
+
+ clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(1L));
+ reader.currentSearchIndex = 3L;
+ assertTrue(pendingEntries(queue).offer(createRequest(3L)));
+ queue.drivePrefetchOnce();
+ assertNotNull(queue.poll("consumer"));
+ assertEquals(1L, queue.getLastDeliveryIntervalMs());
+ assertEquals(60_001L, queue.getMaxDeliveryIntervalMs());
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+ CommonDescriptor.getInstance()
+ .getConfig()
+ .setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ @Test
+ public void testTimingRemainsReadableDuringBlockedPrefetch() throws
Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir =
temporaryFolder.newFolder("blocked-prefetch-timing");
+ final CountDownLatch conversionStarted = new CountDownLatch(1);
+ final CountDownLatch releaseConversion = new CountDownLatch(1);
+ final ExecutorService executor = Executors.newSingleThreadExecutor();
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.getDatabaseName()).thenReturn("db");
+ when(converter.convert(any()))
+ .thenAnswer(
+ ignored -> {
+ conversionStarted.countDown();
+ assertTrue(releaseConversion.await(5L, TimeUnit.SECONDS));
+ return Collections.singletonList(createTablet());
+ });
+ queue =
+ newQueue(
+ "consumerGroup",
+ new DataRegionId(1),
+ reader,
+ converter,
+ newCommitManager(systemDir),
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+ final AtomicLong clockNs = new AtomicLong(Long.MAX_VALUE - 1_000_000L);
+ setTimeTracker(queue, new SubscriptionQueueTimeTracker(clockNs::get));
+ queue.setSubscriptionMemoryManager(new
SubscriptionMemoryManager(1_000_000L));
+
+ assertNull(queue.poll("consumer"));
+ reader.currentSearchIndex = 1L;
+ assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+ final ConsensusPrefetchingQueue blockedQueue = queue;
+ final Future<PrefetchRoundResult> round =
executor.submit(blockedQueue::drivePrefetchOnce);
+ assertTrue(conversionStarted.await(5L, TimeUnit.SECONDS));
+ clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(65_000L));
+
+ assertEquals(65_000L, queue.getPrefetchDurationMs());
+ assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+ assertEquals(65_000L, queue.getPrefetchIdleTimeMs());
+ assertEquals(65_000L, queue.getDeliveryIdleTimeMs());
+ assertEquals("65000",
queue.coreReportMessage().get("prefetchDurationMs"));
+ assertEquals("0",
queue.coreReportMessage().get("subscriptionMemoryUsedInBytes"));
+ assertEquals("0",
queue.coreReportMessage().get("dataNodeSubscriptionMemoryUsedInBytes"));
+
+ releaseConversion.countDown();
+ round.get(5L, TimeUnit.SECONDS);
+ assertEquals(0L, queue.getPrefetchDurationMs());
+ assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+ assertEquals(0L, queue.getPrefetchIdleTimeMs());
+ queue.drivePrefetchOnce();
+ assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+ } finally {
+ releaseConversion.countDown();
+ executor.shutdownNow();
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
+ private static void setTimeTracker(
+ final ConsensusPrefetchingQueue queue, final
SubscriptionQueueTimeTracker tracker)
+ throws Exception {
+ final Field field =
ConsensusPrefetchingQueue.class.getDeclaredField("timeTracker");
+ field.setAccessible(true);
+ field.set(queue, tracker);
+ }
+
@Test
public void
testRealtimeBacklogIsDrainedBeforeMorePendingEntriesAreConverted() throws
Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index 600073d7e3d..0b9e12eda48 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -94,7 +94,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -2388,6 +2388,57 @@ public class ConsensusPrefetchingQueueTest {
}
}
+ @Test
+ public void testPeriodicStatsDoNotReadWal() throws Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final File systemDir = temporaryFolder.newFolder("stats-without-wal-read");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(new
FakeConsensusReqReader());
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ new DataRegionId(1),
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ mock(ConsensusLogToTabletConverter.class),
+ newCommitManager(systemDir),
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+
+ final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+ setSubscriptionWalIterator(queue, iterator);
+ final Field lastStatsLogTime =
+
ConsensusPrefetchingQueue.class.getDeclaredField("lastStatsLogTimeMs");
+ lastStatsLogTime.setAccessible(true);
+ lastStatsLogTime.setLong(queue, 0L);
+ final Method logStats =
+
ConsensusPrefetchingQueue.class.getDeclaredMethod("logPeriodicStatsIfNecessary");
+ logStats.setAccessible(true);
+ logStats.invoke(queue);
+
+ verify(iterator).hasBufferedNext();
+ verify(iterator, never()).hasNext();
+ verify(iterator, never()).next();
+ verify(iterator, never()).refresh();
+ assertEquals(1L, queue.getCurrentReadSearchIndex());
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
@Test
public void testPendingCursorAdvanceFastForwardsWalIteratorInPlace() throws
Exception {
final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -2398,6 +2449,8 @@ public class ConsensusPrefetchingQueueTest {
final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(reader);
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.convert(any())).thenReturn(Collections.emptyList());
queue =
new ConsensusPrefetchingQueue(
"consumerGroup",
@@ -2409,28 +2462,26 @@ public class ConsensusPrefetchingQueueTest {
"topic",
SubscriptionWalRetentionPolicy.UNBOUNDED,
SubscriptionWalRetentionPolicy.UNBOUNDED),
- mock(ConsensusLogToTabletConverter.class),
+ converter,
newCommitManager(systemDir),
new RegionProgress(Collections.emptyMap()),
1L,
1L,
true);
+ for (long searchIndex = 1L; searchIndex <= 7L; searchIndex++) {
+ assertTrue(pendingEntries(queue).offer(createRequest(searchIndex)));
+ }
+ reader.currentSearchIndex = 7L;
+ assertNull(queue.poll("consumer"));
final ProgressWALIterator staleIterator =
mock(ProgressWALIterator.class);
setSubscriptionWalIterator(queue, staleIterator);
-
- final Method advancePendingCursor =
- ConsensusPrefetchingQueue.class.getDeclaredMethod(
- "advanceLocalCursorFromPendingIfPresent",
IndexedConsensusRequest.class, long.class);
- advancePendingCursor.setAccessible(true);
- advancePendingCursor.invoke(
- queue,
- new IndexedConsensusRequest(7L, Collections.emptyList()),
- queue.getCurrentSeekGeneration());
+ queue.drivePrefetchOnce();
assertEquals(8L, queue.getCurrentReadSearchIndex());
+ verify(converter, times(7)).convert(any());
verify(staleIterator)
- .advanceTo(anyLong(),
any(ProgressWALIterator.WriterProgressCoverage.class));
+ .advanceTo(eq(8L),
any(ProgressWALIterator.WriterProgressCoverage.class));
verify(staleIterator, never()).close();
assertSame(staleIterator, subscriptionWalIterator(queue));
} finally {
@@ -2545,10 +2596,14 @@ public class ConsensusPrefetchingQueueTest {
reader.currentSearchIndex = 3L;
assertNull(queue.poll("consumer"));
+ final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+ setSubscriptionWalIterator(queue, iterator);
queue.drivePrefetchOnce();
assertEquals(2, conversionCount.get());
assertEquals(2L, queue.getCurrentReadSearchIndex());
+ verify(iterator).advanceTo(eq(2L),
any(ProgressWALIterator.WriterProgressCoverage.class));
+ verify(iterator, never()).close();
assertEquals(oneTabletBytes, queue.getRetainedTabletBytes());
assertTrue(memoryManager.getFreeMemorySizeInBytes() > 0L);
assertEquals(1, queue.getPrefetchedEventCount());
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
index e25c77803cf..54ea59fbb0a 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
@@ -66,6 +66,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -210,6 +211,127 @@ public class ConsensusPrefetchingQueueWalBackpressureTest
{
}
}
+ @Test
+ public void testWalMemoryBlockRetriesWithoutRecreatingIterator() throws
Exception {
+ final String originalSystemDir =
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+ final CommonConfig config = CommonDescriptor.getInstance().getConfig();
+ final int originalBatchMaxWalEntries =
config.getSubscriptionConsensusBatchMaxWalEntries();
+ final int originalBatchMaxTabletCount =
config.getSubscriptionConsensusBatchMaxTabletCount();
+ final long originalBatchMaxSize =
config.getSubscriptionConsensusBatchMaxSizeInBytes();
+ final int originalBatchMaxDelay =
config.getSubscriptionConsensusBatchMaxDelayInMs();
+ final File systemDir =
temporaryFolder.newFolder("system-wal-memory-retry");
+ final File walDirectory = temporaryFolder.newFolder("wal-memory-retry");
+ ConsensusPrefetchingQueue queue = null;
+ try {
+ final Tablet sampleTablet = createTablet();
+ final long oneTabletBytes = sampleTablet.ramBytesUsed();
+ config.setSubscriptionConsensusBatchMaxWalEntries(128);
+ config.setSubscriptionConsensusBatchMaxTabletCount(128);
+ config.setSubscriptionConsensusBatchMaxSizeInBytes(oneTabletBytes * 16L);
+ config.setSubscriptionConsensusBatchMaxDelayInMs(0);
+
+ writeSealedWal(walDirectory);
+
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(walDirectory);
+ when(walNode.getCurrentSearchIndex()).thenReturn((long) REQUEST_COUNT);
+ when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
+ when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new
WALMetaData());
+
+ final IoTConsensusServerImpl serverImpl =
mock(IoTConsensusServerImpl.class);
+ when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
+ when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new
WriterSafeFrontierTracker());
+
+ final List<Long> conversionAttempts = new ArrayList<>();
+ final ConsensusLogToTabletConverter converter =
mock(ConsensusLogToTabletConverter.class);
+ when(converter.convert(any()))
+ .thenAnswer(
+ invocation -> {
+ final InsertNode insertNode = (InsertNode)
invocation.getArgument(0);
+ conversionAttempts.add(insertNode.getSearchIndex());
+ return Collections.singletonList(createTablet());
+ });
+ when(converter.getDatabaseName()).thenReturn("db");
+
+ final DataRegionId regionId = new DataRegionId(1);
+ final ConsensusSubscriptionCommitManager commitManager =
newCommitManager(systemDir);
+ queue =
+ new ConsensusPrefetchingQueue(
+ "consumerGroup",
+ "topic",
+ TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+ regionId,
+ serverImpl,
+ new SubscriptionWalRetentionPolicy(
+ "topic",
+ SubscriptionWalRetentionPolicy.UNBOUNDED,
+ SubscriptionWalRetentionPolicy.UNBOUNDED),
+ converter,
+ commitManager,
+ new RegionProgress(Collections.emptyMap()),
+ 1L,
+ 1L,
+ true);
+ final TrackingSubscriptionMemoryManager memoryManager =
+ new TrackingSubscriptionMemoryManager(oneTabletBytes + Math.max(1L,
oneTabletBytes / 2L));
+ queue.setSubscriptionMemoryManager(memoryManager);
+
+ assertNull(queue.poll("consumer"));
+ final ProgressWALIterator iterator = subscriptionWalIterator(queue);
+
+ queue.drivePrefetchOnce();
+ assertEquals(Arrays.asList(1L, 2L), conversionAttempts);
+ assertEquals(2L, queue.getCurrentReadSearchIndex());
+ assertTrue(iterator.hasBufferedNext());
+ assertSame(iterator, subscriptionWalIterator(queue));
+ assertMemoryBounded(queue, memoryManager);
+
+ // No memory has been released yet: another round must neither convert
nor lose the request.
+ queue.drivePrefetchOnce();
+ assertEquals(Arrays.asList(1L, 2L), conversionAttempts);
+ assertEquals(2L, queue.getCurrentReadSearchIndex());
+ assertSame(iterator, subscriptionWalIterator(queue));
+
+ for (long expectedLocalSeq = 1L; expectedLocalSeq <= REQUEST_COUNT;
expectedLocalSeq++) {
+ final SubscriptionEvent event = queue.poll("consumer");
+ assertNotNull(event);
+ assertEquals(expectedLocalSeq,
event.getCommitContext().getWriterProgress().getLocalSeq());
+ assertEquals(
+ 1000L + expectedLocalSeq,
+ event.getCommitContext().getWriterProgress().getPhysicalTime());
+ assertTrue(queue.ack("consumer", event.getCommitContext()));
+ assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+
+ queue.drivePrefetchOnce();
+ assertSame(iterator, subscriptionWalIterator(queue));
+ assertMemoryBounded(queue, memoryManager);
+ }
+
+ assertEquals(Arrays.asList(1L, 2L, 2L, 3L, 3L), conversionAttempts);
+ assertEquals(3L, queue.getWalPathAcceptedEntries());
+ assertEquals(4L, queue.getCurrentReadSearchIndex());
+ assertEquals(0, queue.getPrefetchedEventCount());
+ assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
+ assertEquals(0L, queue.getRetainedTabletBytes());
+ assertNull(queue.poll("consumer"));
+ assertEquals(
+ new WriterProgress(1000L + REQUEST_COUNT, REQUEST_COUNT),
+ commitManager
+ .getCommittedRegionProgress("consumerGroup", "topic", regionId)
+ .getWriterPositions()
+ .get(new WriterId(regionId.toString(), WRITER_NODE_ID)));
+ } finally {
+ if (queue != null) {
+ queue.close();
+ }
+
config.setSubscriptionConsensusBatchMaxWalEntries(originalBatchMaxWalEntries);
+
config.setSubscriptionConsensusBatchMaxTabletCount(originalBatchMaxTabletCount);
+ config.setSubscriptionConsensusBatchMaxSizeInBytes(originalBatchMaxSize);
+ config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+ }
+ }
+
private static void writeSealedWal(final File walDirectory) throws Exception
{
final File historicalWal =
new File(
@@ -288,6 +410,13 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
return (BlockingQueue<IndexedConsensusRequest>) field.get(queue);
}
+ private static ProgressWALIterator subscriptionWalIterator(final
ConsensusPrefetchingQueue queue)
+ throws Exception {
+ final Field field =
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");
+ field.setAccessible(true);
+ return (ProgressWALIterator) field.get(queue);
+ }
+
private static ConsensusSubscriptionCommitManager newCommitManager(final
File systemDir)
throws Exception {
IoTDBDescriptor.getInstance().getConfig().setSystemDir(systemDir.getAbsolutePath());
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
index 8af97b31964..d6159e6c692 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.subscription.broker.consensus;
import
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType;
import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodePipeMessages;
import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.SearchNode;
import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType;
import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry;
@@ -397,7 +398,8 @@ public class ProgressWALIteratorTest {
final String detail = iterator.getSkippedBrokenWalFileDetails(0);
assertTrue(detail.contains(firstBrokenWal.getName()));
assertTrue(detail.contains(secondBrokenWal.getName()));
- assertTrue(detail.contains("skippedEntries=unknown"));
+ assertTrue(
+ detail.contains("skippedEntries=" +
DataNodePipeMessages.MESSAGE_UNKNOWN_AD921D60));
assertEquals(2L, iterator.next().getSearchIndex());
assertFalse(iterator.hasNext());
assertTrue(iterator.hasSkippedBrokenWalFiles());
@@ -735,6 +737,47 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testBufferedNextDoesNotScanPastStaleLocalRequest() throws
Exception {
+ final Path dir =
Files.createTempDirectory("progress-wal-iterator-buffered-next");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 2,
WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7,
1L));
+ writer.write(searchableEntry(-1L), singleEntryMeta(19, -1L, 1L, 200L,
8, 20L));
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 300L, 7,
2L));
+ }
+ try (WALWriter ignored = new WALWriter(successorWal, WALFileVersion.V3))
{
+ // Seal the data file so all requests can be read from retained WAL.
+ }
+ try (ProgressWALIterator iterator = new
ProgressWALIterator(dir.toFile(), 1L)) {
+ assertFalse(iterator.hasBufferedNext());
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasBufferedNext());
+
+ iterator.advanceTo(2L, null);
+ assertFalse(iterator.hasBufferedNext());
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasBufferedNext());
+
+ iterator.advanceTo(3L, null);
+ assertTrue(iterator.hasBufferedNext());
+ assertEquals(-1L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasBufferedNext());
+ assertFalse(iterator.hasNext());
+ }
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testAdvanceToKeepsUncoveredFollowerRequestInCurrentFile() throws
Exception {
final Path dir =
Files.createTempDirectory("progress-wal-iterator-current-file-advance");
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionWalPrefetchPerformanceTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionWalPrefetchPerformanceTest.java
new file mode 100644
index 00000000000..b268bfd4249
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionWalPrefetchPerformanceTest.java
@@ -0,0 +1,380 @@
+/*
+ * 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.iotdb.db.subscription.broker.consensus;
+
+import org.apache.iotdb.commons.request.IConsensusRequest;
+import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode;
+import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode;
+import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALMetaData;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALWriter;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALByteBufferForTest;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Manual comparisons of the iterator operations changed by subscription
prefetch optimizations.
+ * Both paths use the current implementation and the same sealed, uncompressed
V3 WAL. Wall time is
+ * diagnostic only; assertions cover work counts, writer progress and retry
payload integrity.
+ */
+public class SubscriptionWalPrefetchPerformanceTest {
+
+ private static final String PREFIX = "iotdb.test.subscription.performance.";
+ private static final int FRAGMENTS = 2;
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ /**
+ * Enable with -Diotdb.test.subscription.performance.prefetch-overhead=true.
Models a lagging
+ * iterator and a pending batch that advances past a retained file with an
uncovered follower.
+ */
+ @Test
+ public void testNoIoStatsProbeAndCoalescedAdvancePerformance() throws
Exception {
+ assumeEnabled("prefetch-overhead");
+ final int entryCount = positiveProperty("entries", 4096);
+ final int batchSize = positiveProperty("batch-size", 64);
+ final int iterations = positiveProperty("iterations", 5);
+ final int warmups = positiveProperty("warmups", 2);
+ assertTrue(entryCount >= 2);
+ final File walDirectory = createWal(entryCount, true, batchSize);
+ final long probeIndex = Math.max(1L, entryCount * 3L / 4L);
+
+ final Sample[] probing = new Sample[iterations];
+ final Sample[] peeking = new Sample[iterations];
+ final Sample[] perEntry = new Sample[iterations];
+ final Sample[] coalesced = new Sample[iterations];
+ for (int round = 0; round < warmups + iterations; round++) {
+ // Alternate order so the optimized path does not always follow the
baseline.
+ final boolean optimizedFirst = (round & 1) == 1;
+ final Sample firstProbe = measureStatsProbe(walDirectory, probeIndex,
optimizedFirst);
+ final Sample secondProbe = measureStatsProbe(walDirectory, probeIndex,
!optimizedFirst);
+ final Sample firstAdvance =
+ measureAdvance(walDirectory, entryCount, batchSize, optimizedFirst);
+ final Sample secondAdvance =
+ measureAdvance(walDirectory, entryCount, batchSize, !optimizedFirst);
+ if (round >= warmups) {
+ final int index = round - warmups;
+ probing[index] = optimizedFirst ? secondProbe : firstProbe;
+ peeking[index] = optimizedFirst ? firstProbe : secondProbe;
+ perEntry[index] = optimizedFirst ? secondAdvance : firstAdvance;
+ coalesced[index] = optimizedFirst ? firstAdvance : secondAdvance;
+ }
+ }
+
+ printWorkload(entryCount, warmups, iterations);
+ System.out.printf(Locale.ROOT, "batchSize=%d, probeSearchIndex=%d%n",
batchSize, probeIndex);
+ printComparison("stats probe (work=entry bodies read)", probing, peeking);
+ printComparison("cursor advance (work=footer tuples checked)", perEntry,
coalesced);
+ }
+
+ /**
+ * Enable with -Diotdb.test.subscription.performance.memory-retry=true.
Replays the queue's old
+ * reset and new cache-restoration operations after each rejected
materialization attempt.
+ * Includes deserialization; excludes Tablet conversion, allocation and
scheduler wait time.
+ */
+ @Test
+ public void testMemoryBlockedRetryPerformance() throws Exception {
+ assumeEnabled("memory-retry");
+ final int entryCount = positiveProperty("entries", 4096);
+ final int retryCount = positiveProperty("retries", 8);
+ final int iterations = positiveProperty("iterations", 5);
+ final int warmups = positiveProperty("warmups", 2);
+ assertTrue(entryCount >= 2);
+ final File walDirectory = createWal(entryCount, false, 0);
+ final long retryIndex = Math.max(1L, entryCount * 3L / 4L);
+
+ final Sample[] recreated = new Sample[iterations];
+ final Sample[] cached = new Sample[iterations];
+ for (int round = 0; round < warmups + iterations; round++) {
+ final boolean optimizedFirst = (round & 1) == 1;
+ final Sample first = measureRetries(walDirectory, retryIndex,
retryCount, optimizedFirst);
+ final Sample second = measureRetries(walDirectory, retryIndex,
retryCount, !optimizedFirst);
+ if (round >= warmups) {
+ final int index = round - warmups;
+ recreated[index] = optimizedFirst ? second : first;
+ cached[index] = optimizedFirst ? first : second;
+ }
+ }
+
+ printWorkload(entryCount, warmups, iterations);
+ System.out.printf(Locale.ROOT, "attempts=%d, retrySearchIndex=%d%n",
retryCount, retryIndex);
+ printComparison("memory retry (work=entry bodies read)", recreated,
cached);
+ }
+
+ private File createWal(final int entryCount, final boolean includeFollower,
final int batchSize)
+ throws Exception {
+ final File directory = temporaryFolder.newFolder();
+ final File historical =
+ new File(
+ directory, WALFileUtils.getLogFileName(0L, 0L,
WALFileStatus.CONTAINS_SEARCH_INDEX));
+ try (WALWriter writer = new WALWriter(historical, WALFileVersion.V3)) {
+ for (long index = 1L; index <= entryCount; index++) {
+ writeRequest(writer, index, 7, index);
+ }
+ if (includeFollower) {
+ writeRequest(writer, -1L, 8, 1L);
+ }
+ }
+ final File successor =
+ new File(
+ directory,
+ WALFileUtils.getLogFileName(1L, entryCount,
WALFileStatus.CONTAINS_SEARCH_INDEX));
+ try (WALWriter writer = new WALWriter(successor, WALFileVersion.V3)) {
+ if (includeFollower) {
+ for (long index = entryCount + 1L; index <= entryCount + batchSize +
1L; index++) {
+ writeRequest(writer, index, 7, index);
+ }
+ }
+ // Even an empty successor makes the first file historical and readable.
+ }
+ return directory;
+ }
+
+ private static void writeRequest(
+ final WALWriter writer, final long searchIndex, final int nodeId, final
long localSeq)
+ throws Exception {
+ for (int fragment = 0; fragment < FRAGMENTS; fragment++) {
+ final RelationalInsertRowNode node =
+ StatementTestUtils.genInsertRowNode(Math.toIntExact(localSeq *
FRAGMENTS + fragment));
+ node.setSearchIndex(searchIndex);
+ node.setLastFragment(fragment == FRAGMENTS - 1);
+ final WALInfoEntry entry = new WALInfoEntry(1L, node);
+ final ByteBuffer buffer = ByteBuffer.allocate(entry.serializedSize());
+ entry.serialize(new WALByteBufferForTest(buffer));
+ final WALMetaData metadata = new WALMetaData();
+ metadata.add(entry.serializedSize(), searchIndex, 1L, 1000L + localSeq,
nodeId, localSeq);
+ // Small individual segments intentionally avoid changing global WAL
compression settings.
+ writer.write(buffer, metadata);
+ }
+ }
+
+ private static Sample measureStatsProbe(
+ final File directory, final long probeIndex, final boolean optimized)
throws Exception {
+ final Field bodyCount = bodyCountField();
+ try (ProgressWALIterator iterator = new ProgressWALIterator(directory,
probeIndex)) {
+ final long startNanos = System.nanoTime();
+ final boolean ready = optimized ? iterator.hasBufferedNext() :
iterator.hasNext();
+ final long elapsedNanos = System.nanoTime() - startNanos;
+ assertEquals(!optimized, ready);
+ final long readBodies = bodyCount.getInt(iterator);
+ // One lookahead fragment is read to establish the end of a logical
request.
+ assertEquals(optimized ? 0L : probeIndex * FRAGMENTS + 1L, readBodies);
+ assertRequest(iterator.next(), probeIndex, 7, probeIndex);
+ return new Sample(elapsedNanos, readBodies);
+ }
+ }
+
+ private static Sample measureAdvance(
+ final File directory, final int entryCount, final int batchSize, final
boolean optimized)
+ throws Exception {
+ final long[] footerTuples = {0L};
+ final ProgressWALIterator.WriterProgressCoverage coverage =
+ (physicalTime, nodeId, localSeq) -> {
+ footerTuples[0]++;
+ return nodeId == 7 && localSeq <= entryCount;
+ };
+ try (ProgressWALIterator iterator = new ProgressWALIterator(directory,
1L)) {
+ final long startNanos = System.nanoTime();
+ if (optimized) {
+ iterator.advanceTo(entryCount + batchSize + 1L, coverage);
+ } else {
+ for (int index = 1; index <= batchSize; index++) {
+ iterator.advanceTo(entryCount + index + 1L, coverage);
+ }
+ }
+ final long elapsedNanos = System.nanoTime() - startNanos;
+ // The first follower tuple prevents skipping the retained file on every
coverage check.
+ assertEquals(
+ (entryCount * (long) FRAGMENTS + 1L) * (optimized ? 1L : batchSize),
footerTuples[0]);
+ assertRequest(iterator.next(), -1L, 8, 1L);
+ final long nextIndex = entryCount + batchSize + 1L;
+ assertRequest(iterator.next(), nextIndex, 7, nextIndex);
+ assertFalse(iterator.hasNext());
+ return new Sample(elapsedNanos, footerTuples[0]);
+ }
+ }
+
+ private static Sample measureRetries(
+ final File directory, final long retryIndex, final int attempts, final
boolean optimized)
+ throws Exception {
+ final Field bodyCount = bodyCountField();
+ ProgressWALIterator iterator = null;
+ IndexedConsensusRequest previousRequest = null;
+ long readBodies = 0L;
+ int previousBodyCount = 0;
+ try {
+ final long startNanos = System.nanoTime();
+ for (int attempt = 0; attempt < attempts; attempt++) {
+ if (iterator == null) {
+ iterator = new ProgressWALIterator(directory, retryIndex);
+ }
+ final IndexedConsensusRequest request = iterator.next();
+ assertRequest(request, retryIndex, 7, retryIndex);
+ final int currentBodyCount = bodyCount.getInt(iterator);
+ readBodies += optimized ? currentBodyCount - previousBodyCount :
currentBodyCount;
+ previousBodyCount = currentBodyCount;
+ if (optimized && previousRequest != null) {
+ assertSame(previousRequest, request);
+ }
+ previousRequest = request;
+ if (attempt + 1 < attempts) {
+ if (optimized) {
+ iterator.retry(request);
+ } else {
+ iterator.close();
+ iterator = null;
+ }
+ }
+ }
+ final long elapsedNanos = System.nanoTime() - startNanos;
+ assertEquals((retryIndex * FRAGMENTS + 1L) * (optimized ? 1L :
attempts), readBodies);
+ // The last attempt succeeds: the pending lookahead must still lead to
the next request.
+ assertRequest(iterator.next(), retryIndex + 1L, 7, retryIndex + 1L);
+ return new Sample(elapsedNanos, readBodies);
+ } finally {
+ if (iterator != null) {
+ iterator.close();
+ }
+ }
+ }
+
+ private static void assertRequest(
+ final IndexedConsensusRequest request,
+ final long searchIndex,
+ final int nodeId,
+ final long localSeq) {
+ assertEquals(searchIndex, request.getSearchIndex());
+ assertEquals(nodeId, request.getNodeId());
+ assertEquals(localSeq, request.getProgressLocalSeq());
+ assertEquals(1000L + localSeq, request.getPhysicalTime());
+ assertEquals(FRAGMENTS, request.getRequests().size());
+ final InsertNode insert =
ConsensusLogToTabletConverter.deserializeToInsertNode(request);
+ assertNotNull(insert);
+ assertEquals(searchIndex, insert.getSearchIndex());
+ assertEquals(nodeId, insert.getNodeId());
+ assertEquals(localSeq, insert.getSyncIndex());
+ assertEquals(1000L + localSeq, insert.getPhysicalTime());
+ assertTrue(insert instanceof InsertRowsNode);
+ final InsertRowsNode rows = (InsertRowsNode) insert;
+ assertEquals(FRAGMENTS, rows.getInsertRowNodeList().size());
+ for (int fragment = 0; fragment < FRAGMENTS; fragment++) {
+ final InsertRowNode row = rows.getInsertRowNodeList().get(fragment);
+ final long expectedValue = localSeq * FRAGMENTS + fragment;
+ assertEquals(expectedValue, row.getTime());
+ assertEquals((double) expectedValue, (Double) row.getValues()[2], 0.0);
+ }
+ for (final IConsensusRequest fragment : request.getRequests()) {
+ assertEquals(0, fragment.serializeToByteBuffer().position());
+ }
+ }
+
+ private static Field bodyCountField() throws Exception {
+ // All counted reads remain in one historical file; inspect existing state
without a
+ // production instrumentation hook or overhead in WALReader.next().
+ final Field field =
+
ProgressWALIterator.class.getDeclaredField("consumedEntryCountInCurrentFile");
+ field.setAccessible(true);
+ return field;
+ }
+
+ private static void assumeEnabled(final String benchmark) {
+ Assume.assumeTrue(
+ "Enable with -D" + PREFIX + benchmark + "=true",
Boolean.getBoolean(PREFIX + benchmark));
+ }
+
+ private static int positiveProperty(final String name, final int
defaultValue) {
+ final int value = Integer.getInteger(PREFIX + name, defaultValue);
+ assertTrue(name + " must be positive", value > 0);
+ return value;
+ }
+
+ private static void printWorkload(final int entryCount, final int warmups,
final int iterations) {
+ System.out.printf(
+ Locale.ROOT,
+ "Subscription WAL benchmark: requests=%d, fragments=%d, warmups=%d,
samples=%d, "
+ + "os=%s, java=%s%n",
+ entryCount,
+ FRAGMENTS,
+ warmups,
+ iterations,
+ System.getProperty("os.name"),
+ System.getProperty("java.version"));
+ }
+
+ private static void printComparison(
+ final String name, final Sample[] baseline, final Sample[] optimized) {
+ final double baselineNanos = medianNanos(baseline);
+ final double optimizedNanos = medianNanos(optimized);
+ System.out.printf(
+ Locale.ROOT,
+ "%s: baselineMedian=%.6f ms, optimizedMedian=%.6f ms, speedup=%.2fx, "
+ + "baselineWork=%d, optimizedWork=%d%n",
+ name,
+ baselineNanos / 1_000_000.0,
+ optimizedNanos / 1_000_000.0,
+ baselineNanos / Math.max(1.0, optimizedNanos),
+ baseline[0].work,
+ optimized[0].work);
+ }
+
+ private static double medianNanos(final Sample[] samples) {
+ final long[] values = new long[samples.length];
+ for (int index = 0; index < samples.length; index++) {
+ values[index] = samples[index].elapsedNanos;
+ }
+ Arrays.sort(values);
+ final int middle = values.length / 2;
+ return values.length % 2 == 0
+ ? values[middle - 1] + (values[middle] - values[middle - 1]) / 2.0
+ : values[middle];
+ }
+
+ private static final class Sample {
+ private final long elapsedNanos;
+ private final long work;
+
+ private Sample(final long elapsedNanos, final long work) {
+ this.elapsedNanos = elapsedNanos;
+ this.work = work;
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
index a2142ad61fd..68eee16ee2f 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
@@ -24,6 +24,8 @@ import org.apache.iotdb.commons.service.metric.MetricService;
import org.apache.iotdb.commons.service.metric.enums.Metric;
import org.apache.iotdb.db.subscription.broker.SubscriptionPrefetchingQueue;
import
org.apache.iotdb.db.subscription.broker.consensus.ConsensusPrefetchingQueue;
+import
org.apache.iotdb.db.subscription.resource.SubscriptionDataNodeResourceManager;
+import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
import org.apache.iotdb.metrics.config.MetricConfig;
import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
import org.apache.iotdb.metrics.metricsets.IMetricSet;
@@ -38,12 +40,15 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
/** Subscription metrics must survive a metric service restart, which drops
all metrics. */
@@ -96,6 +101,52 @@ public class SubscriptionMetricsRestartTest {
assertEquals(0, count(Metric.SUBSCRIPTION_EVENT_TRANSFER));
}
+ @Test
+ public void testSubscriptionMemoryMetrics() throws Exception {
+ final SubscriptionMetrics metrics = SubscriptionMetrics.getInstance();
+ // Subscription can be disabled in the default test configuration. Give
the shared allocator
+ // an explicit test budget and restore its original block after removing
the metrics.
+ final long budget = 10L;
+ final SubscriptionMemoryManager memoryManager =
SubscriptionDataNodeResourceManager.memory();
+ final long originalOversizedEntryCount =
memoryManager.getOversizedEntryCount();
+ final Field blockField =
SubscriptionMemoryManager.class.getDeclaredField("memoryBlock");
+ blockField.setAccessible(true);
+ final Object originalBlock = blockField.get(memoryManager);
+ try {
+ blockField.set(memoryManager, blockField.get(new
SubscriptionMemoryManager(budget)));
+ bind(metrics);
+ assertTrue(memoryManager.tryAllocate(budget + 1L));
+ try {
+ service.restartService();
+ assertEquals(
+ budget, ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES)).getValue(), 0);
+ assertEquals(
+ budget + 1L, ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_USED_BYTES)).getValue(), 0);
+ assertEquals(
+ 1, ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES)).getValue(), 0);
+ assertEquals(
+ originalOversizedEntryCount + 1L,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT)).getValue(),
+ 0);
+ assertFalse(memoryManager.tryAllocate(1L));
+ } finally {
+ memoryManager.release(budget + 1L);
+ }
+ assertEquals(0, ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_USED_BYTES)).getValue(), 0);
+ assertEquals(0, ((AutoGauge)
get(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES)).getValue(), 0);
+ service.removeMetricSet(metrics);
+ boundMetricSets.remove(metrics);
+ assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_USED_BYTES));
+ assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES));
+ assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES));
+ assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT));
+ } finally {
+ service.removeMetricSet(metrics);
+ boundMetricSets.remove(metrics);
+ blockField.set(memoryManager, originalBlock);
+ }
+ }
+
@Test
public void testConsensusPrefetchingQueueMetrics() {
final ConsensusSubscriptionPrefetchingQueueMetrics metrics =
@@ -105,16 +156,52 @@ public class SubscriptionMetricsRestartTest {
when(queue.getPrefetchingQueueId()).thenReturn(QUEUE_ID);
when(queue.getConsensusGroupId()).thenReturn(new DataRegionId(1));
when(queue.getLag()).thenReturn(7L);
+ when(queue.getLastDeliveryIntervalMs()).thenReturn(60_001L);
+ when(queue.getMaxDeliveryIntervalMs()).thenReturn(65_000L);
+ when(queue.getDeliveryIdleTimeMs()).thenReturn(60_000L);
+ when(queue.getPrefetchDurationMs()).thenReturn(65_000L);
+ when(queue.getMaxPrefetchDurationMs()).thenReturn(69_000L);
+ when(queue.getPrefetchIdleTimeMs()).thenReturn(67_000L);
metrics.register(queue);
try {
service.restartService();
assertEquals(7, ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_LAG)).getValue(), 0);
+ assertEquals(
+ 60_001,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS)).getValue(),
+ 0);
+ assertEquals(
+ 65_000,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS)).getValue(),
+ 0);
+ assertEquals(
+ 60_000,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS)).getValue(),
+ 0);
+ assertEquals(
+ 65_000,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS)).getValue(),
+ 0);
+ assertEquals(
+ 69_000,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS)).getValue(),
+ 0);
+ assertEquals(
+ 67_000,
+ ((AutoGauge)
get(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS)).getValue(),
+ 0);
metrics.mark(QUEUE_ID, new DataRegionId(1).toString(), 5);
assertEquals(5, ((Rate)
get(Metric.SUBSCRIPTION_EVENT_TRANSFER)).getCount());
} finally {
metrics.deregister(queue);
}
assertEquals(0, count(Metric.SUBSCRIPTION_CONSENSUS_LAG));
+ assertEquals(0,
count(Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS));
+ assertEquals(0,
count(Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS));
+ assertEquals(0,
count(Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS));
+ assertEquals(0, count(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS));
+ assertEquals(0,
count(Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS));
+ assertEquals(0,
count(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS));
assertEquals(0, count(Metric.SUBSCRIPTION_EVENT_TRANSFER));
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
index beda24e623b..31129d357bb 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
@@ -35,6 +35,8 @@ public class SubscriptionMemoryManagerTest {
assertFalse(memoryManager.tryAllocate(5L));
assertEquals(6L, memoryManager.getUsedMemorySizeInBytes());
assertEquals(4L, memoryManager.getFreeMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(0L, memoryManager.getOversizedEntryCount());
memoryManager.release(6L);
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
@@ -48,10 +50,19 @@ public class SubscriptionMemoryManagerTest {
assertTrue(memoryManager.tryAllocate(11L));
assertFalse(memoryManager.tryAllocate(1L));
assertEquals(11L, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(1L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(1L, memoryManager.getOversizedEntryCount());
memoryManager.release(11L);
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
assertTrue(memoryManager.tryAllocate(10L));
+ assertFalse(memoryManager.tryAllocate(11L));
+ assertEquals(1L, memoryManager.getOversizedEntryCount());
+ memoryManager.release(10L);
+ assertTrue(memoryManager.tryAllocate(12L));
+ assertEquals(2L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(2L, memoryManager.getOversizedEntryCount());
}
@Test
@@ -60,6 +71,7 @@ public class SubscriptionMemoryManagerTest {
assertFalse(memoryManager.tryAllocate(1L));
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOversizedEntryCount());
}
@Test
@@ -133,6 +145,8 @@ public class SubscriptionMemoryManagerTest {
SubscriptionMemoryManager.AllocationRejectionReason.OVERSIZED_ENTRY,
queue.tryAllocate(11L).getRejectionReason());
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+ assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+ assertEquals(0L, memoryManager.getOversizedEntryCount());
assertTrue(queue.tryAllocate(10L).isAccepted());
}
assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
index e20919801e4..e6569beb7e6 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
@@ -233,7 +233,20 @@ public enum Metric {
SUBSCRIPTION_CONSENSUS_PENDING_EVENT_COUNT("subscription_consensus_pending_event_count"),
SUBSCRIPTION_CONSENSUS_LAST_POLL_TIME("subscription_consensus_last_poll_time"),
SUBSCRIPTION_CONSENSUS_LAST_PROGRESS_TIME("subscription_consensus_last_progress_time"),
+ SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS(
+ "subscription_consensus_last_delivery_interval_ms"),
+ SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS(
+ "subscription_consensus_max_delivery_interval_ms"),
+
SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS("subscription_consensus_delivery_idle_time_ms"),
+
SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS("subscription_consensus_prefetch_duration_ms"),
+ SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS(
+ "subscription_consensus_max_prefetch_duration_ms"),
+
SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS("subscription_consensus_prefetch_idle_time_ms"),
SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS("subscription_consensus_progress_status"),
+ SUBSCRIPTION_MEMORY_USED_BYTES("subscription_memory_used_bytes"),
+ SUBSCRIPTION_MEMORY_LIMIT_BYTES("subscription_memory_limit_bytes"),
+ SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES("subscription_memory_overcommit_bytes"),
+
SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT("subscription_memory_oversized_entry_count"),
SUBSCRIPTION_CONSENSUS_MEMORY_USED("subscription_consensus_memory_used"),
SUBSCRIPTION_CONSENSUS_MEMORY_QUOTA("subscription_consensus_memory_quota"),
SUBSCRIPTION_CONSENSUS_MEMORY_MAXIMUM("subscription_consensus_memory_maximum"),