This is an automated email from the ASF dual-hosted git repository.
Duansg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new c884f11995 maintenance: make VictoriaMetrics flush recovery durable
(#4289)
c884f11995 is described below
commit c884f11995d46239a424e6d3251af113feb3a694
Author: Logic <[email protected]>
AuthorDate: Wed Aug 26 10:58:09 2026 +0800
maintenance: make VictoriaMetrics flush recovery durable (#4289)
Co-authored-by: Duansg <[email protected]>
---
.../controller/MetricsDataController.java | 15 +-
.../warehouse/service/MetricsDataService.java | 7 +
.../warehouse/service/WarehouseStorageStatus.java | 30 +++
.../service/impl/MetricsDataServiceImpl.java | 13 +-
.../store/history/tsdb/HistoryDataReader.java | 14 ++
.../tsdb/vm/VictoriaMetricsClusterDataStorage.java | 218 ++++++++++++++----
.../tsdb/vm/VictoriaMetricsDataStorage.java | 241 ++++++++++++++------
.../controller/MetricsDataControllerTest.java | 15 +-
.../warehouse/service/MetricsDataServiceTest.java | 4 +
.../vm/VictoriaMetricsClusterDataStorageTest.java | 249 +++++++++++++++++++++
.../tsdb/vm/VictoriaMetricsDataStorageTest.java | 111 +++++++++
11 files changed, 793 insertions(+), 124 deletions(-)
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/controller/MetricsDataController.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/controller/MetricsDataController.java
index 832059cd2d..b9844a8539 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/controller/MetricsDataController.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/controller/MetricsDataController.java
@@ -26,6 +26,7 @@ import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
+import org.apache.hertzbeat.warehouse.service.WarehouseStorageStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -51,14 +52,14 @@ public class MetricsDataController {
@GetMapping("/api/warehouse/storage/status")
@Operation(summary = "Query Warehouse Storage Server Status", description
= "Query the availability status of the storage service under the warehouse")
- public ResponseEntity<Message<Void>> getWarehouseStorageServerStatus() {
- Boolean status = metricsDataService.getWarehouseStorageServerStatus();
- if (Boolean.TRUE.equals(status)) {
- return ResponseEntity.ok(Message.success());
+ public ResponseEntity<Message<WarehouseStorageStatus>>
getWarehouseStorageServerStatus() {
+ WarehouseStorageStatus status =
metricsDataService.getWarehouseStorageStatus();
+ Message<WarehouseStorageStatus> message = Message.success(status);
+ if (!status.available()) {
+ message.setCode(FAIL_CODE);
+ message.setMsg("Service not available!");
}
-
- // historyDataReader does not exist or is not available
- return ResponseEntity.ok(Message.fail(FAIL_CODE, "Service not
available!"));
+ return ResponseEntity.ok(message);
}
@GetMapping("/api/monitor/{monitorId}/metrics/{metrics}")
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/MetricsDataService.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/MetricsDataService.java
index 6c39b78d58..ea2222cc31 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/MetricsDataService.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/MetricsDataService.java
@@ -31,6 +31,13 @@ public interface MetricsDataService {
*/
Boolean getWarehouseStorageServerStatus();
+ /**
+ * Query storage availability and write-path loss/backlog counters.
+ *
+ * @return warehouse storage status
+ */
+ WarehouseStorageStatus getWarehouseStorageStatus();
+
/**
* Query Real Time Metrics Data
* @param monitorId Monitor Id
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/WarehouseStorageStatus.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/WarehouseStorageStatus.java
new file mode 100644
index 0000000000..439db822fd
--- /dev/null
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/WarehouseStorageStatus.java
@@ -0,0 +1,30 @@
+/*
+ * 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.hertzbeat.warehouse.service;
+
+/**
+ * Operator-visible status for the configured warehouse history storage.
+ *
+ * @param available whether the storage's read-side health check is available
+ * @param droppedMetrics cumulative metric samples discarded by bounded writer
backpressure
+ * @param pendingMetrics metric samples currently buffered or retained for
retry
+ */
+public record WarehouseStorageStatus(boolean available, long droppedMetrics,
int pendingMetrics) {
+}
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/MetricsDataServiceImpl.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/MetricsDataServiceImpl.java
index df336e5e3a..a1c81ebc8e 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/MetricsDataServiceImpl.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/MetricsDataServiceImpl.java
@@ -35,6 +35,7 @@ import org.apache.hertzbeat.common.entity.dto.ValueRow;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.support.exception.CommonException;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
+import org.apache.hertzbeat.warehouse.service.WarehouseStorageStatus;
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader;
import org.springframework.stereotype.Service;
@@ -89,7 +90,17 @@ public class MetricsDataServiceImpl implements
MetricsDataService {
@Override
public Boolean getWarehouseStorageServerStatus() {
- return historyDataReader.isPresent() &&
historyDataReader.get().isServerAvailable();
+ return getWarehouseStorageStatus().available();
+ }
+
+ @Override
+ public WarehouseStorageStatus getWarehouseStorageStatus() {
+ return historyDataReader
+ .map(reader -> new WarehouseStorageStatus(
+ reader.isServerAvailable(),
+ reader.getDroppedMetricCount(),
+ reader.getPendingMetricCount()))
+ .orElseGet(() -> new WarehouseStorageStatus(false, 0, 0));
}
@Override
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
index 08a42bb919..fd821a7bfd 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
@@ -33,6 +33,20 @@ public interface HistoryDataReader {
*/
boolean isServerAvailable();
+ /**
+ * @return cumulative metric samples dropped by the history writer
+ */
+ default long getDroppedMetricCount() {
+ return 0;
+ }
+
+ /**
+ * @return metric samples currently waiting for history persistence
+ */
+ default int getPendingMetricCount() {
+ return 0;
+ }
+
/**
* @return whether this storage supports observability log query
*/
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
index 3777bb1830..e808f62202 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorage.java
@@ -37,6 +37,7 @@ import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -101,17 +102,25 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
private static final String MONITOR_METRICS_KEY = "__metrics__";
private static final String MONITOR_METRIC_KEY = "__metric__";
private static final long MAX_WAIT_MS = 500L;
- private static final int MAX_RETRIES = 3;
+ private static final int MAX_BUFFER_OFFER_ATTEMPTS = 3;
+ private static final int MAX_BATCHES_PER_FLUSH_TASK = 16;
+ private static final long FAILED_FLUSH_RETRY_SECONDS = 1L;
private final VictoriaMetricsClusterProperties vmClusterProps;
private final VictoriaMetricsInsertProperties vmInsertProps;
private final VictoriaMetricsSelectProperties vmSelectProps;
private final RestTemplate restTemplate;
private final
BlockingQueue<VictoriaMetricsDataStorage.VictoriaMetricsContent>
metricsBufferQueue;
+ private final Object metricsFlushLock = new Object();
private final AtomicLong ignoredLabelCollisionCount = new AtomicLong();
private HashedWheelTimer metricsFlushTimer = null;
private MetricsFlushTask metricsFlushtask = null;
+ private final AtomicBoolean immediateFlushPending = new
AtomicBoolean(false);
+ private final AtomicBoolean flushInFlight = new AtomicBoolean(false);
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final AtomicLong droppedMetricCount = new AtomicLong();
+ private List<VictoriaMetricsDataStorage.VictoriaMetricsContent> retryBatch
= Collections.emptyList();
private boolean isBatchImportEnabled = false;
@@ -138,7 +147,7 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
Thread thread = new Thread(r, "victoria-metrics-flush-timer");
thread.setDaemon(true);
return thread;
- }, 1, TimeUnit.SECONDS, 512);
+ }, 100, TimeUnit.MILLISECONDS, 512);
metricsFlushtask = new MetricsFlushTask();
this.metricsFlushTimer.newTimeout(metricsFlushtask, 0,
TimeUnit.SECONDS);
}
@@ -177,6 +186,10 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
@Override
public void saveData(CollectRep.MetricsData metricsData) {
+ if (closed.get()) {
+ log.warn("[Victoria Metrics] Rejecting metrics after storage
shutdown");
+ return;
+ }
if (!isServerAvailable()) {
serverAvailable = checkVictoriaMetricsDatasourceAvailable();
}
@@ -298,9 +311,22 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
@Override
public void destroy() {
+ synchronized (metricsFlushLock) {
+ if (!closed.compareAndSet(false, true)) {
+ return;
+ }
+ }
if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
metricsFlushTimer.stop();
}
+ immediateFlushPending.set(false);
+ while (hasPendingMetrics()) {
+ if (!flushBufferedMetrics()) {
+ log.error("[Victoria Metrics] Unable to flush {} buffered
metrics during shutdown",
+ getPendingMetricCount());
+ break;
+ }
+ }
}
@Override
@@ -599,7 +625,11 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
/**
* Save metric data to victoria-metric via HTTP call
*/
- public void
doSaveData(List<VictoriaMetricsDataStorage.VictoriaMetricsContent> contentList){
+ public void
doSaveData(List<VictoriaMetricsDataStorage.VictoriaMetricsContent> contentList)
{
+ trySaveData(contentList);
+ }
+
+ private boolean
trySaveData(List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
contentList) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -619,12 +649,15 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
httpEntity, String.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("insert metrics data to victoria-metrics success.");
+ return true;
} else {
- log.error("insert metrics data to victoria-metrics failed.
{}", responseEntity.getBody());
+ log.error("insert metrics data to victoria-metrics failed with
status {}",
+ responseEntity.getStatusCode());
}
} catch (Exception e){
log.error("flush metrics data to victoria-metrics error: {}.",
e.getMessage(), e);
}
+ return false;
}
/**
@@ -632,49 +665,123 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
* @param contentList victoriaMetricsContent List
*/
private void
sendVictoriaMetrics(List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
contentList) {
- for (VictoriaMetricsDataStorage.VictoriaMetricsContent content :
contentList) {
- boolean offered = false;
- int retryCount = 0;
- while (!offered && retryCount < MAX_RETRIES) {
+ for (int index = 0; index < contentList.size(); index++) {
+ VictoriaMetricsDataStorage.VictoriaMetricsContent content =
contentList.get(index);
+ boolean offered = metricsBufferQueue.offer(content);
+ for (int attempt = 1; attempt <= MAX_BUFFER_OFFER_ATTEMPTS &&
!offered; attempt++) {
+ if (closed.get()) {
+ return;
+ }
+ triggerImmediateFlush();
try {
- // Attempt to add to the queue for a limited time
offered = metricsBufferQueue.offer(content, MAX_WAIT_MS,
TimeUnit.MILLISECONDS);
- if (!offered) {
- // If the queue is still full, trigger an immediate
refresh to free up space
- if (retryCount == 0) {
- log.debug("victoria metrics buffer queue is full,
triggering immediate flush");
- triggerImmediateFlush();
- }
- retryCount++;
- // The short sleep allows the queue to clear out
- if (retryCount < MAX_RETRIES) {
- Thread.sleep(100L * retryCount);
- }
- }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- log.error("[Victoria Metrics] Interrupted while offering
metrics to buffer queue", e);
- break;
+ recordDroppedMetrics(contentList.size() - index, "producer
interrupted");
+ return;
}
}
- // When the maximum number of retries is reached, if it still
cannot be added to the queue, the data is saved directly
if (!offered) {
- log.warn("[Victoria Metrics] Failed to add metrics to buffer
after {} retries, saving directly", MAX_RETRIES);
- try {
- doSaveData(contentList);
- } catch (Exception e) {
- log.error("[Victoria Metrics] Failed to save metrics
directly: {}", e.getMessage(), e);
- }
+ recordDroppedMetrics(contentList.size() - index, "buffer
remained full");
+ return;
}
- // Refresh in advance to avoid waiting
if (metricsBufferQueue.size() >= vmInsertProps.bufferSize() * 0.8)
{
triggerImmediateFlush();
}
}
}
+ private void recordDroppedMetrics(int count, String reason) {
+ long total = droppedMetricCount.addAndGet(count);
+ if (total == count || (total & (total - 1)) == 0 || total % 100 == 0) {
+ log.error("[Victoria Metrics] Dropped {} metrics because {};
cumulative dropped metrics: {}",
+ count, reason, total);
+ }
+ }
+
+ @Override
+ public long getDroppedMetricCount() {
+ return droppedMetricCount.get();
+ }
+
private void triggerImmediateFlush() {
- metricsFlushTimer.newTimeout(metricsFlushtask, 0,
TimeUnit.MILLISECONDS);
+ scheduleImmediateFlush(0, TimeUnit.MILLISECONDS);
+ }
+
+ private void scheduleImmediateFlush(long delay, TimeUnit unit) {
+ if (closed.get() || metricsFlushTimer == null ||
metricsFlushTimer.isStop()) {
+ return;
+ }
+ if (immediateFlushPending.compareAndSet(false, true)) {
+ try {
+ metricsFlushTimer.newTimeout(new ImmediateMetricsFlushTask(),
delay, unit);
+ } catch (RuntimeException e) {
+ immediateFlushPending.set(false);
+ if (!closed.get()) {
+ log.warn("[Victoria Metrics] Unable to schedule immediate
flush: {}", e.getMessage());
+ }
+ }
+ }
+ }
+
+ private boolean flushBufferedMetrics() {
+ if (!flushInFlight.compareAndSet(false, true)) {
+ return false;
+ }
+ try {
+ List<VictoriaMetricsDataStorage.VictoriaMetricsContent> batch;
+ synchronized (metricsFlushLock) {
+ if (retryBatch.isEmpty()) {
+ List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
nextBatch =
+ new ArrayList<>(vmInsertProps.bufferSize());
+ metricsBufferQueue.drainTo(nextBatch,
vmInsertProps.bufferSize());
+ retryBatch = nextBatch;
+ }
+ batch = retryBatch;
+ }
+ if (batch.isEmpty()) {
+ return true;
+ }
+ if (!trySaveData(batch)) {
+ log.warn("[Victoria Metrics] Retaining {} metrics items for
retry", batch.size());
+ return false;
+ }
+ synchronized (metricsFlushLock) {
+ if (retryBatch == batch) {
+ retryBatch = Collections.emptyList();
+ }
+ }
+ log.debug("[Victoria Metrics] Flushed {} metrics items",
batch.size());
+ return true;
+ } finally {
+ flushInFlight.set(false);
+ }
+ }
+
+ private boolean hasPendingMetrics() {
+ synchronized (metricsFlushLock) {
+ return !retryBatch.isEmpty() || !metricsBufferQueue.isEmpty();
+ }
+ }
+
+ @Override
+ public int getPendingMetricCount() {
+ synchronized (metricsFlushLock) {
+ return retryBatch.size() + metricsBufferQueue.size();
+ }
+ }
+
+ private void schedulePeriodicFlush() {
+ if (closed.get() || metricsFlushTimer == null ||
metricsFlushTimer.isStop()) {
+ return;
+ }
+ try {
+ metricsFlushTimer.newTimeout(metricsFlushtask,
vmInsertProps.flushInterval(), TimeUnit.SECONDS);
+ } catch (RuntimeException e) {
+ if (!closed.get()) {
+ log.warn("[Victoria Metrics] Unable to schedule periodic
flush: {}", e.getMessage());
+ }
+ }
}
/**
@@ -683,18 +790,47 @@ public class VictoriaMetricsClusterDataStorage extends
AbstractHistoryDataStorag
private class MetricsFlushTask implements TimerTask {
@Override
public void run(Timeout timeout) {
+ boolean flushSucceeded = false;
try {
- List<VictoriaMetricsDataStorage.VictoriaMetricsContent> batch
= new ArrayList<>(vmInsertProps.bufferSize());
- metricsBufferQueue.drainTo(batch, vmInsertProps.bufferSize());
- if (!batch.isEmpty()) {
- doSaveData(batch);
- log.debug("[Victoria Metrics] Flushed {} metrics items",
batch.size());
- }
- if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
- metricsFlushTimer.newTimeout(this,
vmInsertProps.flushInterval(), TimeUnit.SECONDS);
- }
+ flushSucceeded = flushBufferedMetrics();
} catch (Exception e) {
log.error("[VictoriaMetrics] flush task error: {}",
e.getMessage(), e);
+ } finally {
+ if (!flushSucceeded && hasPendingMetrics() && !closed.get()) {
+ scheduleImmediateFlush(FAILED_FLUSH_RETRY_SECONDS,
TimeUnit.SECONDS);
+ }
+ schedulePeriodicFlush();
+ }
+ }
+ }
+
+ /**
+ * Executes an immediate flush without creating another periodic chain.
+ */
+ private class ImmediateMetricsFlushTask implements TimerTask {
+ @Override
+ public void run(Timeout timeout) {
+ boolean flushSucceeded = false;
+ try {
+ int flushedBatches = 0;
+ do {
+ flushSucceeded = flushBufferedMetrics();
+ flushedBatches++;
+ } while (flushSucceeded
+ && hasPendingMetrics()
+ && !closed.get()
+ && flushedBatches < MAX_BATCHES_PER_FLUSH_TASK);
+ } catch (Exception e) {
+ log.error("[VictoriaMetrics] immediate flush task error: {}",
e.getMessage(), e);
+ } finally {
+ immediateFlushPending.set(false);
+ if (hasPendingMetrics() && !closed.get()) {
+ if (flushSucceeded) {
+ scheduleImmediateFlush(0, TimeUnit.MILLISECONDS);
+ } else {
+ scheduleImmediateFlush(FAILED_FLUSH_RETRY_SECONDS,
TimeUnit.SECONDS);
+ }
+ }
}
}
}
diff --git
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
index 5a80c9824c..7f199ff4f7 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorage.java
@@ -108,16 +108,24 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
MONITOR_METRIC_KEY,
LABEL_KEY_INSTANCE);
private static final long MAX_WAIT_MS = 500L;
- private static final int MAX_RETRIES = 3;
+ private static final int MAX_BUFFER_OFFER_ATTEMPTS = 3;
+ private static final int MAX_BATCHES_PER_FLUSH_TASK = 16;
+ private static final long FAILED_FLUSH_RETRY_SECONDS = 1L;
private final VictoriaMetricsProperties victoriaMetricsProp;
private final RestTemplate restTemplate;
private final
BlockingQueue<VictoriaMetricsDataStorage.VictoriaMetricsContent>
metricsBufferQueue;
+ private final Object metricsFlushLock = new Object();
private final AtomicLong ignoredLabelCollisionCount = new AtomicLong();
private HashedWheelTimer metricsFlushTimer = null;
+ private MetricsFlushTask metricsFlushTask = null;
private final VictoriaMetricsProperties.InsertConfig insertConfig;
- private final AtomicBoolean draining = new AtomicBoolean(false);
+ private final AtomicBoolean immediateFlushPending = new
AtomicBoolean(false);
+ private final AtomicBoolean flushInFlight = new AtomicBoolean(false);
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final AtomicLong droppedMetricCount = new AtomicLong();
+ private List<VictoriaMetricsContent> retryBatch = Collections.emptyList();
public VictoriaMetricsDataStorage(VictoriaMetricsProperties
victoriaMetricsProperties, RestTemplate restTemplate) {
if (victoriaMetricsProperties == null) {
@@ -135,12 +143,12 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
private void initializeFlushTimer() {
this.metricsFlushTimer = new HashedWheelTimer(r -> {
- Thread thread = new Thread(r,
"victoria-metrics-cluster-flush-timer");
+ Thread thread = new Thread(r, "victoria-metrics-flush-timer");
thread.setDaemon(true);
return thread;
- }, 1, TimeUnit.SECONDS, 512);
- // start flush interval timer
- this.metricsFlushTimer.newTimeout(new MetricsFlushTask(null),
insertConfig.flushInterval(), TimeUnit.SECONDS);
+ }, 100, TimeUnit.MILLISECONDS, 512);
+ metricsFlushTask = new MetricsFlushTask();
+ this.metricsFlushTimer.newTimeout(metricsFlushTask,
insertConfig.flushInterval(), TimeUnit.SECONDS);
}
private boolean checkVictoriaMetricsDatasourceAvailable() {
@@ -169,6 +177,10 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
@Override
public void saveData(CollectRep.MetricsData metricsData) {
+ if (closed.get()) {
+ log.warn("[Victoria Metrics] Rejecting metrics after storage
shutdown");
+ return;
+ }
if (!isServerAvailable()) {
serverAvailable = checkVictoriaMetricsDatasourceAvailable();
}
@@ -317,9 +329,22 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
@Override
public void destroy() {
+ synchronized (metricsFlushLock) {
+ if (!closed.compareAndSet(false, true)) {
+ return;
+ }
+ }
if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
metricsFlushTimer.stop();
}
+ immediateFlushPending.set(false);
+ while (hasPendingMetrics()) {
+ if (!flushBufferedMetrics()) {
+ log.error("[Victoria Metrics] Unable to flush {} buffered
metrics during shutdown",
+ getPendingMetricCount());
+ break;
+ }
+ }
}
@Override
@@ -630,54 +655,121 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
* @param contentList victoriaMetricsContent List
*/
private void
sendVictoriaMetrics(List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
contentList) {
- for (VictoriaMetricsDataStorage.VictoriaMetricsContent content :
contentList) {
- boolean offered = false;
- int retryCount = 0;
- while (!offered && retryCount < MAX_RETRIES) {
+ for (int index = 0; index < contentList.size(); index++) {
+ VictoriaMetricsDataStorage.VictoriaMetricsContent content =
contentList.get(index);
+ boolean offered = metricsBufferQueue.offer(content);
+ for (int attempt = 1; attempt <= MAX_BUFFER_OFFER_ATTEMPTS &&
!offered; attempt++) {
+ if (closed.get()) {
+ return;
+ }
+ triggerImmediateFlush();
try {
- // Attempt to add to the queue for a limited time
offered = metricsBufferQueue.offer(content, MAX_WAIT_MS,
TimeUnit.MILLISECONDS);
- if (!offered) {
- // If the queue is still full, trigger an immediate
refresh to free up space
- if (retryCount == 0) {
- log.debug("victoria metrics buffer queue is full,
triggering immediate flush");
- triggerImmediateFlush();
- }
- retryCount++;
- // The short sleep allows the queue to clear out
- if (retryCount < MAX_RETRIES) {
- Thread.sleep(100L * retryCount);
- }
- }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- log.error("[Victoria Metrics] Interrupted while offering
metrics to buffer queue", e);
- break;
+ recordDroppedMetrics(contentList.size() - index, "producer
interrupted");
+ return;
}
}
- // When the maximum number of retries is reached, if it still
cannot be added to the queue, the data is saved directly
if (!offered) {
- log.warn("[Victoria Metrics] Failed to add metrics to buffer
after {} retries, saving directly", MAX_RETRIES);
- try {
- doSaveData(contentList);
- } catch (Exception e) {
- log.error("[Victoria Metrics] Failed to save metrics
directly: {}", e.getMessage(), e);
- }
+ recordDroppedMetrics(contentList.size() - index, "buffer
remained full");
+ return;
+ }
+ if (metricsBufferQueue.size() >= insertConfig.bufferSize() * 0.8) {
+ triggerImmediateFlush();
}
}
- // Refresh in advance to avoid waiting
- if (metricsBufferQueue.size() >= insertConfig.bufferSize() * 0.8
- && draining.compareAndSet(false, true)) {
- triggerImmediateFlush();
+ }
+
+ private void recordDroppedMetrics(int count, String reason) {
+ long total = droppedMetricCount.addAndGet(count);
+ if (total == count || (total & (total - 1)) == 0 || total % 100 == 0) {
+ log.error("[Victoria Metrics] Dropped {} metrics because {};
cumulative dropped metrics: {}",
+ count, reason, total);
}
}
+ @Override
+ public long getDroppedMetricCount() {
+ return droppedMetricCount.get();
+ }
+
private void triggerImmediateFlush() {
- List<VictoriaMetricsDataStorage.VictoriaMetricsContent> batch = new
ArrayList<>(insertConfig.bufferSize());
- metricsBufferQueue.drainTo(batch, insertConfig.bufferSize());
- draining.set(false);
- if (!batch.isEmpty()) {
- metricsFlushTimer.newTimeout(new MetricsFlushTask(batch), 0,
TimeUnit.MILLISECONDS);
+ scheduleImmediateFlush(0, TimeUnit.MILLISECONDS);
+ }
+
+ private void scheduleImmediateFlush(long delay, TimeUnit unit) {
+ if (closed.get() || metricsFlushTimer == null ||
metricsFlushTimer.isStop()) {
+ return;
+ }
+ if (immediateFlushPending.compareAndSet(false, true)) {
+ try {
+ metricsFlushTimer.newTimeout(new ImmediateMetricsFlushTask(),
delay, unit);
+ } catch (RuntimeException e) {
+ immediateFlushPending.set(false);
+ if (!closed.get()) {
+ log.warn("[Victoria Metrics] Unable to schedule immediate
flush: {}", e.getMessage());
+ }
+ }
+ }
+ }
+
+ private boolean flushBufferedMetrics() {
+ if (!flushInFlight.compareAndSet(false, true)) {
+ return false;
+ }
+ try {
+ List<VictoriaMetricsContent> batch;
+ synchronized (metricsFlushLock) {
+ if (retryBatch.isEmpty()) {
+ List<VictoriaMetricsContent> nextBatch = new
ArrayList<>(insertConfig.bufferSize());
+ metricsBufferQueue.drainTo(nextBatch,
insertConfig.bufferSize());
+ retryBatch = nextBatch;
+ }
+ batch = retryBatch;
+ }
+ if (batch.isEmpty()) {
+ return true;
+ }
+ if (!trySaveData(batch)) {
+ log.warn("[Victoria Metrics] Retaining {} metrics items for
retry", batch.size());
+ return false;
+ }
+ synchronized (metricsFlushLock) {
+ if (retryBatch == batch) {
+ retryBatch = Collections.emptyList();
+ }
+ }
+ log.debug("[Victoria Metrics] Flushed {} metrics items",
batch.size());
+ return true;
+ } finally {
+ flushInFlight.set(false);
+ }
+ }
+
+ private boolean hasPendingMetrics() {
+ synchronized (metricsFlushLock) {
+ return !retryBatch.isEmpty() || !metricsBufferQueue.isEmpty();
+ }
+ }
+
+ @Override
+ public int getPendingMetricCount() {
+ synchronized (metricsFlushLock) {
+ return retryBatch.size() + metricsBufferQueue.size();
+ }
+ }
+
+ private void schedulePeriodicFlush() {
+ if (closed.get() || metricsFlushTimer == null ||
metricsFlushTimer.isStop()) {
+ return;
+ }
+ try {
+ metricsFlushTimer.newTimeout(metricsFlushTask,
insertConfig.flushInterval(), TimeUnit.SECONDS);
+ } catch (RuntimeException e) {
+ if (!closed.get()) {
+ log.warn("[Victoria Metrics] Unable to schedule periodic
flush: {}", e.getMessage());
+ }
}
}
@@ -685,42 +777,46 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
* Regularly refresh the buffer queue to the vm
*/
private class MetricsFlushTask implements TimerTask {
- private final List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
batch;
-
- public
MetricsFlushTask(List<VictoriaMetricsDataStorage.VictoriaMetricsContent> batch)
{
- this.batch = batch;
- }
-
@Override
public void run(Timeout timeout) {
+ boolean flushSucceeded = false;
try {
- if (batch == null) {
- // If the batch is null, it means that the timer is
triggered by flush interval timer
- List<VictoriaMetricsDataStorage.VictoriaMetricsContent>
batchT = new ArrayList<>(insertConfig.bufferSize());
- metricsBufferQueue.drainTo(batchT,
insertConfig.bufferSize());
- triggerDoSaveData(batchT);
- // Reschedule the next flush task
- triggerIntervalFlushTimer();
- } else {
- // If the batch is not null, it means that the timer is
triggered by the immediate flush
- triggerDoSaveData(batch);
- }
+ flushSucceeded = flushBufferedMetrics();
} catch (Exception e) {
log.error("[VictoriaMetrics] flush task error: {}",
e.getMessage(), e);
+ } finally {
+ if (!flushSucceeded && hasPendingMetrics() && !closed.get()) {
+ scheduleImmediateFlush(FAILED_FLUSH_RETRY_SECONDS,
TimeUnit.SECONDS);
+ }
+ schedulePeriodicFlush();
}
}
+ }
- private void triggerDoSaveData(List<VictoriaMetricsContent> batch) {
- if (!batch.isEmpty()) {
- doSaveData(batch);
- log.debug("[Victoria Metrics] Flushed {} metrics items",
batch.size());
- }
- }
-
- private void triggerIntervalFlushTimer() {
- if (metricsFlushTimer != null && !metricsFlushTimer.isStop()) {
- metricsFlushTimer.newTimeout(new MetricsFlushTask(null),
insertConfig.flushInterval(), TimeUnit.SECONDS);
- log.debug("[Victoria Metrics] Rescheduled next flush task in
{} seconds.", insertConfig.flushInterval());
+ private class ImmediateMetricsFlushTask implements TimerTask {
+ @Override
+ public void run(Timeout timeout) {
+ boolean flushSucceeded = false;
+ try {
+ int flushedBatches = 0;
+ do {
+ flushSucceeded = flushBufferedMetrics();
+ flushedBatches++;
+ } while (flushSucceeded
+ && hasPendingMetrics()
+ && !closed.get()
+ && flushedBatches < MAX_BATCHES_PER_FLUSH_TASK);
+ } catch (Exception e) {
+ log.error("[VictoriaMetrics] immediate flush task error: {}",
e.getMessage(), e);
+ } finally {
+ immediateFlushPending.set(false);
+ if (hasPendingMetrics() && !closed.get()) {
+ if (flushSucceeded) {
+ scheduleImmediateFlush(0, TimeUnit.MILLISECONDS);
+ } else {
+ scheduleImmediateFlush(FAILED_FLUSH_RETRY_SECONDS,
TimeUnit.SECONDS);
+ }
+ }
}
}
}
@@ -728,7 +824,7 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
/**
* Save metric data to victoria-metric via HTTP call
*/
- private void doSaveData(List<VictoriaMetricsContent> contentList) {
+ private boolean trySaveData(List<VictoriaMetricsContent> contentList) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -765,12 +861,15 @@ public class VictoriaMetricsDataStorage extends
AbstractHistoryDataStorage {
httpEntity, String.class);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("insert metrics data to victoria-metrics success.");
+ return true;
} else {
- log.error("insert metrics data to victoria-metrics failed.
{}", responseEntity.getBody());
+ log.error("insert metrics data to victoria-metrics failed with
status {}",
+ responseEntity.getStatusCode());
}
} catch (Exception e){
log.error("flush metrics data to victoria-metrics error: {}.",
e.getMessage(), e);
}
+ return false;
}
}
diff --git
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/controller/MetricsDataControllerTest.java
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/controller/MetricsDataControllerTest.java
index 674f2b5d33..ce26d541f7 100644
---
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/controller/MetricsDataControllerTest.java
+++
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/controller/MetricsDataControllerTest.java
@@ -30,6 +30,7 @@ import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
+import org.apache.hertzbeat.warehouse.service.WarehouseStorageStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -64,18 +65,24 @@ class MetricsDataControllerTest {
@Test
void getWarehouseStorageServerStatus() throws Exception {
-
when(metricsDataService.getWarehouseStorageServerStatus()).thenReturn(true);
+ when(metricsDataService.getWarehouseStorageStatus())
+ .thenReturn(new WarehouseStorageStatus(true, 0, 0));
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/warehouse/storage/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
- .andExpect(jsonPath("$.data").isEmpty())
+ .andExpect(jsonPath("$.data.available").value(true))
+ .andExpect(jsonPath("$.data.droppedMetrics").value(0))
+ .andExpect(jsonPath("$.data.pendingMetrics").value(0))
.andExpect(jsonPath("$.msg").isEmpty())
.andReturn();
-
when(metricsDataService.getWarehouseStorageServerStatus()).thenReturn(false);
+ when(metricsDataService.getWarehouseStorageStatus())
+ .thenReturn(new WarehouseStorageStatus(false, 7, 3));
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/warehouse/storage/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
- .andExpect(jsonPath("$.data").isEmpty())
+ .andExpect(jsonPath("$.data.available").value(false))
+ .andExpect(jsonPath("$.data.droppedMetrics").value(7))
+ .andExpect(jsonPath("$.data.pendingMetrics").value(3))
.andReturn();
}
diff --git
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/service/MetricsDataServiceTest.java
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/service/MetricsDataServiceTest.java
index b73646a9e9..bf8f88a834 100644
---
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/service/MetricsDataServiceTest.java
+++
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/service/MetricsDataServiceTest.java
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.warehouse.service;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -68,7 +69,10 @@ public class MetricsDataServiceTest {
assertFalse(metricsDataService.getWarehouseStorageServerStatus());
when(historyDataReader.isServerAvailable()).thenReturn(true);
+ when(historyDataReader.getDroppedMetricCount()).thenReturn(7L);
+ when(historyDataReader.getPendingMetricCount()).thenReturn(3);
assertTrue(metricsDataService.getWarehouseStorageServerStatus());
+ assertEquals(new WarehouseStorageStatus(true, 7, 3),
metricsDataService.getWarehouseStorageStatus());
}
@Test
diff --git
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorageTest.java
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorageTest.java
new file mode 100644
index 0000000000..960b28ea89
--- /dev/null
+++
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsClusterDataStorageTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.hertzbeat.warehouse.store.history.tsdb.vm;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+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 org.apache.hertzbeat.common.timer.TimerTask;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Test case for {@link VictoriaMetricsClusterDataStorage}.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class VictoriaMetricsClusterDataStorageTest {
+
+ @Mock
+ private RestTemplate restTemplate;
+
+ @Test
+ void flushesDataAddedWhileAnImmediateFlushIsRunning() throws Exception {
+ mockHealthCheck();
+ CountDownLatch firstWriteStarted = new CountDownLatch(1);
+ CountDownLatch releaseFirstWrite = new CountDownLatch(1);
+ List<String> successfulBodies = new CopyOnWriteArrayList<>();
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenAnswer(invocation -> {
+ HttpEntity<String> request = invocation.getArgument(1);
+ if (writes.getAndIncrement() == 0) {
+ firstWriteStarted.countDown();
+ assertThat(releaseFirstWrite.await(5,
TimeUnit.SECONDS)).isTrue();
+ }
+ successfulBodies.add(request.getBody());
+ return ResponseEntity.noContent().build();
+ });
+ VictoriaMetricsClusterDataStorage storage = createStorage(2, 3600);
+
+ try {
+ // Allow the constructor's initial empty periodic run to settle.
+ Thread.sleep(1200);
+ saveOneMetric(storage);
+ saveOneMetric(storage);
+ assertThat(firstWriteStarted.await(5, TimeUnit.SECONDS)).isTrue();
+
+ saveOneMetric(storage);
+ saveOneMetric(storage);
+ releaseFirstWrite.countDown();
+
+ await().atMost(5, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(successfulBodies).hasSize(2));
+
assertThat(successfulBodies.stream().mapToLong(VictoriaMetricsClusterDataStorageTest::lineCount).sum())
+ .isEqualTo(4);
+ } finally {
+ releaseFirstWrite.countDown();
+ storage.destroy();
+ }
+ }
+
+ @Test
+ void clusterFlushClaimsTheRetryBatchWhileTheHttpWriteIsInFlight() throws
Exception {
+ mockHealthCheck();
+ CountDownLatch firstWriteStarted = new CountDownLatch(1);
+ CountDownLatch releaseFirstWrite = new CountDownLatch(1);
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenAnswer(invocation -> {
+ if (writes.incrementAndGet() == 1) {
+ firstWriteStarted.countDown();
+ assertThat(releaseFirstWrite.await(5,
TimeUnit.SECONDS)).isTrue();
+ }
+ return ResponseEntity.noContent().build();
+ });
+ VictoriaMetricsClusterDataStorage storage = createStorage(10, 3600);
+ ExecutorService flushers = Executors.newFixedThreadPool(2);
+
+ try {
+ Thread.sleep(1200);
+ saveOneMetric(storage);
+ Future<Boolean> first = flushers.submit(() ->
+ ReflectionTestUtils.invokeMethod(storage,
"flushBufferedMetrics"));
+ assertThat(firstWriteStarted.await(5, TimeUnit.SECONDS)).isTrue();
+ Future<Boolean> second = flushers.submit(() ->
+ ReflectionTestUtils.invokeMethod(storage,
"flushBufferedMetrics"));
+
+ assertThat(second.get(2, TimeUnit.SECONDS)).isFalse();
+ assertThat(writes).hasValue(1);
+ releaseFirstWrite.countDown();
+ assertThat(first.get(2, TimeUnit.SECONDS)).isTrue();
+ assertThat(writes).hasValue(1);
+ } finally {
+ releaseFirstWrite.countDown();
+ storage.destroy();
+ flushers.shutdownNow();
+ }
+ }
+
+ @Test
+ void retriesPeriodicFlushFailuresQuicklyWhenTheConfiguredIntervalIsLong()
throws Exception {
+ mockHealthCheck();
+ List<String> attemptedBodies = new CopyOnWriteArrayList<>();
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenAnswer(invocation -> {
+ HttpEntity<String> request = invocation.getArgument(1);
+ attemptedBodies.add(request.getBody());
+ if (writes.getAndIncrement() == 0) {
+ return
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
+ }
+ return ResponseEntity.noContent().build();
+ });
+ VictoriaMetricsClusterDataStorage storage = createStorage(10, 3600);
+
+ try {
+ // Let the constructor's initial empty periodic run schedule the
+ // production-length interval, then invoke that periodic path.
+ Thread.sleep(1200);
+ saveOneMetric(storage);
+ TimerTask periodicTask = (TimerTask)
ReflectionTestUtils.getField(storage, "metricsFlushtask");
+ assertThat(periodicTask).isNotNull();
+ periodicTask.run(null);
+
+ await().atMost(4, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(attemptedBodies).hasSizeGreaterThanOrEqualTo(2));
+
assertThat(attemptedBodies.get(1)).isEqualTo(attemptedBodies.get(0));
+ assertThat(lineCount(attemptedBodies.get(1))).isEqualTo(1);
+ } finally {
+ storage.destroy();
+ }
+ }
+
+ @Test
+ void destroyFlushesBufferedMetricsAndRejectsLaterWrites() {
+ mockHealthCheck();
+ List<String> successfulBodies = new CopyOnWriteArrayList<>();
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenAnswer(invocation -> {
+ HttpEntity<String> request = invocation.getArgument(1);
+ successfulBodies.add(request.getBody());
+ return ResponseEntity.noContent().build();
+ });
+ VictoriaMetricsClusterDataStorage storage = createStorage(10, 3600);
+
+ saveOneMetric(storage);
+ storage.destroy();
+ saveOneMetric(storage);
+
+ assertThat(successfulBodies).hasSize(1);
+ assertThat(lineCount(successfulBodies.get(0))).isEqualTo(1);
+ }
+
+ @Test
+ void persistentWriteFailureDoesNotBlockTheWarehouseProducerIndefinitely()
throws Exception {
+ mockHealthCheck();
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(anyString(), any(HttpEntity.class),
eq(String.class)))
+ .thenAnswer(invocation -> {
+ writes.incrementAndGet();
+ return
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
+ });
+ VictoriaMetricsClusterDataStorage storage = createStorage(1, 3600);
+ ExecutorService producer = Executors.newSingleThreadExecutor();
+ Future<?> pendingWrite = null;
+
+ try {
+ Thread.sleep(1200);
+ saveOneMetric(storage);
+ await().atMost(3, TimeUnit.SECONDS).until(() -> writes.get() > 0);
+ saveOneMetric(storage);
+
+ pendingWrite = producer.submit(() -> saveOneMetric(storage));
+
+ pendingWrite.get(3, TimeUnit.SECONDS);
+
assertThat(storage.getDroppedMetricCount()).isGreaterThanOrEqualTo(1);
+ } finally {
+ if (pendingWrite != null) {
+ pendingWrite.cancel(true);
+ }
+ storage.destroy();
+ producer.shutdownNow();
+ }
+ }
+
+ private void mockHealthCheck() {
+ when(restTemplate.exchange(
+ anyString(),
+ eq(HttpMethod.GET),
+ any(HttpEntity.class),
+ eq(String.class)))
+ .thenReturn(ResponseEntity.ok("{\"status\":\"success\"}"));
+ }
+
+ private VictoriaMetricsClusterDataStorage createStorage(int bufferSize,
int flushInterval) {
+ VictoriaMetricsInsertProperties insert =
+ new VictoriaMetricsInsertProperties("http://localhost:8480",
null, null, bufferSize, flushInterval);
+ VictoriaMetricsSelectProperties select =
+ new VictoriaMetricsSelectProperties("http://localhost:8481",
null, null);
+ VictoriaMetricsClusterProperties properties =
+ new VictoriaMetricsClusterProperties(true, "0", insert,
select);
+ return new VictoriaMetricsClusterDataStorage(properties, restTemplate);
+ }
+
+ private static void saveOneMetric(VictoriaMetricsClusterDataStorage
storage) {
+
storage.saveData(VictoriaMetricsDataStorageTest.generateMockedMetricsData());
+ }
+
+ private static long lineCount(String body) {
+ return body.lines().filter(line -> !line.isBlank()).count();
+ }
+}
diff --git
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
index dd0ac7d9f3..31528e84cc 100644
---
a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
+++
b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/store/history/tsdb/vm/VictoriaMetricsDataStorageTest.java
@@ -55,10 +55,17 @@ import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+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.AtomicReference;
+import org.springframework.test.util.ReflectionTestUtils;
+
/**
* Test case for {@link VictoriaMetricsDataStorage}
*/
@@ -194,6 +201,110 @@ class VictoriaMetricsDataStorageTest {
.isGreaterThanOrEqualTo(threadCount *
writeSize / bufferSize));
}
+ @Test
+ void failedSingleNodeFlushRetainsTheBatchAndRetriesQuickly() {
+ when(victoriaMetricsProperties.insert()).thenReturn(new
VictoriaMetricsProperties.InsertConfig(
+ 10, 1, new VictoriaMetricsProperties.Compression(false)));
+ List<String> attemptedBodies = new CopyOnWriteArrayList<>();
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(
+ startsWith(victoriaMetricsProperties.url()),
+ any(HttpEntity.class),
+ eq(String.class)
+ )).thenAnswer(invocation -> {
+ HttpEntity<String> request = invocation.getArgument(1);
+ attemptedBodies.add(request.getBody());
+ if (writes.getAndIncrement() == 0) {
+ return
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
+ }
+ return ResponseEntity.noContent().build();
+ });
+ victoriaMetricsDataStorage = new
VictoriaMetricsDataStorage(victoriaMetricsProperties, restTemplate);
+
+ victoriaMetricsDataStorage.saveData(generateMockedMetricsData());
+
+ Awaitility.await()
+ .pollInterval(250, TimeUnit.MILLISECONDS)
+ .atMost(7, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(attemptedBodies).hasSizeGreaterThanOrEqualTo(2));
+ assertThat(attemptedBodies.get(1)).isEqualTo(attemptedBodies.get(0));
+ }
+
+ @Test
+ void singleNodeFlushClaimsTheRetryBatchWhileTheHttpWriteIsInFlight()
throws Exception {
+ when(victoriaMetricsProperties.insert()).thenReturn(new
VictoriaMetricsProperties.InsertConfig(
+ 10, 3600, new VictoriaMetricsProperties.Compression(false)));
+ CountDownLatch firstWriteStarted = new CountDownLatch(1);
+ CountDownLatch releaseFirstWrite = new CountDownLatch(1);
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(
+ startsWith(victoriaMetricsProperties.url()),
+ any(HttpEntity.class),
+ eq(String.class)
+ )).thenAnswer(invocation -> {
+ if (writes.incrementAndGet() == 1) {
+ firstWriteStarted.countDown();
+ assertThat(releaseFirstWrite.await(5,
TimeUnit.SECONDS)).isTrue();
+ }
+ return ResponseEntity.noContent().build();
+ });
+ victoriaMetricsDataStorage = new
VictoriaMetricsDataStorage(victoriaMetricsProperties, restTemplate);
+ victoriaMetricsDataStorage.saveData(generateMockedMetricsData());
+ ExecutorService flushers = Executors.newFixedThreadPool(2);
+
+ try {
+ Future<Boolean> first = flushers.submit(() ->
+
ReflectionTestUtils.invokeMethod(victoriaMetricsDataStorage,
"flushBufferedMetrics"));
+ assertThat(firstWriteStarted.await(5, TimeUnit.SECONDS)).isTrue();
+ Future<Boolean> second = flushers.submit(() ->
+
ReflectionTestUtils.invokeMethod(victoriaMetricsDataStorage,
"flushBufferedMetrics"));
+
+ assertThat(second.get(2, TimeUnit.SECONDS)).isFalse();
+ assertThat(writes).hasValue(1);
+ releaseFirstWrite.countDown();
+ assertThat(first.get(2, TimeUnit.SECONDS)).isTrue();
+ assertThat(writes).hasValue(1);
+ } finally {
+ releaseFirstWrite.countDown();
+ flushers.shutdownNow();
+ }
+ }
+
+ @Test
+ void
persistentSingleNodeFailureDoesNotBlockTheWarehouseProducerIndefinitely()
throws Exception {
+ when(victoriaMetricsProperties.insert()).thenReturn(new
VictoriaMetricsProperties.InsertConfig(
+ 1, 3600, new VictoriaMetricsProperties.Compression(false)));
+ AtomicInteger writes = new AtomicInteger();
+ when(restTemplate.postForEntity(
+ startsWith(victoriaMetricsProperties.url()),
+ any(HttpEntity.class),
+ eq(String.class)
+ )).thenAnswer(invocation -> {
+ writes.incrementAndGet();
+ return
ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
+ });
+ victoriaMetricsDataStorage = new
VictoriaMetricsDataStorage(victoriaMetricsProperties, restTemplate);
+ ExecutorService producer = Executors.newSingleThreadExecutor();
+ Future<?> pendingWrite = null;
+
+ try {
+ victoriaMetricsDataStorage.saveData(generateMockedMetricsData());
+ Awaitility.await().atMost(3, TimeUnit.SECONDS).until(() ->
writes.get() > 0);
+ victoriaMetricsDataStorage.saveData(generateMockedMetricsData());
+
+ pendingWrite = producer.submit(
+ () ->
victoriaMetricsDataStorage.saveData(generateMockedMetricsData()));
+
+ pendingWrite.get(3, TimeUnit.SECONDS);
+
assertThat(victoriaMetricsDataStorage.getDroppedMetricCount()).isGreaterThanOrEqualTo(1);
+ } finally {
+ if (pendingWrite != null) {
+ pendingWrite.cancel(true);
+ }
+ producer.shutdownNow();
+ }
+ }
+
@Test
void customLabelsKeepJobButCannotOverrideStorageInstance() {
when(victoriaMetricsProperties.insert()).thenReturn(new
VictoriaMetricsProperties.InsertConfig(
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]