[feat]: migrate blocking executors to virtual threads (#4062)

This commit is contained in:
Logic
2026-03-12 00:36:29 +08:00
committed by GitHub
parent d99e567494
commit af56746de1
170 changed files with 7607 additions and 1429 deletions
@@ -21,6 +21,7 @@ import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
@@ -32,7 +33,6 @@ import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
@@ -280,7 +280,9 @@ public class MonitorToolsImpl implements MonitorTools {
// Validate that all required parameters for this monitor type are provided
try {
MonitorDto monitorDto = MonitorDto.builder().monitor(monitor).params(paramList).build();
MonitorDto monitorDto = new MonitorDto();
monitorDto.setMonitor(monitor);
monitorDto.setParams(paramList);
monitorService.validate(monitorDto, false);
} catch (IllegalArgumentException argumentException) {
if (argumentException.getMessage().contains("required")) {
@@ -456,7 +458,7 @@ public class MonitorToolsImpl implements MonitorTools {
}
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
List<ParamDefineInfo> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
@@ -468,7 +470,7 @@ public class MonitorToolsImpl implements MonitorTools {
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
app, paramDefines.size()));
for (ParamDefine paramDefine : paramDefines) {
for (ParamDefineInfo paramDefine : paramDefines) {
response.append("• Field: ").append(paramDefine.getField()).append("\n");
// Add display name if available
+5
View File
@@ -40,6 +40,11 @@
<artifactId>hertzbeat-common-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common-spring</artifactId>
<scope>provided</scope>
</dependency>
<!-- plugin -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
@@ -18,12 +18,20 @@
package org.apache.hertzbeat.alert;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -31,16 +39,25 @@ import org.springframework.stereotype.Component;
*/
@Component
@Slf4j
public class AlerterWorkerPool {
public class AlerterWorkerPool implements DisposableBean {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
private ManagedExecutor notifyExecutor;
private ManagedExecutor logWorkerExecutor;
private Map<Byte, Semaphore> notifyChannelPermits;
private int notifyMaxConcurrentPerChannel;
public AlerterWorkerPool() {
this(VirtualThreadProperties.defaults());
}
@Autowired
public AlerterWorkerPool(VirtualThreadProperties virtualThreadProperties) {
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
initWorkExecutor();
initNotifyExecutor();
initLogWorkerExecutor();
initNotifyExecutor(properties);
initLogWorkerExecutor(properties);
}
private void initWorkExecutor() {
@@ -61,16 +78,32 @@ public class AlerterWorkerPool {
new ThreadPoolExecutor.AbortPolicy());
}
private void initNotifyExecutor() {
private void initNotifyExecutor(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("Alerter notifyExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.AlerterProperties alerterProperties = properties.alerter();
VirtualThreadProperties.PoolProperties notifyProperties = alerterProperties.notifyPool();
notifyMaxConcurrentPerChannel = Math.max(1, alerterProperties.notifyMaxConcurrentPerChannel());
notifyChannelPermits = new ConcurrentHashMap<>(8);
notifyExecutor = ManagedExecutors.newVirtualExecutor("notify-worker", "notify-worker-",
notifyProperties.mode(), notifyProperties.maxConcurrentJobs(), handler);
return;
}
notifyMaxConcurrentPerChannel = 0;
notifyChannelPermits = null;
notifyExecutor = ManagedExecutors.wrap("notify-worker", createLegacyNotifyExecutor(handler));
}
private ThreadPoolExecutor createLegacyNotifyExecutor(Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter notifyExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("notify-worker-%d")
.build();
notifyExecutor = new ThreadPoolExecutor(6,
return new ThreadPoolExecutor(6,
6,
10,
TimeUnit.SECONDS,
@@ -79,16 +112,27 @@ public class AlerterWorkerPool {
new ThreadPoolExecutor.AbortPolicy());
}
private void initLogWorkerExecutor() {
private void initLogWorkerExecutor(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.QueueProperties logWorkerProperties = properties.alerter().logWorker();
logWorkerExecutor = ManagedExecutors.newQueuedVirtualExecutor("alerter-log-worker", "log-worker-",
logWorkerProperties.maxConcurrentJobs(), logWorkerProperties.queueCapacity(), handler);
return;
}
logWorkerExecutor = ManagedExecutors.wrap("alerter-log-worker", createLegacyLogWorkerExecutor(handler));
}
private ThreadPoolExecutor createLegacyLogWorkerExecutor(Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
return new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
@@ -113,6 +157,41 @@ public class AlerterWorkerPool {
notifyExecutor.execute(runnable);
}
/**
* Executes the given runnable task using the notify executor with per-channel concurrency control.
*
* @param channelType notification channel type
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeNotify(byte channelType, Runnable runnable) throws RejectedExecutionException {
if (notifyChannelPermits == null) {
notifyExecutor.execute(runnable);
return;
}
Semaphore semaphore = notifyChannelPermits.computeIfAbsent(channelType,
key -> new Semaphore(notifyMaxConcurrentPerChannel));
if (!semaphore.tryAcquire()) {
throw new RejectedExecutionException(
"notify-worker rejected task because channel concurrency limit was reached for type " + channelType);
}
boolean submitted = false;
try {
notifyExecutor.execute(() -> {
try {
runnable.run();
} finally {
semaphore.release();
}
});
submitted = true;
} finally {
if (!submitted) {
semaphore.release();
}
}
}
/**
* Executes the given runnable task using the logWorkerExecutor.
*
@@ -122,4 +201,11 @@ public class AlerterWorkerPool {
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
logWorkerExecutor.execute(runnable);
}
@Override
public void destroy() {
workerExecutor.shutdownNow();
notifyExecutor.close();
logWorkerExecutor.close();
}
}
@@ -25,12 +25,19 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@@ -40,18 +47,33 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
*/
@Slf4j
@Component
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
public class PeriodicAlertRuleScheduler implements CommandLineRunner, DisposableBean {
private final MetricsPeriodicAlertCalculator metricsCalculator;
private final LogPeriodicAlertCalculator logCalculator;
private final AlertDefineDao alertDefineDao;
private final ScheduledExecutorService scheduledExecutor;
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
private final ExecutorService periodicExecutor;
private final Semaphore periodicPermits;
private final boolean virtualThreadsEnabled;
private final Map<Long, ScheduledTaskState> scheduledTasks;
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
this(metricsCalculator, logCalculator, alertDefineDao, VirtualThreadProperties.defaults());
}
@Autowired
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator,
LogPeriodicAlertCalculator logCalculator,
AlertDefineDao alertDefineDao,
VirtualThreadProperties virtualThreadProperties) {
this.metricsCalculator = metricsCalculator;
this.logCalculator = logCalculator;
this.alertDefineDao = alertDefineDao;
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("Scheduled periodic alert threshold has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Scheduled periodic alert threshold has uncaughtException.");
@@ -61,17 +83,27 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
.setNameFormat("periodic-alert-threshold-worker-%d")
.build();
this.scheduledExecutor = Executors.newScheduledThreadPool(10, threadFactory);
this.scheduledFutures = new ConcurrentHashMap<>();
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.virtualThreadsEnabled = properties.enabled();
int maxConcurrentPeriodicTasks = Math.max(1, properties.alerter().periodicMaxConcurrentJobs());
this.periodicExecutor = virtualThreadsEnabled
? Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("periodic-alert-task-", 0)
.uncaughtExceptionHandler(handler)
.factory())
: null;
this.periodicPermits = virtualThreadsEnabled ? new Semaphore(maxConcurrentPeriodicTasks) : null;
this.scheduledTasks = new ConcurrentHashMap<>();
}
public void cancelSchedule(Long ruleId) {
if (ruleId == null) {
return;
}
ScheduledFuture<?> future = scheduledFutures.get(ruleId);
if (future != null) {
future.cancel(true);
scheduledFutures.remove(ruleId);
ScheduledTaskState state = scheduledTasks.remove(ruleId);
if (state != null) {
state.cancel();
}
}
@@ -83,14 +115,12 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
cancelSchedule(rule.getId());
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
metricsCalculator.calculate(rule);
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
logCalculator.calculate(rule);
}
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
scheduledFutures.put(rule.getId(), future);
ScheduledTaskState state = new ScheduledTaskState(rule);
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(
virtualThreadsEnabled ? state::trigger : () -> executeRule(rule),
0, rule.getPeriod(), TimeUnit.SECONDS);
state.setScheduledFuture(future);
scheduledTasks.put(rule.getId(), state);
}
}
@@ -106,4 +136,107 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
updateSchedule(rule);
}
}
@Override
public void destroy() {
scheduledTasks.values().forEach(ScheduledTaskState::cancel);
scheduledTasks.clear();
scheduledExecutor.shutdownNow();
if (periodicExecutor != null) {
periodicExecutor.shutdownNow();
}
}
private void executeRule(AlertDefine rule) {
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
metricsCalculator.calculate(rule);
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
logCalculator.calculate(rule);
}
}
private final class ScheduledTaskState {
private final AlertDefine rule;
private ScheduledFuture<?> scheduledFuture;
private Future<?> runningFuture;
private boolean running;
private boolean pending;
private boolean cancelled;
private ScheduledTaskState(AlertDefine rule) {
this.rule = rule;
}
private synchronized void setScheduledFuture(ScheduledFuture<?> scheduledFuture) {
this.scheduledFuture = scheduledFuture;
}
private synchronized void trigger() {
if (cancelled) {
return;
}
if (running) {
pending = true;
return;
}
running = true;
submitLocked();
}
private synchronized void cancel() {
cancelled = true;
pending = false;
ScheduledFuture<?> periodicFuture = scheduledFuture;
Future<?> currentFuture = runningFuture;
if (periodicFuture != null) {
periodicFuture.cancel(true);
}
if (currentFuture != null) {
currentFuture.cancel(true);
}
}
private void submitLocked() {
try {
runningFuture = periodicExecutor.submit(() -> {
boolean permitAcquired = false;
try {
periodicPermits.acquire();
permitAcquired = true;
if (!Thread.currentThread().isInterrupted()) {
executeRule(rule);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("Periodic alert rule {} execution error: {}", rule.getName(), e.getMessage(), e);
} finally {
if (permitAcquired) {
periodicPermits.release();
}
onComplete();
}
});
} catch (RuntimeException e) {
running = false;
throw e;
}
}
private synchronized void onComplete() {
runningFuture = null;
if (cancelled) {
running = false;
pending = false;
return;
}
if (!pending) {
running = false;
return;
}
pending = false;
submitLocked();
}
}
}
@@ -20,6 +20,11 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
@@ -32,10 +37,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Alarm Evaluator - Final alarm logic trigger
@@ -47,36 +48,48 @@ import java.util.concurrent.TimeUnit;
*/
@Component
@Slf4j
public class AlarmEvaluator {
public class AlarmEvaluator implements DisposableBean {
private static final String WINDOW_START_TIME = "window_start_time";
private static final String WINDOW_END_TIME = "window_end_time";
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
private final AlarmCommonReduce alarmCommonReduce;
private ThreadPoolExecutor workerExecutor;
private final ManagedExecutor workerExecutor;
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
initAlarmEvaluator();
this(alarmCommonReduce, VirtualThreadProperties.defaults());
}
public void initAlarmEvaluator() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(2,
@Autowired
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce, VirtualThreadProperties virtualThreadProperties) {
this.alarmCommonReduce = alarmCommonReduce;
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.workerExecutor = initAlarmEvaluator(properties);
}
public ManagedExecutor initAlarmEvaluator(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().windowEvaluator();
return ManagedExecutors.newQueuedVirtualExecutor("alerter-window-evaluator", "alerter-window-evaluator-",
queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler);
}
return ManagedExecutors.wrap("alerter-window-evaluator", new java.util.concurrent.ThreadPoolExecutor(2,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
java.util.concurrent.TimeUnit.SECONDS,
new java.util.concurrent.LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build(),
new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
}
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
@@ -314,4 +327,9 @@ public class AlarmEvaluator {
}
}
}
}
@Override
public void destroy() {
workerExecutor.close();
}
}
@@ -21,6 +21,7 @@ import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.RejectedExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.config.AlertSseManager;
@@ -121,14 +122,27 @@ public class AlertNoticeDispatch {
}
private void sendNotify(GroupAlert alert) {
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> workerPool.executeNotify(() -> rule.getReceiverId()
.forEach(receiverId -> {
try {
sendNoticeMsg(getOneReceiverById(receiverId),
getOneTemplateById(rule.getTemplateId()), alert);
} catch (AlertNoticeException e) {
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
}
}))));
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> {
NoticeTemplate noticeTemplate = getOneTemplateById(rule.getTemplateId());
rule.getReceiverId().forEach(receiverId -> {
NoticeReceiver receiver = getOneReceiverById(receiverId);
if (receiver == null || receiver.getType() == null) {
log.warn("DispatchTask skip invalid receiver, receiverId: {}, alertId: {}", receiverId, alert.getId());
return;
}
try {
workerPool.executeNotify(receiver.getType(), () -> {
try {
sendNoticeMsg(receiver, noticeTemplate, alert);
} catch (AlertNoticeException e) {
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
}
});
} catch (RejectedExecutionException e) {
log.warn("DispatchTask rejected notify task, receiverId: {}, type: {}, message: {}",
receiverId, receiver.getType(), e.getMessage());
}
});
}));
}
}
@@ -18,16 +18,16 @@
package org.apache.hertzbeat.alert.reduce;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
@@ -35,33 +35,45 @@ import org.springframework.stereotype.Service;
*/
@Service
@Slf4j
public class AlarmCommonReduce {
public class AlarmCommonReduce implements DisposableBean {
private final AlarmGroupReduce alarmGroupReduce;
private ThreadPoolExecutor workerExecutor;
private final ManagedExecutor workerExecutor;
public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce) {
initWorkExecutor();
this.alarmGroupReduce = alarmGroupReduce;
this(alarmGroupReduce, VirtualThreadProperties.defaults());
}
private void initWorkExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(2,
@Autowired
public AlarmCommonReduce(AlarmGroupReduce alarmGroupReduce, VirtualThreadProperties virtualThreadProperties) {
this.alarmGroupReduce = alarmGroupReduce;
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.workerExecutor = initWorkExecutor(properties);
}
private ManagedExecutor initWorkExecutor(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().reduce();
return ManagedExecutors.newQueuedVirtualExecutor("alerter-reduce-worker", "alerter-reduce-worker-",
queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler);
}
return ManagedExecutors.wrap("alerter-reduce-worker", new java.util.concurrent.ThreadPoolExecutor(2,
2,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
java.util.concurrent.TimeUnit.SECONDS,
new java.util.concurrent.LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build(),
new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
}
@@ -103,10 +115,6 @@ public class AlarmCommonReduce {
* Fingerprint is based on labels excluding timestamp related fields
*/
private String generateAlertFingerprint(Map<String, String> labels) {
// Remove timestamp related fields
labels.remove("timestamp");
labels.remove("start_at");
labels.remove("active_at");
return labels.entrySet().stream()
.filter(e -> !"timestamp".equals(e.getKey())
&& !"starts_at".equals(e.getKey()) && !"actives_at".equals(e.getKey())
@@ -116,4 +124,9 @@ public class AlarmCommonReduce {
.map(e -> e.getKey() + ":" + e.getValue())
.collect(Collectors.joining(","));
}
@Override
public void destroy() {
workerExecutor.close();
}
}
@@ -26,17 +26,22 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -45,7 +50,7 @@ import org.springframework.stereotype.Component;
*/
@Component
@Slf4j
public class AlarmGroupReduce {
public class AlarmGroupReduce implements DisposableBean {
/**
* Default initial group wait time 30s
@@ -88,16 +93,60 @@ public class AlarmGroupReduce {
*/
private final Map<String, GroupAlertCache> groupCacheMap;
private final ScheduledExecutorService scheduledExecutor;
private final ExecutorService workerExecutor;
private final ScheduledDispatchTask checkTask;
public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao) {
this(alarmInhibitReduce, alertGroupConvergeDao, VirtualThreadProperties.defaults(), true);
}
@Autowired
public AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
VirtualThreadProperties virtualThreadProperties) {
this(alarmInhibitReduce, alertGroupConvergeDao, virtualThreadProperties, true);
}
AlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
VirtualThreadProperties virtualThreadProperties, boolean autoStart) {
this.alarmInhibitReduce = alarmInhibitReduce;
this.groupDefines = new ConcurrentHashMap<>(8);
this.groupCacheMap = new ConcurrentHashMap<>(8);
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.scheduledExecutor = createScheduler();
this.workerExecutor = createVirtualExecutor(properties);
this.checkTask = new ScheduledDispatchTask(workerExecutor, this::runCheckAndSendGroups);
List<AlertGroupConverge> groupConverges = alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue();
refreshGroupDefines(groupConverges);
startCheckAndSendGroups();
if (autoStart) {
startCheckAndSendGroups();
}
}
private void startCheckAndSendGroups() {
scheduledExecutor.scheduleAtFixedRate(this::dispatchCheckAndSendGroups, 10000, CHECK_INTERVAL,
TimeUnit.MILLISECONDS);
}
void dispatchCheckAndSendGroups() {
checkTask.dispatch();
}
void beforeCheckAndSendGroupsRun() {
}
@Override
public void destroy() {
scheduledExecutor.shutdownNow();
if (workerExecutor != null) {
workerExecutor.shutdownNow();
}
}
private ScheduledExecutorService createScheduler() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Check alarm groups calculate has uncaughtException.");
@@ -106,21 +155,36 @@ public class AlarmGroupReduce {
.setDaemon(true)
.setNameFormat("alarm-group-calculate-%d")
.build();
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
long now = System.currentTimeMillis();
groupCacheMap.forEach((groupKey, cache) -> {
if (shouldSendGroup(cache, now)) {
sendGroupAlert(cache);
cache.setLastSendTime(now);
cache.getAlertFingerprints().clear();
}
});
} catch (Exception e) {
log.error("Check alarm groups calculate has exception.: {}", e.getMessage(), e);
}
}, 10000, CHECK_INTERVAL, java.util.concurrent.TimeUnit.MILLISECONDS);
return Executors.newSingleThreadScheduledExecutor(threadFactory);
}
private ExecutorService createVirtualExecutor(VirtualThreadProperties properties) {
if (!properties.enabled()) {
return null;
}
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("alarm-group-calculate-vt-", 0)
.uncaughtExceptionHandler((thread, throwable) -> {
log.error("Check alarm groups calculate worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.factory());
}
private void runCheckAndSendGroups() {
beforeCheckAndSendGroupsRun();
try {
long now = System.currentTimeMillis();
groupCacheMap.forEach((groupKey, cache) -> {
if (shouldSendGroup(cache, now)) {
sendGroupAlert(cache);
cache.setLastSendTime(now);
cache.getAlertFingerprints().clear();
}
});
} catch (Exception e) {
log.error("Check alarm groups calculate has exception.: {}", e.getMessage(), e);
}
}
/**
@@ -339,4 +403,63 @@ public class AlarmGroupReduce {
private long lastSendTime;
private long lastRepeatTime;
}
private static final class ScheduledDispatchTask {
private final ExecutorService executor;
private final Runnable task;
private boolean running;
private int pendingRuns;
private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
this.executor = executor;
this.task = task;
}
private void dispatch() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns++;
shouldSchedule = !running;
if (shouldSchedule) {
running = true;
}
}
if (shouldSchedule) {
scheduleRun();
}
}
private void scheduleRun() {
if (executor != null) {
executor.execute(this::runOnce);
} else {
runOnce();
}
}
private void runOnce() {
try {
task.run();
} finally {
scheduleNextIfNeeded();
}
}
private void scheduleNextIfNeeded() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns = Math.max(0, pendingRuns - 1);
shouldSchedule = pendingRuns > 0;
if (!shouldSchedule) {
running = false;
return;
}
}
scheduleRun();
}
}
}
@@ -18,25 +18,28 @@
package org.apache.hertzbeat.alert.reduce;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.dao.AlertInhibitDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Collections;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.AllArgsConstructor;
@@ -46,7 +49,7 @@ import lombok.AllArgsConstructor;
*/
@Component
@Slf4j
public class AlarmInhibitReduce {
public class AlarmInhibitReduce implements DisposableBean {
/**
* Interval for checking and cleaning up expired source alerts
@@ -73,22 +76,69 @@ public class AlarmInhibitReduce {
/**
* Default TTL for source alerts (4 hours)
*/
private static long SOURCE_ALERT_TTL = 4 * 60 * 60 * 1000L;
private final long sourceAlertTtl;
private final ScheduledExecutorService cleanupScheduler;
private final ExecutorService cleanupExecutor;
private final ScheduledDispatchTask cleanupTask;
public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao
, AlerterProperties alerterProperties) {
this(alarmSilenceReduce, alertInhibitDao, alerterProperties, VirtualThreadProperties.defaults(), true);
}
@Autowired
public AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties) {
this(alarmSilenceReduce, alertInhibitDao, alerterProperties, virtualThreadProperties, true);
}
AlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
AlerterProperties alerterProperties, VirtualThreadProperties virtualThreadProperties,
boolean autoStart) {
this.alarmSilenceReduce = alarmSilenceReduce;
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
if (alerterProperties.getInhibit() != null && alerterProperties.getInhibit().getTtl() > 0) {
SOURCE_ALERT_TTL = alerterProperties.getInhibit().getTtl();
this.sourceAlertTtl = alerterProperties.getInhibit().getTtl();
} else {
this.sourceAlertTtl = 4 * 60 * 60 * 1000L;
}
inhibitRules = new ConcurrentHashMap<>(8);
sourceAlertCache = new ConcurrentHashMap<>(8);
this.cleanupScheduler = createCleanupScheduler();
this.cleanupExecutor = createCleanupExecutor(properties);
this.cleanupTask = new ScheduledDispatchTask(cleanupExecutor, this::runCleanupCache);
List<AlertInhibit> inhibits = alertInhibitDao.findAlertInhibitsByEnableIsTrue();
refreshInhibitRules(inhibits);
startScheduledCleanupCache();
if (autoStart) {
startScheduledCleanupCache();
}
}
private void startScheduledCleanupCache() {
cleanupScheduler.scheduleAtFixedRate(this::dispatchCleanupCache, CHECK_INTERVAL, CHECK_INTERVAL,
TimeUnit.MILLISECONDS);
}
void dispatchCleanupCache() {
cleanupTask.dispatch();
}
void beforeCleanupCacheRun() {
}
@Override
public void destroy() {
cleanupScheduler.shutdownNow();
if (cleanupExecutor != null) {
cleanupExecutor.shutdownNow();
}
}
private ScheduledExecutorService createCleanupScheduler() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Scheduled clean up inhibit cache has uncaughtException.");
@@ -97,17 +147,30 @@ public class AlarmInhibitReduce {
.setDaemon(true)
.setNameFormat("inhibit-clean-up-%d")
.build();
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
// Scheduled cleanup of all expired source alerts
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
sourceAlertCache.values().forEach(this::cleanupExpiredEntries);
// Remove empty rule caches
sourceAlertCache.entrySet().removeIf(entry -> entry.getValue().isEmpty());
} catch (Exception e) {
log.error("Error during scheduled cleanup", e);
}
}, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.MILLISECONDS);
return Executors.newSingleThreadScheduledExecutor(threadFactory);
}
private ExecutorService createCleanupExecutor(VirtualThreadProperties properties) {
if (!properties.enabled()) {
return null;
}
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("inhibit-clean-up-vt-", 0)
.uncaughtExceptionHandler((thread, throwable) -> {
log.error("Scheduled clean up inhibit cache worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.factory());
}
private void runCleanupCache() {
beforeCleanupCacheRun();
try {
sourceAlertCache.values().forEach(this::cleanupExpiredEntries);
sourceAlertCache.entrySet().removeIf(entry -> entry.getValue().isEmpty());
} catch (Exception e) {
log.error("Error during scheduled cleanup", e);
}
}
/**
@@ -264,7 +327,7 @@ public class AlarmInhibitReduce {
SourceAlertEntry entry = new SourceAlertEntry(
alert,
System.currentTimeMillis(),
System.currentTimeMillis() + SOURCE_ALERT_TTL
System.currentTimeMillis() + sourceAlertTtl
);
ruleCache.put(alert.getFingerprint(), entry);
cleanupExpiredEntries(ruleCache);
@@ -315,4 +378,63 @@ public class AlarmInhibitReduce {
private final long createTime;
private final long expiryTime;
}
private static final class ScheduledDispatchTask {
private final ExecutorService executor;
private final Runnable task;
private boolean running;
private int pendingRuns;
private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
this.executor = executor;
this.task = task;
}
private void dispatch() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns++;
shouldSchedule = !running;
if (shouldSchedule) {
running = true;
}
}
if (shouldSchedule) {
scheduleRun();
}
}
private void scheduleRun() {
if (executor != null) {
executor.execute(this::runOnce);
} else {
runOnce();
}
}
private void runOnce() {
try {
task.run();
} finally {
scheduleNextIfNeeded();
}
}
private void scheduleNextIfNeeded() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns = Math.max(0, pendingRuns - 1);
shouldSchedule = pendingRuns > 0;
if (!shouldSchedule) {
running = false;
return;
}
}
scheduleRun();
}
}
}
@@ -18,9 +18,18 @@
package org.apache.hertzbeat.alert;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
@@ -28,60 +37,178 @@ import org.junit.jupiter.api.Test;
*/
class AlerterWorkerPoolTest {
private static final int NUMBER_OF_THREADS = 10;
private AlerterWorkerPool pool;
private AtomicInteger counter;
private CountDownLatch latch;
private static final int NUMBER_OF_TASKS = 10;
@BeforeEach
void setUp() {
pool = new AlerterWorkerPool();
counter = new AtomicInteger();
latch = new CountDownLatch(NUMBER_OF_THREADS);
private AlerterWorkerPool pool;
@AfterEach
void tearDown() {
if (pool != null) {
pool.destroy();
}
}
@Test
void executeJob() throws InterruptedException {
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
pool = new AlerterWorkerPool();
AtomicInteger counter = new AtomicInteger();
CountDownLatch latch = new CountDownLatch(NUMBER_OF_TASKS);
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
pool.executeJob(() -> {
counter.incrementAndGet();
latch.countDown();
});
}
latch.await();
assertEquals(NUMBER_OF_THREADS, counter.get());
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertEquals(NUMBER_OF_TASKS, counter.get());
}
@Test
void executeNotify() throws InterruptedException {
counter = new AtomicInteger();
latch = new CountDownLatch(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
pool.executeNotify(() -> {
counter.incrementAndGet();
latch.countDown();
});
}
latch.await();
void executeNotifyRunsOnVirtualThread() throws Exception {
pool = new AlerterWorkerPool();
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
assertEquals(NUMBER_OF_THREADS, counter.get());
pool.executeNotify((byte) 1, () -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void executeNotifyRejectsWhenGlobalConcurrencyLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1),
10,
VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
VirtualThreadProperties.QueueProperties.reduceDefaults(),
VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
8),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
pool = new AlerterWorkerPool(properties);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
pool.executeNotify((byte) 1, () -> {
started.countDown();
try {
release.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(started.await(5, TimeUnit.SECONDS));
try {
assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 2, () -> {
}));
} finally {
release.countDown();
}
}
@Test
void executeNotifyRejectsWhenChannelLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 8),
10,
VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
VirtualThreadProperties.QueueProperties.reduceDefaults(),
VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
1),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
pool = new AlerterWorkerPool(properties);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
pool.executeNotify((byte) 1, () -> {
started.countDown();
try {
release.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(started.await(5, TimeUnit.SECONDS));
try {
assertThrows(RejectedExecutionException.class, () -> pool.executeNotify((byte) 1, () -> {
}));
} finally {
release.countDown();
}
}
@Test
void executeLogJob() throws InterruptedException {
counter = new AtomicInteger();
latch = new CountDownLatch(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
pool.executeLogJob(() -> {
counter.incrementAndGet();
latch.countDown();
});
}
latch.await();
pool = new AlerterWorkerPool();
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
assertEquals(NUMBER_OF_THREADS, counter.get());
pool.executeLogJob(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void executeLogJobRejectsWhenQueueCapacityReached() throws InterruptedException {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
10,
new VirtualThreadProperties.QueueProperties(1, 1),
VirtualThreadProperties.QueueProperties.reduceDefaults(),
VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
4),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
pool = new AlerterWorkerPool(properties);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
pool.executeLogJob(() -> {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
pool.executeLogJob(secondStarted::countDown);
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
try {
assertThrows(RejectedExecutionException.class, () -> pool.executeLogJob(() -> {
}));
} finally {
releaseFirst.countDown();
}
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
}
}
@@ -0,0 +1,213 @@
/*
* 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.alert.calculate.periodic;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Tests for {@link PeriodicAlertRuleScheduler}.
*/
@ExtendWith(MockitoExtension.class)
class PeriodicAlertRuleSchedulerTest {
@Mock
private MetricsPeriodicAlertCalculator metricsCalculator;
@Mock
private LogPeriodicAlertCalculator logCalculator;
@Mock
private AlertDefineDao alertDefineDao;
private PeriodicAlertRuleScheduler scheduler;
@BeforeEach
void setUp() {
scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, alertDefineDao,
VirtualThreadProperties.defaults());
}
@AfterEach
void tearDown() {
if (scheduler != null) {
scheduler.destroy();
}
}
@Test
void updateScheduleRunsPeriodicCalculationOnVirtualThread() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
doAnswer(invocation -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
return null;
}).when(metricsCalculator).calculate(any(AlertDefine.class));
AlertDefine rule = metricRule(1L);
scheduler.updateSchedule(rule);
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void updateScheduleKeepsSingleInFlightExecutionPerRule() throws InterruptedException {
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger concurrent = new AtomicInteger();
AtomicInteger maxConcurrent = new AtomicInteger();
AtomicInteger invocations = new AtomicInteger();
doAnswer(invocation -> {
int active = concurrent.incrementAndGet();
maxConcurrent.updateAndGet(current -> Math.max(current, active));
int count = invocations.incrementAndGet();
try {
if (count == 1) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (count == 2) {
secondStarted.countDown();
}
} finally {
concurrent.decrementAndGet();
}
return null;
}).when(metricsCalculator).calculate(any(AlertDefine.class));
scheduler.updateSchedule(metricRule(2L));
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
Thread.sleep(1200L);
assertEquals(1, maxConcurrent.get());
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
@Test
void cancelScheduleInterruptsRunningVirtualTask() throws InterruptedException {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch interrupted = new CountDownLatch(1);
doAnswer(invocation -> {
started.countDown();
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
interrupted.countDown();
Thread.currentThread().interrupt();
}
return null;
}).when(metricsCalculator).calculate(any(AlertDefine.class));
AlertDefine rule = metricRule(3L);
scheduler.updateSchedule(rule);
assertTrue(started.await(5, TimeUnit.SECONDS));
scheduler.cancelSchedule(rule.getId());
assertTrue(interrupted.await(5, TimeUnit.SECONDS));
}
@Test
void updateScheduleHonorsConfiguredGlobalPeriodicConcurrencyLimit() throws InterruptedException {
scheduler.destroy();
scheduler = new PeriodicAlertRuleScheduler(metricsCalculator, logCalculator, alertDefineDao,
periodicProperties(1));
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger concurrent = new AtomicInteger();
AtomicInteger maxConcurrent = new AtomicInteger();
doAnswer(invocation -> {
int active = concurrent.incrementAndGet();
maxConcurrent.updateAndGet(current -> Math.max(current, active));
AlertDefine rule = invocation.getArgument(0);
try {
if (rule.getId().equals(4L)) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (rule.getId().equals(5L)) {
secondStarted.countDown();
}
} finally {
concurrent.decrementAndGet();
}
return null;
}).when(metricsCalculator).calculate(any(AlertDefine.class));
scheduler.updateSchedule(metricRule(4L));
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
scheduler.updateSchedule(metricRule(5L));
Thread.sleep(200L);
assertEquals(1, maxConcurrent.get());
assertEquals(1L, secondStarted.getCount());
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
private AlertDefine metricRule(Long id) {
return AlertDefine.builder()
.id(id)
.name("periodic-rule-" + id)
.type(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
.period(1)
.enable(true)
.build();
}
private VirtualThreadProperties periodicProperties(int maxConcurrentJobs) {
return new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
maxConcurrentJobs,
VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
VirtualThreadProperties.QueueProperties.reduceDefaults(),
VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
4),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
}
}
@@ -19,9 +19,11 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,14 +34,20 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertAll;
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.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -98,6 +106,13 @@ class AlarmEvaluatorTest {
windowData.addMatchingLog(matchingEvent);
}
@AfterEach
void tearDown() {
if (alarmEvaluator != null) {
alarmEvaluator.destroy();
}
}
@Test
void testProcessWindowDataWithIndividualMode() throws InterruptedException {
// Given - alert define with individual mode
@@ -343,4 +358,104 @@ class AlarmEvaluatorTest {
assertEquals(2, alerts.size());
assertEquals(2, alerts.get(0).getTriggerTimes()); // Each alert should have trigger times = total count
}
@Test
void testSendAndProcessWindowDataRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
doAnswer(invocation -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
return null;
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
alarmEvaluator.sendAndProcessWindowData(windowData);
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void testSendAndProcessWindowDataQueuesWhenConcurrencyLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
10,
VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
VirtualThreadProperties.QueueProperties.reduceDefaults(),
new VirtualThreadProperties.QueueProperties(1, 0),
4),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
alarmEvaluator.destroy();
alarmEvaluator = new AlarmEvaluator(alarmCommonReduce, properties);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger invocationOrder = new AtomicInteger();
doAnswer(invocation -> {
int order = invocationOrder.incrementAndGet();
if (order == 1) {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else if (order == 2) {
secondStarted.countDown();
}
return null;
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
WindowAggregator.WindowData firstWindow = cloneWindowDataWithBody("first");
WindowAggregator.WindowData secondWindow = cloneWindowDataWithBody("second");
firstWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
secondWindow.getAlertDefine().setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
alarmEvaluator.sendAndProcessWindowData(firstWindow);
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
alarmEvaluator.sendAndProcessWindowData(secondWindow);
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
}
private WindowAggregator.WindowData cloneWindowDataWithBody(String body) {
LogEntry logEntry = LogEntry.builder()
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
.severityText("ERROR")
.body(body)
.build();
AlertDefine define = AlertDefine.builder()
.id(alertDefine.getId())
.name(alertDefine.getName())
.type(alertDefine.getType())
.expr(alertDefine.getExpr())
.times(alertDefine.getTimes())
.template(alertDefine.getTemplate())
.labels(alertDefine.getLabels())
.annotations(alertDefine.getAnnotations())
.enable(alertDefine.isEnable())
.build();
MatchingLogEvent event = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(define)
.eventTimestamp(System.currentTimeMillis())
.workerTimestamp(System.currentTimeMillis())
.build();
WindowAggregator.WindowData clonedWindowData = new WindowAggregator.WindowData(
new WindowAggregator.WindowKey(define.getId(), System.currentTimeMillis() - 60000, System.currentTimeMillis()),
define);
clonedWindowData.addMatchingLog(event);
return clonedWindowData;
}
}
@@ -19,7 +19,10 @@ package org.apache.hertzbeat.alert.notice;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -149,4 +152,32 @@ class AlertNoticeDispatchTest {
assertFalse(alertNoticeDispatch.sendNoticeMsg(receiver, null, alert));
}
@Test
void testDispatchAlarmUsesTypedNotifyExecution() {
NoticeTemplate template = new NoticeTemplate();
template.setId(1L);
template.setName("default-template");
when(alertStoreHandler.store(alert)).thenReturn(alert);
when(noticeConfigService.getReceiverFilterRule(alert)).thenReturn(Collections.singletonList(
org.apache.hertzbeat.common.entity.alerter.NoticeRule.builder()
.receiverId(Collections.singletonList(1L))
.templateId(1L)
.build()));
when(noticeConfigService.getReceiverById(1L)).thenReturn(receiver);
when(noticeConfigService.getOneTemplateById(1L)).thenReturn(template);
doNothing().when(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
doAnswer(invocation -> {
Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(workerPool).executeNotify(anyByte(), any(Runnable.class));
alertNoticeDispatch.dispatchAlarm(alert);
verify(workerPool).executeNotify(eq((byte) 1), any(Runnable.class));
verify(alertNotifyHandler).send(eq(receiver), eq(template), eq(alert));
verify(emitterManager).broadcast(any(String.class));
}
}
@@ -17,7 +17,20 @@
package org.apache.hertzbeat.alert.reduce;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -40,14 +53,86 @@ class AlarmCommonReduceTest {
@BeforeEach
void setUp() {
testAlert = SingleAlert.builder().build();
testAlert = SingleAlert.builder().labels(new HashMap<>(Map.of("alertname", "test"))).build();
alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce);
}
@AfterEach
void tearDown() {
if (alarmCommonReduce != null) {
alarmCommonReduce.destroy();
}
}
@Test
void testReduceAndSendAlarm() {
alarmCommonReduce.reduceAndSendAlarm(testAlert);
}
@Test
void testReduceAndSendAlarmRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
doAnswer(invocation -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
return null;
}).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class));
alarmCommonReduce.reduceAndSendAlarm(testAlert);
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void testReduceAndSendAlarmQueuesWhenConcurrencyLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
new VirtualThreadProperties.AlerterProperties(
VirtualThreadProperties.PoolProperties.alerterNotifyDefaults(),
10,
VirtualThreadProperties.QueueProperties.logWorkerDefaults(),
new VirtualThreadProperties.QueueProperties(1, 0),
VirtualThreadProperties.QueueProperties.windowEvaluatorDefaults(),
4),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
alarmCommonReduce.destroy();
alarmCommonReduce = new AlarmCommonReduce(alarmGroupReduce, properties);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger invocationOrder = new AtomicInteger();
doAnswer(invocation -> {
int order = invocationOrder.incrementAndGet();
if (order == 1) {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else if (order == 2) {
secondStarted.countDown();
}
return null;
}).when(alarmGroupReduce).processGroupAlert(any(SingleAlert.class));
alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder()
.labels(new HashMap<>(Map.of("name", "first"))).build());
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
alarmCommonReduce.reduceAndSendAlarm(SingleAlert.builder()
.labels(new HashMap<>(Map.of("name", "second"))).build());
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
}
}
@@ -38,6 +38,9 @@
package org.apache.hertzbeat.alert.reduce;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
@@ -47,9 +50,15 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
@@ -73,7 +82,15 @@ class AlarmGroupReduceTest {
MockitoAnnotations.openMocks(this);
when(alertGroupConvergeDao.findAlertGroupConvergesByEnableIsTrue())
.thenReturn(Collections.emptyList());
alarmGroupReduce = new AlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao);
alarmGroupReduce = new AlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
new VirtualThreadProperties(), false);
}
@AfterEach
void tearDown() {
if (alarmGroupReduce != null) {
alarmGroupReduce.destroy();
}
}
@Test
@@ -112,6 +129,42 @@ class AlarmGroupReduceTest {
verify(alarmInhibitReduce, never()).inhibitAlarm(any()); // Should not send immediately due to group wait
}
@Test
void dispatchCheckAndSendGroupsRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
alarmGroupReduce.destroy();
alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null);
alarmGroupReduce.dispatchCheckAndSendGroups();
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void dispatchCheckAndSendGroupsDoesNotRunConcurrently() throws Exception {
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger maxConcurrent = new AtomicInteger();
alarmGroupReduce.destroy();
alarmGroupReduce = new TestAlarmGroupReduce(alarmInhibitReduce, alertGroupConvergeDao,
new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted,
maxConcurrent, new AtomicInteger());
alarmGroupReduce.dispatchCheckAndSendGroups();
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
alarmGroupReduce.dispatchCheckAndSendGroups();
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
private Map<String, String> createLabels(String... keyValues) {
Map<String, String> labels = new HashMap<>();
for (int i = 0; i < keyValues.length; i += 2) {
@@ -119,4 +172,67 @@ class AlarmGroupReduceTest {
}
return labels;
}
private static final class TestAlarmGroupReduce extends AlarmGroupReduce {
private final CountDownLatch virtualThreadLatch;
private final AtomicBoolean virtualThread;
private final CountDownLatch firstStarted;
private final CountDownLatch releaseFirst;
private final CountDownLatch secondStarted;
private final AtomicInteger maxConcurrent;
private final AtomicInteger concurrent;
private final AtomicInteger invocations;
private TestAlarmGroupReduce(AlarmInhibitReduce alarmInhibitReduce, AlertGroupConvergeDao alertGroupConvergeDao,
VirtualThreadProperties properties, CountDownLatch virtualThreadLatch,
AtomicBoolean virtualThread, CountDownLatch firstStarted,
CountDownLatch releaseFirst, CountDownLatch secondStarted,
AtomicInteger maxConcurrent, AtomicInteger invocations) {
super(alarmInhibitReduce, alertGroupConvergeDao, properties, false);
this.virtualThreadLatch = virtualThreadLatch;
this.virtualThread = virtualThread;
this.firstStarted = firstStarted;
this.releaseFirst = releaseFirst;
this.secondStarted = secondStarted;
this.maxConcurrent = maxConcurrent;
this.invocations = invocations;
this.concurrent = maxConcurrent == null ? null : new AtomicInteger();
}
@Override
void beforeCheckAndSendGroupsRun() {
if (virtualThread != null) {
virtualThread.set(Thread.currentThread().isVirtual());
}
if (virtualThreadLatch != null) {
virtualThreadLatch.countDown();
}
if (maxConcurrent == null || invocations == null) {
return;
}
int running = concurrent.incrementAndGet();
maxConcurrent.accumulateAndGet(running, Math::max);
int currentInvocation = invocations.incrementAndGet();
try {
if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (currentInvocation == 2 && secondStarted != null) {
secondStarted.countDown();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
concurrent.decrementAndGet();
}
}
}
}
@@ -39,6 +39,7 @@
package org.apache.hertzbeat.alert.reduce;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -49,14 +50,19 @@ import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.dao.AlertInhibitDao;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.alerter.AlertInhibit;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
@@ -89,7 +95,15 @@ class AlarmInhibitReduceTest {
inhibitProperties.setTtl(60000);
when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties);
alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
new VirtualThreadProperties(), false);
}
@AfterEach
void tearDown() {
if (alarmInhibitReduce != null) {
alarmInhibitReduce.destroy();
}
}
@Test
@@ -290,7 +304,9 @@ class AlarmInhibitReduceTest {
AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
inhibitProperties.setTtl(100);
when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties);
alarmInhibitReduce.destroy();
alarmInhibitReduce = new AlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
new VirtualThreadProperties(), false);
AlertInhibit rule = AlertInhibit.builder()
.id(1L)
@@ -324,6 +340,42 @@ class AlarmInhibitReduceTest {
verify(alarmSilenceReduce).silenceAlarm(targetGroupAlert);
}
@Test
void dispatchCleanupCacheRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
alarmInhibitReduce.destroy();
alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
new VirtualThreadProperties(), latch, virtualThread, null, null, null, null, null);
alarmInhibitReduce.dispatchCleanupCache();
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void dispatchCleanupCacheDoesNotRunConcurrently() throws Exception {
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger maxConcurrent = new AtomicInteger();
alarmInhibitReduce.destroy();
alarmInhibitReduce = new TestAlarmInhibitReduce(alarmSilenceReduce, alertInhibitDao, alerterProperties,
new VirtualThreadProperties(), null, null, firstStarted, releaseFirst, secondStarted,
maxConcurrent, new AtomicInteger());
alarmInhibitReduce.dispatchCleanupCache();
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
alarmInhibitReduce.dispatchCleanupCache();
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
private GroupAlert createGroupAlert(String status, Map<String, String> labels, List<SingleAlert> alerts) {
return GroupAlert.builder()
.status(status)
@@ -347,4 +399,68 @@ class AlarmInhibitReduceTest {
.labels(labels)
.build();
}
}
private static final class TestAlarmInhibitReduce extends AlarmInhibitReduce {
private final CountDownLatch virtualThreadLatch;
private final AtomicBoolean virtualThread;
private final CountDownLatch firstStarted;
private final CountDownLatch releaseFirst;
private final CountDownLatch secondStarted;
private final AtomicInteger maxConcurrent;
private final AtomicInteger concurrent;
private final AtomicInteger invocations;
private TestAlarmInhibitReduce(AlarmSilenceReduce alarmSilenceReduce, AlertInhibitDao alertInhibitDao,
AlerterProperties alerterProperties, VirtualThreadProperties properties,
CountDownLatch virtualThreadLatch, AtomicBoolean virtualThread,
CountDownLatch firstStarted, CountDownLatch releaseFirst,
CountDownLatch secondStarted, AtomicInteger maxConcurrent,
AtomicInteger invocations) {
super(alarmSilenceReduce, alertInhibitDao, alerterProperties, properties, false);
this.virtualThreadLatch = virtualThreadLatch;
this.virtualThread = virtualThread;
this.firstStarted = firstStarted;
this.releaseFirst = releaseFirst;
this.secondStarted = secondStarted;
this.maxConcurrent = maxConcurrent;
this.invocations = invocations;
this.concurrent = maxConcurrent == null ? null : new AtomicInteger();
}
@Override
void beforeCleanupCacheRun() {
if (virtualThread != null) {
virtualThread.set(Thread.currentThread().isVirtual());
}
if (virtualThreadLatch != null) {
virtualThreadLatch.countDown();
}
if (maxConcurrent == null || invocations == null) {
return;
}
int running = concurrent.incrementAndGet();
maxConcurrent.accumulateAndGet(running, Math::max);
int currentInvocation = invocations.incrementAndGet();
try {
if (currentInvocation == 1 && firstStarted != null && releaseFirst != null) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (currentInvocation == 2 && secondStarted != null) {
secondStarted.countDown();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
concurrent.decrementAndGet();
}
}
}
}
@@ -17,12 +17,15 @@
package org.apache.hertzbeat.collector.dispatch;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import org.junit.jupiter.api.BeforeEach;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
@@ -32,33 +35,57 @@ class WorkerPoolTest {
private WorkerPool workerPool;
private Runnable mockTask;
@BeforeEach
void setUp() {
@AfterEach
void tearDown() throws Exception {
if (workerPool != null) {
workerPool.destroy();
}
}
@Test
void testExecuteJobRunsOnVirtualThread() throws Exception {
workerPool = new WorkerPool();
mockTask = mock(Runnable.class);
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
workerPool.executeJob(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void testExecuteJob() {
void testExecuteJobRejectsWhenConcurrencyLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1),
VirtualThreadProperties.PoolProperties.commonDefaults(),
VirtualThreadProperties.PoolProperties.managerDefaults(),
VirtualThreadProperties.AlerterProperties.defaults(),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
workerPool = new WorkerPool(properties);
assertDoesNotThrow(() -> workerPool.executeJob(mockTask));
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
workerPool.executeJob(() -> {
started.countDown();
try {
release.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(started.await(5, TimeUnit.SECONDS));
try {
assertThrows(RejectedExecutionException.class, () -> workerPool.executeJob(() -> {
}));
} finally {
release.countDown();
}
}
@Test
void testExecuteJobThrowsException() {
workerPool = mock(WorkerPool.class);
doThrow(new RejectedExecutionException()).when(workerPool).executeJob(mockTask);
assertThrows(RejectedExecutionException.class, () -> workerPool.executeJob(mockTask));
}
@Test
void testDestroy() {
assertDoesNotThrow(() -> workerPool.destroy());
}
}
@@ -18,19 +18,27 @@
package org.apache.hertzbeat.collector.dispatch.entrance;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.channel.Channel;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.remoting.RemotingClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -62,7 +70,7 @@ class CollectServerTest {
private DispatchProperties.EntranceProperties.NettyProperties nettyProperties;
@Mock
private CommonThreadPool threadPool;
private BackgroundTaskExecutor threadPool;
@Mock
private CollectorInfoProperties infoProperties;
@@ -142,4 +150,69 @@ class CollectServerTest {
assertNotNull(scheduledExecutor);
}
@Test
void testDispatchHeartbeatRunsOnVirtualThread() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties();
collectServer = new CollectServer(collectJobService, timerDispatch, properties(), threadPool, infoProperties, properties);
RemotingClient remotingClient = mock(RemotingClient.class);
ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient);
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
org.mockito.Mockito.doAnswer(invocation -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
return null;
}).when(remotingClient).sendMsg(any(ClusterMsg.Message.class));
collectServer.dispatchHeartbeat("collector1");
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void testDispatchHeartbeatDoesNotRunConcurrently() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties();
collectServer = new CollectServer(collectJobService, timerDispatch, properties(), threadPool, infoProperties, properties);
RemotingClient remotingClient = mock(RemotingClient.class);
ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger concurrent = new AtomicInteger();
AtomicInteger maxConcurrent = new AtomicInteger();
AtomicInteger invocations = new AtomicInteger();
org.mockito.Mockito.doAnswer(invocation -> {
int running = concurrent.incrementAndGet();
maxConcurrent.accumulateAndGet(running, Math::max);
int currentInvocation = invocations.incrementAndGet();
if (currentInvocation == 1) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (currentInvocation == 2) {
secondStarted.countDown();
}
concurrent.decrementAndGet();
return null;
}).when(remotingClient).sendMsg(any(ClusterMsg.Message.class));
collectServer.dispatchHeartbeat("collector1");
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
collectServer.dispatchHeartbeat("collector1");
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
private DispatchProperties properties() {
return properties;
}
}
@@ -114,7 +114,7 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
public void start() {
try {
// Pull the collection task from the task queue and put it into the thread pool for execution
workerPool.executeJob(() -> {
workerPool.executeLongRunning(() -> {
Thread.currentThread().setName("metrics-task-dispatcher");
while (!Thread.currentThread().isInterrupted()) {
MetricsCollect metricsCollect = null;
@@ -379,4 +379,4 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
private Metrics metrics;
private Timeout timeout;
}
}
}
@@ -75,3 +75,12 @@ push:
common:
queue:
type: netty
hertzbeat:
# Optional virtual-thread overrides. Remove this whole block to use built-in defaults.
vthreads:
enabled: true
common:
mode: UNBOUNDED_VT
collector:
mode: LIMIT_AND_REJECT
@@ -20,14 +20,15 @@ package org.apache.hertzbeat.collector.collect.common.cache;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* Singleton LRU global resource cache for client-server connections
@@ -56,10 +57,20 @@ public class GlobalConnectionCache {
*/
private final ConcurrentLinkedHashMap<Object, AbstractConnection<?>> cacheMap;
private final ScheduledExecutorService scheduledExecutor;
private final ExecutorService cleanupExecutor;
private final ScheduledDispatchTask cleanupTask;
/**
* Private constructor to prevent instantiation
*/
private GlobalConnectionCache() {
this(true);
}
GlobalConnectionCache(boolean autoStart) {
cacheMap = new ConcurrentLinkedHashMap.Builder<Object, AbstractConnection<?>>()
.maximumWeightedCapacity(Integer.MAX_VALUE)
.listener((key, value) -> {
@@ -72,7 +83,13 @@ public class GlobalConnectionCache {
log.info("GlobalConnectionCache discarded key: {}, value: {}.", key, value);
})
.build();
initCacheMonitor();
this.scheduledExecutor = createCacheMonitorScheduler();
this.cleanupExecutor = createCleanupExecutor();
this.cleanupTask = new ScheduledDispatchTask(cleanupExecutor, this::runCleanTimeoutOrUnHealthyCache);
if (autoStart) {
initCacheMonitor();
Runtime.getRuntime().addShutdownHook(new Thread(this::destroy));
}
}
/**
@@ -95,18 +112,42 @@ public class GlobalConnectionCache {
* Initialize the cache monitor for cleaning up expired connections
*/
private void initCacheMonitor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("connection-cache-timeout-detector-%d")
.setDaemon(true)
.build();
ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
scheduledExecutor.scheduleWithFixedDelay(this::cleanTimeoutOrUnHealthyCache, 2, 100, TimeUnit.SECONDS);
scheduledExecutor.scheduleWithFixedDelay(this::dispatchCleanupCache, 2, 100, TimeUnit.SECONDS);
}
/**
* Clean and remove timeout or unhealthy cache entries
*/
private void cleanTimeoutOrUnHealthyCache() {
void dispatchCleanupCache() {
cleanupTask.dispatch();
}
void beforeCleanTimeoutOrUnHealthyCacheRun() {
}
void destroy() {
scheduledExecutor.shutdownNow();
cleanupExecutor.shutdownNow();
}
private ScheduledExecutorService createCacheMonitorScheduler() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("connection-cache-timeout-detector-%d")
.setDaemon(true)
.build();
return Executors.newSingleThreadScheduledExecutor(threadFactory);
}
private ExecutorService createCleanupExecutor() {
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("connection-cache-cleaner-vt-", 0)
.uncaughtExceptionHandler((thread, throwable) ->
log.error("Connection cache cleanup has uncaughtException.", throwable))
.factory());
}
private void runCleanTimeoutOrUnHealthyCache() {
beforeCleanTimeoutOrUnHealthyCacheRun();
try {
cacheMap.forEach((key, value) -> {
Long[] cacheTime = timeoutMap.get(key);
@@ -204,4 +245,59 @@ public class GlobalConnectionCache {
log.error("Connection close error for key {}: {}", key, e.getMessage(), e);
}
}
private static final class ScheduledDispatchTask {
private final ExecutorService executor;
private final Runnable task;
private boolean running;
private int pendingRuns;
private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
this.executor = executor;
this.task = task;
}
private void dispatch() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns++;
shouldSchedule = !running;
if (shouldSchedule) {
running = true;
}
}
if (shouldSchedule) {
scheduleRun();
}
}
private void scheduleRun() {
executor.execute(this::runOnce);
}
private void runOnce() {
try {
task.run();
} finally {
scheduleNextIfNeeded();
}
}
private void scheduleNextIfNeeded() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns = Math.max(0, pendingRuns - 1);
shouldSchedule = pendingRuns > 0;
if (!shouldSchedule) {
running = false;
return;
}
}
scheduleRun();
}
}
}
@@ -22,6 +22,7 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
@@ -52,6 +53,14 @@ public class CommonHttpClient {
private static PoolingHttpClientConnectionManager connectionManager;
private static ScheduledExecutorService scheduledExecutor;
private static ExecutorService cleanupExecutor;
private static ScheduledDispatchTask cleanupTask;
private static volatile Runnable beforeCleanupHook;
/**
* all max total connection
*/
@@ -137,15 +146,9 @@ public class CommonHttpClient {
// clean up available but idle connections
.evictIdleConnections(100, TimeUnit.SECONDS)
.build();
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("http-connection-pool-cleaner-%d")
.setDaemon(true)
.build();
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1, threadFactory);
scheduledExecutor.scheduleWithFixedDelay(() -> {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(40, TimeUnit.SECONDS);
}, 40L, 40L, TimeUnit.SECONDS);
initializeCleanupExecutors();
scheduledExecutor.scheduleWithFixedDelay(CommonHttpClient::dispatchConnectionPoolCleanup,
40L, 40L, TimeUnit.SECONDS);
// shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(CommonHttpClient::close));
@@ -156,13 +159,119 @@ public class CommonHttpClient {
public static CloseableHttpClient getHttpClient() {
return httpClient;
}
public static PoolingHttpClientConnectionManager getConnectionManager() {
return connectionManager;
}
static void dispatchConnectionPoolCleanup() {
if (cleanupTask != null) {
cleanupTask.dispatch();
}
}
static void setConnectionManagerForTest(PoolingHttpClientConnectionManager manager) {
connectionManager = manager;
}
static void setBeforeCleanupHookForTest(Runnable hook) {
beforeCleanupHook = hook;
}
public static void close() {
try {
httpClient.close();
if (httpClient != null) {
httpClient.close();
}
} catch (Exception e) {
log.error("close http client error", e);
}
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
}
if (cleanupExecutor != null) {
cleanupExecutor.shutdownNow();
}
}
private static void initializeCleanupExecutors() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("http-connection-pool-cleaner-%d")
.setDaemon(true)
.build();
scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
cleanupExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("http-connection-pool-cleaner-vt-", 0)
.uncaughtExceptionHandler((thread, throwable) ->
log.error("HTTP connection pool cleanup has uncaughtException.", throwable))
.factory());
cleanupTask = new ScheduledDispatchTask(cleanupExecutor, CommonHttpClient::runConnectionPoolCleanup);
}
private static void runConnectionPoolCleanup() {
Runnable hook = beforeCleanupHook;
if (hook != null) {
hook.run();
}
if (connectionManager == null) {
return;
}
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(40, TimeUnit.SECONDS);
}
private static final class ScheduledDispatchTask {
private final ExecutorService executor;
private final Runnable task;
private boolean running;
private int pendingRuns;
private ScheduledDispatchTask(ExecutorService executor, Runnable task) {
this.executor = executor;
this.task = task;
}
private void dispatch() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns++;
shouldSchedule = !running;
if (shouldSchedule) {
running = true;
}
}
if (shouldSchedule) {
scheduleRun();
}
}
private void scheduleRun() {
executor.execute(this::runOnce);
}
private void runOnce() {
try {
task.run();
} finally {
scheduleNextIfNeeded();
}
}
private void scheduleNextIfNeeded() {
boolean shouldSchedule;
synchronized (this) {
pendingRuns = Math.max(0, pendingRuns - 1);
shouldSchedule = pendingRuns > 0;
if (!shouldSchedule) {
running = false;
return;
}
}
scheduleRun();
}
}
}
@@ -18,15 +18,19 @@
package org.apache.hertzbeat.collector.dispatch;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Collection task worker thread pool
@@ -35,25 +39,55 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class WorkerPool implements DisposableBean {
private ThreadPoolExecutor workerExecutor;
private final ManagedExecutor workerExecutor;
private final ManagedExecutor longRunningExecutor;
public WorkerPool() {
initWorkExecutor();
this(VirtualThreadProperties.defaults());
}
private void initWorkExecutor() {
// thread factory
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("[Important] WorkerPool workerExecutor has uncaughtException.", throwable);
@Autowired
public WorkerPool(VirtualThreadProperties virtualThreadProperties) {
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.workerExecutor = createWorkerExecutor(properties);
this.longRunningExecutor = createLongRunningExecutor(properties, workerExecutor);
}
private ManagedExecutor createWorkerExecutor(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("[Important] WorkerPool workerExecutor has uncaughtException.", throwable);
log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.PoolProperties poolProperties = properties.collector();
return ManagedExecutors.newVirtualExecutor("collector-worker", "collect-worker-",
poolProperties.mode(), poolProperties.maxConcurrentJobs(), handler);
}
return ManagedExecutors.wrap("collector-worker", createLegacyExecutor(handler));
}
private ManagedExecutor createLongRunningExecutor(VirtualThreadProperties properties, ManagedExecutor fallback) {
if (!properties.enabled()) {
return fallback;
}
return ManagedExecutors.newPlatformExecutor("collector-long-running", "collect-long-running-",
(thread, throwable) -> {
log.error("[Important] WorkerPool longRunningExecutor has uncaughtException.", throwable);
log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable);
})
});
}
private ExecutorService createLegacyExecutor(Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("collect-worker-%d")
.build();
int coreSize = Math.max(2, Runtime.getRuntime().availableProcessors());
int maxSize = Runtime.getRuntime().availableProcessors() * 16;
workerExecutor = new ThreadPoolExecutor(coreSize,
return new ThreadPoolExecutor(coreSize,
maxSize,
10,
TimeUnit.SECONDS,
@@ -72,10 +106,20 @@ public class WorkerPool implements DisposableBean {
workerExecutor.execute(runnable);
}
/**
* Run the long-lived dispatcher job outside of the collector admission limit.
*
* @param runnable dispatcher task
*/
public void executeLongRunning(Runnable runnable) {
longRunningExecutor.execute(runnable);
}
@Override
public void destroy() throws Exception {
if (workerExecutor != null) {
workerExecutor.shutdownNow();
workerExecutor.close();
if (longRunningExecutor != workerExecutor) {
longRunningExecutor.close();
}
}
}
@@ -32,19 +32,24 @@ import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOfflineProce
import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProcessor;
import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.RemotingClient;
import org.apache.hertzbeat.remoting.event.NettyEventListener;
import org.apache.hertzbeat.remoting.netty.NettyClientConfig;
import org.apache.hertzbeat.remoting.netty.NettyRemotingClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
@@ -70,11 +75,42 @@ public class CollectServer implements CommandLineRunner {
private ScheduledExecutorService scheduledExecutor;
private final ExecutorService heartbeatExecutor;
private final Object heartbeatLock = new Object();
private boolean heartbeatRunning;
private boolean heartbeatPending;
private final Runnable closeApplicationAction;
public CollectServer(final CollectJobService collectJobService,
final TimerDispatch timerDispatch,
final DispatchProperties properties,
final CommonThreadPool threadPool,
final BackgroundTaskExecutor threadPool,
final CollectorInfoProperties infoProperties) {
this(collectJobService, timerDispatch, properties, threadPool, infoProperties, null,
VirtualThreadProperties.defaults());
}
public CollectServer(final CollectJobService collectJobService,
final TimerDispatch timerDispatch,
final DispatchProperties properties,
final BackgroundTaskExecutor threadPool,
final CollectorInfoProperties infoProperties,
final VirtualThreadProperties virtualThreadProperties) {
this(collectJobService, timerDispatch, properties, threadPool, infoProperties, null, virtualThreadProperties);
}
@Autowired
public CollectServer(final CollectJobService collectJobService,
final TimerDispatch timerDispatch,
final DispatchProperties properties,
final BackgroundTaskExecutor threadPool,
final CollectorInfoProperties infoProperties,
final ConfigurableApplicationContext applicationContext,
final VirtualThreadProperties virtualThreadProperties) {
if (properties == null || properties.getEntrance() == null || properties.getEntrance().getNetty() == null) {
log.error("init error, please config dispatch entrance netty props in application.yml");
throw new IllegalArgumentException("please config dispatch entrance netty props");
@@ -87,10 +123,12 @@ public class CollectServer implements CommandLineRunner {
this.timerDispatch = timerDispatch;
this.collectJobService.setCollectServer(this);
this.infoProperties = infoProperties;
this.heartbeatExecutor = createHeartbeatExecutor(virtualThreadProperties);
this.closeApplicationAction = createCloseApplicationAction(applicationContext);
this.init(properties, threadPool);
}
private void init(final DispatchProperties properties, final CommonThreadPool threadPool) {
private void init(final DispatchProperties properties, final BackgroundTaskExecutor threadPool) {
NettyClientConfig nettyClientConfig = new NettyClientConfig();
DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty();
nettyClientConfig.setServerHost(nettyProperties.getManagerHost());
@@ -101,13 +139,19 @@ public class CollectServer implements CommandLineRunner {
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new GoOfflineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new GoOnlineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_CLOSE, new GoCloseProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new GoOfflineProcessor(timerDispatch));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new GoOnlineProcessor(timerDispatch));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_CLOSE,
new GoCloseProcessor(this, timerDispatch, closeApplicationAction));
}
public void shutdown() {
this.scheduledExecutor.shutdownNow();
if (this.scheduledExecutor != null) {
this.scheduledExecutor.shutdownNow();
}
if (this.heartbeatExecutor != null) {
this.heartbeatExecutor.shutdownNow();
}
this.remotingClient.shutdown();
}
@@ -120,6 +164,21 @@ public class CollectServer implements CommandLineRunner {
this.remotingClient.sendMsg(message);
}
void dispatchHeartbeat(String identity) {
if (heartbeatExecutor == null) {
sendHeartbeat(identity);
return;
}
synchronized (heartbeatLock) {
if (heartbeatRunning) {
heartbeatPending = true;
return;
}
heartbeatRunning = true;
}
submitHeartbeat(identity);
}
@Override
public void run(String... args) throws Exception {
this.remotingClient.start();
@@ -161,19 +220,8 @@ public class CollectServer implements CommandLineRunner {
.build();
scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
// schedule send heartbeat message
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.HEARTBEAT)
.build();
CollectServer.this.sendMsg(heartbeat);
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
} catch (Exception e) {
log.error("schedule send heartbeat to server error.{}", e.getMessage());
}
}, 5, 5, TimeUnit.SECONDS);
scheduledExecutor.scheduleAtFixedRate(() -> CollectServer.this.dispatchHeartbeat(identity),
5, 5, TimeUnit.SECONDS);
}
}
@@ -182,4 +230,82 @@ public class CollectServer implements CommandLineRunner {
log.info("handle idle event triggered. collector is going offline.");
}
}
private ExecutorService createHeartbeatExecutor(VirtualThreadProperties virtualThreadProperties) {
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
if (!properties.enabled()) {
return null;
}
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("heartbeat-worker-vt-", 0)
.uncaughtExceptionHandler((thread, throwable) -> {
log.error("HeartBeat worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.factory());
}
private void submitHeartbeat(String identity) {
boolean submitted = false;
try {
heartbeatExecutor.execute(() -> {
try {
sendHeartbeat(identity);
} finally {
onHeartbeatComplete(identity);
}
});
submitted = true;
} finally {
if (!submitted) {
synchronized (heartbeatLock) {
heartbeatRunning = false;
heartbeatPending = false;
}
}
}
}
private void onHeartbeatComplete(String identity) {
boolean shouldRunAgain;
synchronized (heartbeatLock) {
if (heartbeatPending) {
heartbeatPending = false;
shouldRunAgain = true;
} else {
heartbeatRunning = false;
shouldRunAgain = false;
}
}
if (shouldRunAgain) {
submitHeartbeat(identity);
}
}
private void sendHeartbeat(String identity) {
try {
ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.HEARTBEAT)
.build();
CollectServer.this.sendMsg(heartbeat);
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
} catch (Exception e) {
log.error("schedule send heartbeat to server error.{}", e.getMessage());
}
}
private Runnable createCloseApplicationAction(ConfigurableApplicationContext applicationContext) {
if (applicationContext == null) {
return () -> {};
}
return () -> {
SpringApplication.exit(applicationContext, () -> 0);
if (applicationContext.isActive()) {
applicationContext.close();
}
};
}
}
@@ -23,9 +23,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
import org.springframework.boot.SpringApplication;
/**
* handle collector close message
@@ -34,24 +32,25 @@ import org.springframework.boot.SpringApplication;
@Slf4j
public class GoCloseProcessor implements NettyRemotingProcessor {
private final CollectServer collectServer;
private TimerDispatch timerDispatch;
private final TimerDispatch timerDispatch;
private final Runnable closeApplicationAction;
public GoCloseProcessor(final CollectServer collectServer) {
public GoCloseProcessor(final CollectServer collectServer,
final TimerDispatch timerDispatch,
final Runnable closeApplicationAction) {
this.collectServer = collectServer;
this.timerDispatch = timerDispatch;
this.closeApplicationAction = closeApplicationAction;
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsg());
}
this.timerDispatch.goOffline();
this.collectServer.shutdown();
SpringApplication.exit(SpringContextHolder.getApplicationContext(), () -> 0);
SpringContextHolder.shutdown();
closeApplicationAction.run();
log.info("receive offline message and close success");
return null;
}
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
/**
@@ -32,14 +31,15 @@ import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
*/
@Slf4j
public class GoOfflineProcessor implements NettyRemotingProcessor {
private TimerDispatch timerDispatch;
private final TimerDispatch timerDispatch;
public GoOfflineProcessor(TimerDispatch timerDispatch) {
this.timerDispatch = timerDispatch;
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
timerDispatch.goOffline();
log.info("receive offline message and handle success");
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
@@ -24,7 +24,6 @@ import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -36,13 +35,14 @@ import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@Slf4j
public class GoOnlineProcessor implements NettyRemotingProcessor {
private TimerDispatch timerDispatch;
private final TimerDispatch timerDispatch;
public GoOnlineProcessor(TimerDispatch timerDispatch) {
this.timerDispatch = timerDispatch;
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().isEmpty()) {
log.warn("The message that server response to collector is empty, please upgrade server");
} else {
@@ -25,9 +25,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
import org.apache.hertzbeat.collector.dispatch.MetricsTaskDispatch;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -35,6 +37,8 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.timer.HashedWheelTimer;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Component;
@@ -69,7 +73,19 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
*/
private final AtomicBoolean started;
private final Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier;
public TimerDispatcher() {
this(() -> timeout -> {
});
}
@Autowired
public TimerDispatcher(ObjectProvider<MetricsTaskDispatch> metricsTaskDispatchProvider) {
this(resolveMetricsTaskDispatchSupplier(metricsTaskDispatchProvider));
}
private TimerDispatcher(Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier) {
this.wheelTimer = new HashedWheelTimer(r -> {
Thread ret = new Thread(r, "wheelTimer");
ret.setDaemon(true);
@@ -79,6 +95,16 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
this.currentTempTaskMap = new ConcurrentHashMap<>(8);
this.eventListeners = new ConcurrentHashMap<>(8);
this.started = new AtomicBoolean(true);
this.metricsTaskDispatchSupplier = metricsTaskDispatchSupplier;
}
private static Supplier<MetricsTaskDispatch> resolveMetricsTaskDispatchSupplier(
ObjectProvider<MetricsTaskDispatch> metricsTaskDispatchProvider) {
if (metricsTaskDispatchProvider == null) {
return () -> timeout -> {
};
}
return metricsTaskDispatchProvider::getObject;
}
@Override
@@ -87,7 +113,8 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
log.warn("Collector is offline, can not dispatch collect jobs.");
return;
}
WheelTimerTask timerJob = new WheelTimerTask(addJob);
// Delay dispatcher lookup to avoid a startup cycle with CommonDispatcher.
WheelTimerTask timerJob = new WheelTimerTask(addJob, metricsTaskDispatchSupplier);
if (addJob.isCyclic()) {
Long nextExecutionTime = getNextExecutionInterval(addJob);
Timeout timeout = wheelTimer.newTimeout(timerJob, nextExecutionTime, TimeUnit.SECONDS);
@@ -27,7 +27,6 @@ import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.TimerTask;
import org.apache.hertzbeat.common.util.AesUtil;
@@ -35,6 +34,7 @@ import org.apache.hertzbeat.common.util.AesUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
@@ -44,11 +44,15 @@ import java.util.stream.Collectors;
public class WheelTimerTask implements TimerTask {
private final Job job;
private final MetricsTaskDispatch metricsTaskDispatch;
private final Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier;
private static final Gson GSON = new Gson();
public WheelTimerTask(Job job) {
this.metricsTaskDispatch = SpringContextHolder.getBean(MetricsTaskDispatch.class);
public WheelTimerTask(Job job, MetricsTaskDispatch metricsTaskDispatch) {
this(job, () -> metricsTaskDispatch);
}
public WheelTimerTask(Job job, Supplier<MetricsTaskDispatch> metricsTaskDispatchSupplier) {
this.metricsTaskDispatchSupplier = metricsTaskDispatchSupplier;
this.job = job;
// The initialization job will monitor the actual parameter value and replace the collection field
initJobMetrics(job);
@@ -93,7 +97,7 @@ public class WheelTimerTask implements TimerTask {
@Override
public void run(Timeout timeout) throws Exception {
job.setDispatchTime(System.currentTimeMillis());
metricsTaskDispatch.dispatchMetricsTask(timeout);
metricsTaskDispatchSupplier.get().dispatchMetricsTask(timeout);
}
public Job getJob() {
@@ -0,0 +1,175 @@
/*
* 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.collector.collect.common.cache;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link GlobalConnectionCache}.
*/
class GlobalConnectionCacheTest {
private TestGlobalConnectionCache globalConnectionCache;
@BeforeEach
void setUp() {
globalConnectionCache = new TestGlobalConnectionCache();
}
@AfterEach
void tearDown() {
if (globalConnectionCache != null) {
globalConnectionCache.destroy();
}
}
@Test
void dispatchCleanupCacheRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
globalConnectionCache.setVirtualThreadHook(latch, virtualThread);
globalConnectionCache.dispatchCleanupCache();
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void dispatchCleanupCacheDoesNotRunConcurrently() throws Exception {
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger maxConcurrent = new AtomicInteger();
globalConnectionCache.setConcurrencyHook(firstStarted, releaseFirst, secondStarted, maxConcurrent);
globalConnectionCache.dispatchCleanupCache();
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
globalConnectionCache.dispatchCleanupCache();
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
@Test
void dispatchCleanupCacheClosesExpiredConnections() throws Exception {
TestConnection connection = new TestConnection();
globalConnectionCache.addCache("expired", connection, -1L);
globalConnectionCache.dispatchCleanupCache();
assertTrue(connection.closed.await(5, TimeUnit.SECONDS));
assertEquals(1, connection.closeCount.get());
assertTrue(globalConnectionCache.getCache("expired", false).isEmpty());
}
private static final class TestGlobalConnectionCache extends GlobalConnectionCache {
private CountDownLatch virtualThreadLatch;
private AtomicBoolean virtualThread;
private CountDownLatch firstStarted;
private CountDownLatch releaseFirst;
private CountDownLatch secondStarted;
private AtomicInteger maxConcurrent;
private final AtomicInteger concurrent = new AtomicInteger();
private final AtomicInteger invocations = new AtomicInteger();
private TestGlobalConnectionCache() {
super(false);
}
private void setVirtualThreadHook(CountDownLatch latch, AtomicBoolean flag) {
this.virtualThreadLatch = latch;
this.virtualThread = flag;
}
private void setConcurrencyHook(CountDownLatch firstStarted, CountDownLatch releaseFirst,
CountDownLatch secondStarted, AtomicInteger maxConcurrent) {
this.firstStarted = firstStarted;
this.releaseFirst = releaseFirst;
this.secondStarted = secondStarted;
this.maxConcurrent = maxConcurrent;
}
@Override
void beforeCleanTimeoutOrUnHealthyCacheRun() {
if (virtualThread != null) {
virtualThread.set(Thread.currentThread().isVirtual());
}
if (virtualThreadLatch != null) {
virtualThreadLatch.countDown();
}
if (maxConcurrent == null) {
return;
}
int active = concurrent.incrementAndGet();
maxConcurrent.accumulateAndGet(active, Math::max);
int currentInvocation = invocations.incrementAndGet();
try {
if (currentInvocation == 1) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (currentInvocation == 2) {
secondStarted.countDown();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
concurrent.decrementAndGet();
}
}
}
private static final class TestConnection extends AbstractConnection<Object> {
private final CountDownLatch closed = new CountDownLatch(1);
private final AtomicInteger closeCount = new AtomicInteger();
@Override
public Object getConnection() {
return new Object();
}
@Override
public void closeConnection() {
closeCount.incrementAndGet();
closed.countDown();
}
}
}
@@ -0,0 +1,121 @@
/*
* 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.collector.collect.common.http;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
* Tests for CommonHttpClient cleanup dispatch.
*/
class CommonHttpClientVirtualThreadTest {
private final PoolingHttpClientConnectionManager originalConnectionManager = CommonHttpClient.getConnectionManager();
@AfterEach
void tearDown() {
CommonHttpClient.setBeforeCleanupHookForTest(null);
CommonHttpClient.setConnectionManagerForTest(originalConnectionManager);
}
@Test
void dispatchConnectionPoolCleanupRunsOnVirtualThread() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class);
CommonHttpClient.setConnectionManagerForTest(manager);
CommonHttpClient.setBeforeCleanupHookForTest(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
CommonHttpClient.dispatchConnectionPoolCleanup();
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void dispatchConnectionPoolCleanupDoesNotRunConcurrently() throws Exception {
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicInteger concurrent = new AtomicInteger();
AtomicInteger maxConcurrent = new AtomicInteger();
AtomicInteger invocations = new AtomicInteger();
PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class);
CommonHttpClient.setConnectionManagerForTest(manager);
CommonHttpClient.setBeforeCleanupHookForTest(() -> {
int active = concurrent.incrementAndGet();
maxConcurrent.accumulateAndGet(active, Math::max);
int currentInvocation = invocations.incrementAndGet();
try {
if (currentInvocation == 1) {
firstStarted.countDown();
releaseFirst.await(5, TimeUnit.SECONDS);
} else if (currentInvocation == 2) {
secondStarted.countDown();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
concurrent.decrementAndGet();
}
});
CommonHttpClient.dispatchConnectionPoolCleanup();
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
CommonHttpClient.dispatchConnectionPoolCleanup();
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertEquals(1, maxConcurrent.get());
}
@Test
void dispatchConnectionPoolCleanupClosesExpiredAndIdleConnections() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
PoolingHttpClientConnectionManager manager = mock(PoolingHttpClientConnectionManager.class);
doAnswer(invocation -> {
latch.countDown();
return null;
}).when(manager).closeExpiredConnections();
CommonHttpClient.setConnectionManagerForTest(manager);
CommonHttpClient.dispatchConnectionPoolCleanup();
assertTrue(latch.await(5, TimeUnit.SECONDS));
verify(manager, times(1)).closeExpiredConnections();
verify(manager, times(1)).closeIdleConnections(40, TimeUnit.SECONDS);
}
}
@@ -20,19 +20,15 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatcher;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.lang.reflect.Field;
@@ -51,32 +47,27 @@ class GoOnlineProcessorTest {
@Mock
private ChannelHandlerContext channelHandlerContext;
private MockedStatic<SpringContextHolder> springContextHolderMockedStatic;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
goOnlineProcessor = new GoOnlineProcessor();
timerDispatcher = new TimerDispatcher();
springContextHolderMockedStatic = Mockito.mockStatic(SpringContextHolder.class);
springContextHolderMockedStatic.when(() -> SpringContextHolder.getBean(TimerDispatch.class)).thenReturn(timerDispatcher);
goOnlineProcessor = new GoOnlineProcessor(timerDispatcher);
}
@AfterEach
void tearDown() throws Exception {
springContextHolderMockedStatic.close();
timerDispatcher.destroy();
}
@Test
void verifyTaskMapPreservation() throws Exception {
Job job = Job.builder()
.app("test")
.id(12345L)
.metrics(Lists.newArrayList(Metrics.builder().interval(100L).build()))
.configmap(Lists.newArrayList())
.isCyclic(true)
.build();
.app("test")
.id(12345L)
.metrics(Lists.newArrayList(Metrics.builder().interval(100L).build()))
.configmap(Lists.newArrayList())
.isCyclic(true)
.build();
timerDispatcher.addJob(job, null);
Field cyclicTaskMapField = TimerDispatcher.class.getDeclaredField("currentCyclicTaskMap");
@@ -85,20 +76,20 @@ class GoOnlineProcessorTest {
assertEquals(1, currentCyclicTaskMap.size(), "Task map should have 1 job initially");
ClusterMsg.Message responseMsg = ClusterMsg.Message.newBuilder()
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
goOnlineProcessor.handle(channelHandlerContext, responseMsg);
assertEquals(1, currentCyclicTaskMap.size(), "Task map should still have 1 job after receiving RESPONSE");
ClusterMsg.Message requestMsg = ClusterMsg.Message.newBuilder()
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.REQUEST)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.REQUEST)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(job)))
.setIdentity("test-identity")
.build();
goOnlineProcessor.handle(channelHandlerContext, requestMsg);
assertEquals(0, currentCyclicTaskMap.size(), "Task map should be empty after receiving REQUEST");
}
@@ -46,7 +46,6 @@ import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.TopicPartitionInfo;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Collections;
@@ -223,14 +222,14 @@ public class KafkaCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null, "Metrics cannot be null");
require(metrics != null, "Metrics cannot be null");
KafkaProtocol kafkaProtocol = metrics.getKclient();
// Ensure that metrics and kafkaProtocol are not null
Assert.isTrue(metrics != null && kafkaProtocol != null, "Kafka collect must have kafkaProtocol params");
require(kafkaProtocol != null, "Kafka collect must have kafkaProtocol params");
// Ensure that host and port are not empty
Assert.hasText(kafkaProtocol.getHost(), "Kafka Protocol host is required.");
Assert.hasText(kafkaProtocol.getPort(), "Kafka Protocol port is required.");
requireHasText(kafkaProtocol.getHost(), "Kafka Protocol host is required.");
requireHasText(kafkaProtocol.getPort(), "Kafka Protocol port is required.");
}
@Override
@@ -384,4 +383,14 @@ public class KafkaCollectImpl extends AbstractCollect {
public String supportProtocol() {
return DispatchConstants.PROTOCOL_KAFKA;
}
private static void require(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
private static void requireHasText(String value, String message) {
require(value != null && !value.trim().isEmpty(), message);
}
}
@@ -43,7 +43,6 @@ import org.apache.hertzbeat.common.entity.job.protocol.MongodbProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.bson.Document;
import org.springframework.util.Assert;
/**
* Mongodb single collect
@@ -87,11 +86,11 @@ public class MongodbSingleCollectImpl extends AbstractCollect {
*/
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException{
Assert.isTrue(metrics != null && metrics.getMongodb() != null, "Mongodb collect must has mongodb params");
require(metrics != null && metrics.getMongodb() != null, "Mongodb collect must has mongodb params");
MongodbProtocol mongodbProtocol = metrics.getMongodb();
Assert.hasText(mongodbProtocol.getCommand(), "Mongodb Protocol command is required.");
Assert.hasText(mongodbProtocol.getHost(), "Mongodb Protocol host is required.");
Assert.hasText(mongodbProtocol.getPort(), "Mongodb Protocol port is required.");
requireHasText(mongodbProtocol.getCommand(), "Mongodb Protocol command is required.");
requireHasText(mongodbProtocol.getHost(), "Mongodb Protocol host is required.");
requireHasText(mongodbProtocol.getPort(), "Mongodb Protocol port is required.");
}
@Override
@@ -226,4 +225,14 @@ public class MongodbSingleCollectImpl extends AbstractCollect {
connectionCommonCache.addCache(identifier, mongodbConnect, 3600 * 1000L);
return mongoClient;
}
private static void require(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
private static void requireHasText(String value, String message) {
require(value != null && !value.trim().isEmpty(), message);
}
}
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
@@ -33,8 +34,6 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.NgqlProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder;
import org.springframework.util.Assert;
import org.springframework.util.StopWatch;
/**
* connect nebulaGraph and collect metrics use NGQL
@@ -53,18 +52,17 @@ public class NgqlCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
NgqlProtocol ngql = metrics.getNgql();
Assert.hasText(ngql.getHost(), "NGQL protocol host is required");
Assert.hasText(ngql.getPort(), "Port protocol host is required");
Assert.hasText(ngql.getParseType(), "NGQL protocol parseType is required");
Assert.hasText(ngql.getUsername(), "NGQL protocol username is required");
Assert.hasText(ngql.getPassword(), "NGQL protocol password is required");
requireHasText(ngql.getHost(), "NGQL protocol host is required");
requireHasText(ngql.getPort(), "Port protocol host is required");
requireHasText(ngql.getParseType(), "NGQL protocol parseType is required");
requireHasText(ngql.getUsername(), "NGQL protocol username is required");
requireHasText(ngql.getPassword(), "NGQL protocol password is required");
}
@Override
public void collect(Builder builder, Metrics metrics) {
NgqlProtocol ngql = metrics.getNgql();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
long startTimeNanos = System.nanoTime();
NebulaTemplate nebulaTemplate = new NebulaTemplate();
try {
boolean initSuccess = nebulaTemplate.initSession(ngql);
@@ -79,8 +77,7 @@ public class NgqlCollectImpl extends AbstractCollect {
return;
}
stopWatch.stop();
long responseTime = stopWatch.getTotalTimeMillis();
long responseTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos);
try {
switch (ngql.getParseType()) {
case PARSE_TYPE_FILTER_COUNT -> filterCount(nebulaTemplate, ngql, metrics.getAliasFields(), builder, responseTime);
@@ -247,4 +244,10 @@ public class NgqlCollectImpl extends AbstractCollect {
result.put("running_jobs", String.valueOf(jobs.stream().filter(job -> Objects.equals(job.get("Status"), STATUS_RUNNING)).count()));
return result;
}
private static void requireHasText(String value, String message) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(message);
}
}
}
@@ -28,12 +28,6 @@
<artifactId>hertzbeat-collector-rocketmq</artifactId>
<name>${project.artifactId}</name>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
@@ -47,4 +41,4 @@
</dependency>
</dependencies>
</project>
</project>
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.collector.collect.rocketmq;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -28,10 +27,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@@ -40,6 +35,8 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.JsonPathParser;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol;
@@ -55,23 +52,21 @@ import org.apache.rocketmq.common.protocol.body.KVTable;
import org.apache.rocketmq.common.protocol.body.SubscriptionGroupWrapper;
import org.apache.rocketmq.common.protocol.body.TopicList;
import org.apache.rocketmq.common.protocol.route.BrokerData;
import org.apache.rocketmq.common.utils.ThreadUtils;
import org.apache.rocketmq.remoting.RPCHook;
import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.util.Assert;
/**
* rocketmq collect
*/
@Slf4j
public class RocketmqSingleCollectImpl extends AbstractCollect implements DisposableBean {
public class RocketmqSingleCollectImpl extends AbstractCollect {
private static final int WAIT_TIMEOUT = 10;
static final int QUEUE_CAPACITY = 5000;
private static final Set<String> SYSTEM_GROUP_SET = new HashSet<>();
private final ExecutorService executorService;
private final ManagedExecutor executorService;
static {
// system consumer group
@@ -86,24 +81,27 @@ public class RocketmqSingleCollectImpl extends AbstractCollect implements Dispos
}
public RocketmqSingleCollectImpl() {
this(createExecutor());
}
RocketmqSingleCollectImpl(ManagedExecutor executorService) {
this.executorService = executorService;
}
private static ManagedExecutor createExecutor() {
Runtime runtime = Runtime.getRuntime();
int corePoolSize = Math.max(8, runtime.availableProcessors());
int maximumPoolSize = Math.max(16, runtime.availableProcessors());
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("RocketMQCollectGroup has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("rocketMQ-collector-%d")
.build();
this.executorService = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5000), threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("RocketMQCollectGroup has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
return ManagedExecutors.newDiscardOldestVirtualExecutor("rocketmq-collector", "rocketmq-collector-",
corePoolSize, maximumPoolSize, QUEUE_CAPACITY, handler);
}
@Override
public void destroy() {
ThreadUtils.shutdownGracefully(this.executorService, 10L, TimeUnit.SECONDS);
this.executorService.close();
}
/**
@@ -112,10 +110,10 @@ public class RocketmqSingleCollectImpl extends AbstractCollect implements Dispos
*/
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null && metrics.getRocketmq() != null, "Rocketmq collect must has rocketmq params");
require(metrics != null && metrics.getRocketmq() != null, "Rocketmq collect must has rocketmq params");
RocketmqProtocol rocketmq = metrics.getRocketmq();
Assert.hasText(rocketmq.getNamesrvHost(), "Rocketmq Protocol namesrvHost is required.");
Assert.hasText(rocketmq.getNamesrvPort(), "Rocketmq Protocol namesrvPort is required.");
requireHasText(rocketmq.getNamesrvHost(), "Rocketmq Protocol namesrvHost is required.");
requireHasText(rocketmq.getNamesrvPort(), "Rocketmq Protocol namesrvPort is required.");
}
@Override
@@ -270,7 +268,7 @@ public class RocketmqSingleCollectImpl extends AbstractCollect implements Dispos
if (SYSTEM_GROUP_SET.contains(consumerGroup)) {
continue;
}
executorService.submit(() -> {
executeConsumerTask(() -> {
RocketmqCollectData.ConsumerInfo consumerInfo = new RocketmqCollectData.ConsumerInfo();
consumerInfoList.add(consumerInfo);
consumerInfo.setConsumerGroup(consumerGroup);
@@ -369,4 +367,18 @@ public class RocketmqSingleCollectImpl extends AbstractCollect implements Dispos
builder.addValueRow(valueRowBuilder.build());
}
}
void executeConsumerTask(Runnable runnable) {
executorService.execute(runnable);
}
private static void require(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
private static void requireHasText(String value, String message) {
require(value != null && !value.trim().isEmpty(), message);
}
}
@@ -19,12 +19,20 @@ package org.apache.hertzbeat.collector.collect.rocketmq;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.RocketmqProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -39,6 +47,13 @@ public class RocketmqSingleCollectTest {
collect = new RocketmqSingleCollectImpl();
}
@AfterEach
void tearDown() {
if (collect != null) {
collect.destroy();
}
}
@Test
void preCheck() {
// metrics is null
@@ -97,4 +112,51 @@ public class RocketmqSingleCollectTest {
void supportProtocol() {
assertEquals(DispatchConstants.PROTOCOL_ROCKETMQ, collect.supportProtocol());
}
@Test
void executeConsumerTaskRunsOnVirtualThread() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
collect.executeConsumerTask(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void executeConsumerTaskDiscardsOldestWhenQueueIsFull() throws InterruptedException {
ManagedExecutor executor = ManagedExecutors.newDiscardOldestVirtualExecutor("rocketmq-test",
"rocketmq-test-", 1, 1, 1, (thread, throwable) -> {
});
RocketmqSingleCollectImpl testCollect = new RocketmqSingleCollectImpl(executor);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch thirdStarted = new CountDownLatch(1);
AtomicBoolean secondExecuted = new AtomicBoolean(false);
try {
testCollect.executeConsumerTask(() -> {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
testCollect.executeConsumerTask(() -> secondExecuted.set(true));
testCollect.executeConsumerTask(thirdStarted::countDown);
releaseFirst.countDown();
assertTrue(thirdStarted.await(5, TimeUnit.SECONDS));
assertFalse(secondExecuted.get());
} finally {
releaseFirst.countDown();
testCollect.destroy();
}
}
}
+1
View File
@@ -28,6 +28,7 @@
<artifactId>hertzbeat-common-core</artifactId>
<name>${project.artifactId}</name>
<description>Framework-agnostic shared runtime models, utilities, and protocol support.</description>
<dependencies>
<!-- Tool dependencies -->
@@ -0,0 +1,39 @@
/*
* 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.common.concurrent;
/**
* Task admission mode for managed executors.
*/
public enum AdmissionMode {
/**
* Start each task on its own virtual thread without an executor-level concurrency cap.
*/
UNBOUNDED_VT,
/**
* Reject immediately when the configured concurrency cap has been reached.
*/
LIMIT_AND_REJECT,
/**
* Block the submitter until a permit is available.
*/
LIMIT_AND_BLOCK
}
@@ -0,0 +1,48 @@
/*
* 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.common.concurrent;
import java.util.concurrent.RejectedExecutionException;
/**
* Generic background task executor abstraction for runtime components.
*/
public interface BackgroundTaskExecutor {
/**
* Execute a short-lived task.
*
* @param runnable task
* @throws RejectedExecutionException when execution is rejected
*/
void execute(Runnable runnable) throws RejectedExecutionException;
/**
* Execute a long-lived background task.
*
* @param runnable task
*/
void executeLongRunning(Runnable runnable);
/**
* Release executor resources.
*
* @throws Exception close exception
*/
void destroy() throws Exception;
}
@@ -0,0 +1,36 @@
/*
* 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.common.concurrent;
import java.util.concurrent.Executor;
/**
* Executor with a stable logical name and close semantics.
*/
public interface ManagedExecutor extends Executor, AutoCloseable {
/**
* Logical executor name for logging and metrics tags.
*
* @return executor name
*/
String name();
@Override
void close();
}
@@ -0,0 +1,428 @@
/*
* 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.common.concurrent;
import java.util.ArrayDeque;
import java.util.Objects;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Factory methods for managed executors.
*/
public final class ManagedExecutors {
private ManagedExecutors() {
}
/**
* Wrap an existing executor service.
*
* @param name executor name
* @param executorService delegate executor service
* @return managed executor wrapper
*/
public static ManagedExecutor wrap(String name, ExecutorService executorService) {
return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT);
}
/**
* Create a per-task virtual-thread executor with optional admission control.
*
* @param name executor name
* @param threadNamePrefix thread name prefix
* @param mode admission mode
* @param maxConcurrentTasks max concurrent tasks for limited modes
* @param handler uncaught exception handler
* @return managed executor
*/
public static ManagedExecutor newVirtualExecutor(String name, String threadNamePrefix, AdmissionMode mode,
int maxConcurrentTasks, Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = Thread.ofVirtual()
.name(threadNamePrefix, 0)
.uncaughtExceptionHandler(handler)
.factory();
ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory);
return new DefaultManagedExecutor(name, executorService, semaphore(mode, maxConcurrentTasks), mode);
}
/**
* Create a platform-thread-per-task executor for a small number of long-running tasks.
*
* @param name executor name
* @param threadNamePrefix thread name prefix
* @param handler uncaught exception handler
* @return managed executor
*/
public static ManagedExecutor newPlatformExecutor(String name, String threadNamePrefix,
Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = Thread.ofPlatform()
.daemon(true)
.name(threadNamePrefix, 0)
.uncaughtExceptionHandler(handler)
.factory();
ExecutorService executorService = Executors.newThreadPerTaskExecutor(threadFactory);
return new DefaultManagedExecutor(name, executorService, null, AdmissionMode.UNBOUNDED_VT);
}
/**
* Create a queued executor that preserves queue semantics while executing tasks on virtual threads.
*
* @param name executor name
* @param threadNamePrefix virtual-thread name prefix
* @param maxConcurrentTasks max concurrent tasks
* @param queueCapacity queue capacity, {@code <= 0} means unbounded
* @param handler uncaught exception handler
* @return managed executor
*/
public static ManagedExecutor newQueuedVirtualExecutor(String name, String threadNamePrefix, int maxConcurrentTasks,
int queueCapacity, Thread.UncaughtExceptionHandler handler) {
if (maxConcurrentTasks <= 0) {
throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for queued executors");
}
return new QueuedVirtualManagedExecutor(name, threadNamePrefix, maxConcurrentTasks, queueCapacity, handler);
}
/**
* Create a virtual-thread executor that preserves {@link java.util.concurrent.ThreadPoolExecutor}
* core/max/queue semantics with discard-oldest overflow handling.
*
* @param name executor name
* @param threadNamePrefix virtual-thread name prefix
* @param coreConcurrentTasks core concurrent tasks
* @param maxConcurrentTasks max concurrent tasks
* @param queueCapacity queue capacity
* @param handler uncaught exception handler
* @return managed executor
*/
public static ManagedExecutor newDiscardOldestVirtualExecutor(String name, String threadNamePrefix,
int coreConcurrentTasks, int maxConcurrentTasks,
int queueCapacity,
Thread.UncaughtExceptionHandler handler) {
if (coreConcurrentTasks <= 0) {
throw new IllegalArgumentException("coreConcurrentTasks must be greater than zero");
}
if (maxConcurrentTasks < coreConcurrentTasks) {
throw new IllegalArgumentException("maxConcurrentTasks must be greater than or equal to coreConcurrentTasks");
}
if (queueCapacity <= 0) {
throw new IllegalArgumentException("queueCapacity must be greater than zero");
}
return new DiscardOldestVirtualManagedExecutor(name, threadNamePrefix, coreConcurrentTasks,
maxConcurrentTasks, queueCapacity, handler);
}
private static Semaphore semaphore(AdmissionMode mode, int maxConcurrentTasks) {
if (mode == AdmissionMode.UNBOUNDED_VT) {
return null;
}
if (maxConcurrentTasks <= 0) {
throw new IllegalArgumentException("maxConcurrentTasks must be greater than zero for limited executors");
}
return new Semaphore(maxConcurrentTasks);
}
private static final class DefaultManagedExecutor implements ManagedExecutor {
private final String name;
private final ExecutorService delegate;
private final Semaphore permits;
private final AdmissionMode admissionMode;
private DefaultManagedExecutor(String name, ExecutorService delegate, Semaphore permits,
AdmissionMode admissionMode) {
this.name = Objects.requireNonNull(name, "name");
this.delegate = Objects.requireNonNull(delegate, "delegate");
this.permits = permits;
this.admissionMode = Objects.requireNonNull(admissionMode, "admissionMode");
}
@Override
public String name() {
return name;
}
@Override
public void execute(Runnable command) {
Objects.requireNonNull(command, "command");
acquirePermit();
boolean submitted = false;
try {
delegate.execute(() -> {
try {
command.run();
} finally {
releasePermit();
}
});
submitted = true;
} finally {
if (!submitted) {
releasePermit();
}
}
}
@Override
public void close() {
delegate.shutdownNow();
}
private void acquirePermit() {
if (permits == null) {
return;
}
switch (admissionMode) {
case LIMIT_AND_REJECT:
if (!permits.tryAcquire()) {
throw new RejectedExecutionException(name + " rejected task because concurrency limit was reached");
}
break;
case LIMIT_AND_BLOCK:
try {
permits.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException(name + " interrupted while waiting for an execution permit", e);
}
break;
case UNBOUNDED_VT:
break;
default:
throw new IllegalStateException("Unsupported admission mode: " + admissionMode);
}
}
private void releasePermit() {
if (permits != null) {
permits.release();
}
}
}
private static final class QueuedVirtualManagedExecutor implements ManagedExecutor {
private final String name;
private final ExecutorService delegate;
private final ExecutorService dispatcher;
private final BlockingDeque<Runnable> queue;
private final Semaphore permits;
private final Semaphore permitSignals;
private final AtomicBoolean closed;
private QueuedVirtualManagedExecutor(String name, String threadNamePrefix, int maxConcurrentTasks,
int queueCapacity, Thread.UncaughtExceptionHandler handler) {
this.name = Objects.requireNonNull(name, "name");
ThreadFactory virtualFactory = Thread.ofVirtual()
.name(threadNamePrefix, 0)
.uncaughtExceptionHandler(handler)
.factory();
this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory);
this.queue = queueCapacity > 0 ? new LinkedBlockingDeque<>(queueCapacity) : new LinkedBlockingDeque<>();
this.permits = new Semaphore(maxConcurrentTasks);
this.permitSignals = new Semaphore(0);
this.closed = new AtomicBoolean(false);
ThreadFactory dispatcherFactory = Thread.ofPlatform()
.daemon(true)
.name(threadNamePrefix + "dispatcher-", 0)
.uncaughtExceptionHandler(handler)
.factory();
this.dispatcher = Executors.newSingleThreadExecutor(dispatcherFactory);
this.dispatcher.execute(this::dispatchLoop);
}
@Override
public String name() {
return name;
}
@Override
public void execute(Runnable command) {
Objects.requireNonNull(command, "command");
if (closed.get()) {
throw new RejectedExecutionException(name + " rejected task because executor is closed");
}
if (!queue.offerLast(command)) {
throw new RejectedExecutionException(name + " rejected task because queue capacity was reached");
}
}
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
return;
}
dispatcher.shutdownNow();
delegate.shutdownNow();
queue.clear();
}
private void dispatchLoop() {
try {
while (!Thread.currentThread().isInterrupted()) {
Runnable command = queue.takeFirst();
if (!permits.tryAcquire()) {
queue.putFirst(command);
permitSignals.acquire();
continue;
}
submit(command);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void submit(Runnable command) {
boolean submitted = false;
try {
delegate.execute(() -> {
try {
command.run();
} finally {
permits.release();
permitSignals.release();
}
});
submitted = true;
} finally {
if (!submitted) {
permits.release();
permitSignals.release();
}
}
}
}
private static final class DiscardOldestVirtualManagedExecutor implements ManagedExecutor {
private final String name;
private final ExecutorService delegate;
private final int coreConcurrentTasks;
private final int maxConcurrentTasks;
private final ArrayDeque<Runnable> queue;
private final int queueCapacity;
private final Object lock;
private boolean closed;
private int runningTasks;
private DiscardOldestVirtualManagedExecutor(String name, String threadNamePrefix, int coreConcurrentTasks,
int maxConcurrentTasks, int queueCapacity,
Thread.UncaughtExceptionHandler handler) {
this.name = Objects.requireNonNull(name, "name");
ThreadFactory virtualFactory = Thread.ofVirtual()
.name(threadNamePrefix, 0)
.uncaughtExceptionHandler(handler)
.factory();
this.delegate = Executors.newThreadPerTaskExecutor(virtualFactory);
this.coreConcurrentTasks = coreConcurrentTasks;
this.maxConcurrentTasks = maxConcurrentTasks;
this.queueCapacity = queueCapacity;
this.queue = new ArrayDeque<>(queueCapacity);
this.lock = new Object();
this.closed = false;
this.runningTasks = 0;
}
@Override
public String name() {
return name;
}
@Override
public void execute(Runnable command) {
Objects.requireNonNull(command, "command");
Runnable taskToStart = null;
synchronized (lock) {
if (closed) {
throw new RejectedExecutionException(name + " rejected task because executor is closed");
}
if (runningTasks < coreConcurrentTasks) {
runningTasks++;
taskToStart = command;
} else if (queue.size() < queueCapacity) {
queue.offerLast(command);
return;
} else if (runningTasks < maxConcurrentTasks) {
runningTasks++;
taskToStart = command;
} else {
queue.pollFirst();
queue.offerLast(command);
return;
}
}
submit(taskToStart);
}
@Override
public void close() {
synchronized (lock) {
if (closed) {
return;
}
closed = true;
queue.clear();
}
delegate.shutdownNow();
}
private void submit(Runnable command) {
boolean submitted = false;
try {
delegate.execute(() -> {
try {
command.run();
} finally {
onTaskComplete();
}
});
submitted = true;
} finally {
if (!submitted) {
synchronized (lock) {
runningTasks--;
}
throw new RejectedExecutionException(name + " rejected task because delegate submission failed");
}
}
}
private void onTaskComplete() {
Runnable nextTask = null;
synchronized (lock) {
if (closed) {
runningTasks--;
return;
}
nextTask = queue.pollFirst();
if (nextTask == null) {
runningTasks--;
return;
}
}
submit(nextTask);
}
}
}
@@ -0,0 +1,201 @@
/*
* 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.common.config;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
/**
* Framework-agnostic virtual-thread runtime configuration.
*/
public record VirtualThreadProperties(
boolean enabled,
PoolProperties collector,
PoolProperties common,
PoolProperties manager,
AlerterProperties alerter,
PoolProperties warehouse,
AsyncProperties async) {
private static final int DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS = 512;
private static final int DEFAULT_MANAGER_MAX_CONCURRENT_JOBS = 10;
private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS = 64;
private static final int DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS = 10;
private static final int DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL = 4;
public VirtualThreadProperties {
collector = normalizePool(collector, PoolProperties.collectorDefaults());
common = common == null ? PoolProperties.commonDefaults() : common;
manager = normalizePool(manager, PoolProperties.managerDefaults());
alerter = alerter == null ? AlerterProperties.defaults() : alerter;
warehouse = warehouse == null ? PoolProperties.warehouseDefaults() : warehouse;
async = async == null ? AsyncProperties.defaults() : async;
}
public VirtualThreadProperties() {
this(true, PoolProperties.collectorDefaults(), PoolProperties.commonDefaults(),
PoolProperties.managerDefaults(), AlerterProperties.defaults(),
PoolProperties.warehouseDefaults(), AsyncProperties.defaults());
}
/**
* Create a detached properties instance with runtime defaults.
*
* @return defaults instance
*/
public static VirtualThreadProperties defaults() {
return new VirtualThreadProperties();
}
/**
* Pool-level configuration.
*/
public record PoolProperties(
AdmissionMode mode,
int maxConcurrentJobs) {
public PoolProperties {
mode = mode == null ? AdmissionMode.UNBOUNDED_VT : mode;
}
public PoolProperties() {
this(AdmissionMode.UNBOUNDED_VT, 0);
}
public static PoolProperties collectorDefaults() {
return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, defaultCollectorConcurrency());
}
public static PoolProperties warehouseDefaults() {
return new PoolProperties();
}
public static PoolProperties commonDefaults() {
return new PoolProperties();
}
public static PoolProperties managerDefaults() {
return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_MANAGER_MAX_CONCURRENT_JOBS);
}
public static PoolProperties alerterNotifyDefaults() {
return new PoolProperties(AdmissionMode.LIMIT_AND_REJECT, DEFAULT_NOTIFY_MAX_CONCURRENT_JOBS);
}
private static int defaultCollectorConcurrency() {
return DEFAULT_COLLECTOR_MAX_CONCURRENT_JOBS;
}
}
/**
* Alerter-specific executor configuration.
*/
public record AlerterProperties(
PoolProperties notifyPool,
int periodicMaxConcurrentJobs,
QueueProperties logWorker,
QueueProperties reduce,
QueueProperties windowEvaluator,
int notifyMaxConcurrentPerChannel) {
public AlerterProperties {
notifyPool = normalizePool(notifyPool, PoolProperties.alerterNotifyDefaults());
periodicMaxConcurrentJobs = periodicMaxConcurrentJobs <= 0
? DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS : periodicMaxConcurrentJobs;
logWorker = normalizeQueue(logWorker, QueueProperties.logWorkerDefaults());
reduce = normalizeQueue(reduce, QueueProperties.reduceDefaults());
windowEvaluator = normalizeQueue(windowEvaluator, QueueProperties.windowEvaluatorDefaults());
notifyMaxConcurrentPerChannel = notifyMaxConcurrentPerChannel <= 0
? DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL : notifyMaxConcurrentPerChannel;
}
public AlerterProperties() {
this(PoolProperties.alerterNotifyDefaults(), DEFAULT_PERIODIC_MAX_CONCURRENT_JOBS,
QueueProperties.logWorkerDefaults(), QueueProperties.reduceDefaults(),
QueueProperties.windowEvaluatorDefaults(), DEFAULT_NOTIFY_MAX_CONCURRENT_PER_CHANNEL);
}
public static AlerterProperties defaults() {
return new AlerterProperties();
}
}
/**
* Queue-preserving executor configuration.
*/
public record QueueProperties(
int maxConcurrentJobs,
int queueCapacity) {
public QueueProperties() {
this(0, 0);
}
public static QueueProperties reduceDefaults() {
return new QueueProperties(2, 0);
}
public static QueueProperties logWorkerDefaults() {
return new QueueProperties(10, 1000);
}
public static QueueProperties windowEvaluatorDefaults() {
return new QueueProperties(2, 0);
}
}
/**
* Async executor configuration.
*/
public record AsyncProperties(
boolean enabled,
int concurrencyLimit,
boolean rejectWhenLimitReached,
long taskTerminationTimeout) {
public AsyncProperties() {
this(true, 256, true, 5000L);
}
public static AsyncProperties defaults() {
return new AsyncProperties();
}
}
private static PoolProperties normalizePool(PoolProperties configured, PoolProperties defaults) {
if (configured == null) {
return defaults;
}
if (configured.mode() != AdmissionMode.UNBOUNDED_VT && configured.maxConcurrentJobs() <= 0) {
return new PoolProperties(configured.mode(), defaults.maxConcurrentJobs());
}
return configured;
}
private static QueueProperties normalizeQueue(QueueProperties configured, QueueProperties defaults) {
if (configured == null) {
return defaults;
}
int maxConcurrentJobs = configured.maxConcurrentJobs() <= 0
? defaults.maxConcurrentJobs() : configured.maxConcurrentJobs();
int queueCapacity = configured.queueCapacity();
if (queueCapacity <= 0 && defaults.queueCapacity() > 0) {
queueCapacity = defaults.queueCapacity();
}
return new QueueProperties(maxConcurrentJobs, queueCapacity);
}
}
@@ -21,17 +21,13 @@ import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* SMS configuration
* Framework-agnostic SMS runtime configuration.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "alerter.sms")
public class SmsConfig {
/**
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.common.entity.job;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Collections;
import java.util.Comparator;
@@ -35,13 +34,11 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.util.CollectionUtils;
/**
* Collect task details
* Collect task details.
*/
@Data
@AllArgsConstructor
@@ -51,15 +48,15 @@ import org.springframework.util.CollectionUtils;
public class Job {
/**
* Task Job id
* Task Job id.
*/
private long id;
/**
* Tenant id
* Tenant id.
*/
private long tenantId = 0;
/**
* Monitoring Task ID
* Monitoring Task ID.
*/
private long monitorId;
/**
@@ -68,11 +65,11 @@ public class Job {
*/
private Map<String, String> metadata;
/**
* bind labels
* bind labels.
*/
private Map<String, String> labels;
/**
* bind annotations
* bind annotations.
*/
private Map<String, String> annotations;
/**
@@ -86,12 +83,12 @@ public class Job {
*/
private String category;
/**
* Type of monitoring eg: linux | mysql | jvm
* Type of monitoring eg: linux | mysql | jvm.
*/
private String app;
/**
* The internationalized name of the monitoring type
* PING CONNECT
* PING CONNECT.
*/
private Map<String, String> name;
/**
@@ -101,55 +98,54 @@ public class Job {
*/
private Map<String, String> help;
/**
* The monitor help link
* The monitor help link.
*/
private Map<String, String> helpLink;
/**
* Task dispatch start timestamp
* Task dispatch start timestamp.
*/
private long timestamp;
/**
* Default task collection time interval (unit: second) eg: 30,60,600
* Default task collection time interval (unit: second) eg: 30,60,600.
*/
private long defaultInterval = 600L;
/**
* Refresh time list for one cycle of the job
* Refresh time list for one cycle of the job.
*/
private ConcurrentLinkedDeque<Long> intervals;
/**
* Whether it is a recurring periodic task true is yes, false is no
* Whether it is a recurring periodic task true is yes, false is no.
*/
private boolean isCyclic = false;
/**
* monitor input need params
* monitor input need params.
*/
private List<ParamDefine> params;
private List<RuntimeParamDefine> params;
/**
* Metrics configuration eg: cpu memory
* eg: cpu memory
* Metrics configuration eg: cpu memory.
*/
private List<Metrics> metrics;
/**
* Monitoring configuration parameter properties and values eg: username password timeout host
* Monitoring configuration parameter properties and values eg: username password timeout host.
*/
private List<Configmap> configmap;
/**
* Whether it is a service discovery job, true is yes, false is no
* Whether it is a service discovery job, true is yes, false is no.
*/
private boolean isSd = false;
/**
* Whether to use the Prometheus proxy
* Whether to use the Prometheus proxy.
*/
private boolean prometheusProxyMode = false;
/**
* Scheduling type: interval or cron
* Scheduling type: interval or cron.
*/
private String scheduleType = "interval";
/**
* Cron expression for scheduling, used when scheduleType is "cron"
* Cron expression for scheduling, used when scheduleType is "cron".
*/
private String cronExpression = null;
@@ -160,7 +156,7 @@ public class Job {
private Map<String, Configmap> envConfigmaps;
/**
* collector use - timestamp when the task was scheduled by the time wheel
* collector use - timestamp when the task was scheduled by the time wheel.
*/
@JsonIgnore
private transient long dispatchTime;
@@ -179,13 +175,13 @@ public class Job {
private transient LinkedList<Set<Metrics>> priorMetrics;
/**
* collector use - Temporarily store one-time task metrics response data
* collector use - Temporarily store one-time task metrics response data.
*/
@JsonIgnore
private transient List<CollectRep.MetricsData> responseDataTemp;
/**
* collector use - construct to initialize metrics execution view
* collector use - construct to initialize metrics execution view.
*/
public synchronized void constructPriorMetrics() {
long now = System.currentTimeMillis();
@@ -232,7 +228,7 @@ public class Job {
}
/**
* collector use - to get the next set of priority metric group tasks
* collector use - to get the next set of priority metric group tasks.
*
* @param metrics Current Metrics
* @param first Is it the first time to get
@@ -314,7 +310,7 @@ public class Job {
}
/**
* The greatest common divisor
* The greatest common divisor.
*/
public static long gcd(long a, long b) {
while (b != 0) {
@@ -326,7 +322,7 @@ public class Job {
}
/**
* The least common multiple
* The least common multiple.
*/
public static long lcm(List<Long> array) {
if (array != null && !array.isEmpty()) {
@@ -340,9 +336,8 @@ public class Job {
}
/**
*
* @param metricsIntervals A unique list composed of intervals for all metrics
* Generate a list of refresh intervals for metric collection
* Generate a list of refresh intervals for metric collection.
*/
public synchronized void generateMetricsIntervals(List<Long> metricsIntervals) {
// 1. To find the least common multiple (LCM) of all metric refresh intervals
@@ -368,7 +363,7 @@ public class Job {
}
public synchronized long getInterval() {
if (!CollectionUtils.isEmpty(this.intervals)) {
if (this.intervals != null && !this.intervals.isEmpty()) {
Long interval = this.intervals.removeFirst();
if (interval != null) {
this.intervals.addLast(interval);
@@ -17,38 +17,60 @@
package org.apache.hertzbeat.common.entity.job;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Monitoring configuration parameter properties and values
* During the process, you need to replace the content with the identifier ^_^key^_^
* in the protocol configuration parameter with the real value in the configuration parameter
* Framework-agnostic parameter definition used by runtime templates and jobs.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Configmap implements Serializable {
public class RuntimeParamDefine {
private String app;
private Map<String, String> name;
private String field;
private String type;
private boolean required = false;
private String defaultValue;
private String placeholder;
private String range;
private Short limit;
private List<Option> options;
private String keyAlias;
private String valueAlias;
private boolean hide = false;
private Map<String, List<Object>> depend;
/**
* Parameter key, replace the content with the identifier ^^_key_^^ in the protocol
* configuration parameter with the real value in the configuration parameter
* Runtime option definition.
*/
private String key;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static final class Option {
/**
* parameter value
*/
private Object value;
private String label;
/**
* Parameter type
* 0: number 1: string 2: encrypted string 3: json string mapped by map
* number,string,secret
*/
private byte type = 1;
private String value;
}
}
@@ -81,7 +81,7 @@ public class SshTunnel implements CommonRequestProtocol, Protocol {
@Override
public boolean isInvalid() {
// todo: add
return true;
// todo add
return false;
}
}
@@ -17,23 +17,20 @@
* under the License.
*/
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: cluster_msg.proto
package org.apache.hertzbeat.common.entity.plugin;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine;
/**
* The configuration file of the plugin, including parameters and other information
* The configuration file of the plugin, including parameters and other information.
*/
@Data
public class PluginConfig {
private List<ParamDefine> params;
private List<RuntimeParamDefine> params;
public PluginConfig() {
this.params = new ArrayList<>();
@@ -25,4 +25,4 @@ public class AlertExpressionException extends RuntimeException {
public AlertExpressionException(String message) {
super(message);
}
}
}
@@ -36,4 +36,4 @@ public class CommonDataQueueUnknownException extends RuntimeException {
public CommonDataQueueUnknownException(Throwable cause) {
super(cause);
}
}
}
@@ -26,4 +26,4 @@ public class ExpressionVisitorException extends RuntimeException {
public ExpressionVisitorException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -0,0 +1,184 @@
/*
* 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.common.concurrent;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link ManagedExecutors}.
*/
class ManagedExecutorsTest {
@Test
void shouldRunTaskOnVirtualThread() throws Exception {
ManagedExecutor executor = ManagedExecutors.newVirtualExecutor("test", "test-vt-",
AdmissionMode.UNBOUNDED_VT, 0, (thread, throwable) -> {
});
try {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
executor.execute(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
} finally {
executor.close();
}
}
@Test
void shouldRejectTaskWhenAdmissionLimitReached() throws Exception {
ManagedExecutor executor = ManagedExecutors.newVirtualExecutor("limited", "limited-vt-",
AdmissionMode.LIMIT_AND_REJECT, 1, (thread, throwable) -> {
});
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
try {
executor.execute(() -> {
started.countDown();
try {
release.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(started.await(5, TimeUnit.SECONDS));
assertThrows(RejectedExecutionException.class, () -> executor.execute(() -> {
}));
} finally {
release.countDown();
executor.close();
}
}
@Test
void shouldQueueTasksWhileKeepingVirtualThreadExecution() throws Exception {
ManagedExecutor executor = ManagedExecutors.newQueuedVirtualExecutor("queued", "queued-vt-",
1, 0, (thread, throwable) -> {
});
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
AtomicBoolean firstVirtual = new AtomicBoolean(false);
AtomicBoolean secondVirtual = new AtomicBoolean(false);
try {
executor.execute(() -> {
firstVirtual.set(Thread.currentThread().isVirtual());
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
executor.execute(() -> {
secondVirtual.set(Thread.currentThread().isVirtual());
secondStarted.countDown();
});
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
releaseFirst.countDown();
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
assertTrue(firstVirtual.get());
assertTrue(secondVirtual.get());
} finally {
releaseFirst.countDown();
executor.close();
}
}
@Test
void shouldRejectTaskWhenQueuedExecutorCapacityReached() throws Exception {
ManagedExecutor executor = ManagedExecutors.newQueuedVirtualExecutor("queued", "queued-vt-",
1, 1, (thread, throwable) -> {
});
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch secondStarted = new CountDownLatch(1);
try {
executor.execute(() -> {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
executor.execute(secondStarted::countDown);
assertFalse(secondStarted.await(200, TimeUnit.MILLISECONDS));
assertThrows(RejectedExecutionException.class, () -> executor.execute(() -> {
}));
} finally {
releaseFirst.countDown();
executor.close();
}
}
@Test
void shouldDiscardOldestTaskWhenDiscardOldestExecutorQueueIsFull() throws Exception {
ManagedExecutor executor = ManagedExecutors.newDiscardOldestVirtualExecutor("discard-oldest",
"discard-oldest-vt-", 1, 1, 1, (thread, throwable) -> {
});
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch thirdStarted = new CountDownLatch(1);
AtomicBoolean secondExecuted = new AtomicBoolean(false);
AtomicBoolean thirdVirtual = new AtomicBoolean(false);
try {
executor.execute(() -> {
firstStarted.countDown();
try {
releaseFirst.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
executor.execute(() -> secondExecuted.set(true));
executor.execute(() -> {
thirdVirtual.set(Thread.currentThread().isVirtual());
thirdStarted.countDown();
});
releaseFirst.countDown();
assertTrue(thirdStarted.await(5, TimeUnit.SECONDS));
assertFalse(secondExecuted.get());
assertTrue(thirdVirtual.get());
} finally {
releaseFirst.countDown();
executor.close();
}
}
}
+1
View File
@@ -28,6 +28,7 @@
<artifactId>hertzbeat-common-spring</artifactId>
<name>${project.artifactId}</name>
<description>Spring Boot, configuration, validation, and JPA integration built on common-core.</description>
<dependencies>
<!-- hertzbeat-common-core dependency -->
@@ -21,6 +21,7 @@ import org.apache.hertzbeat.common.constants.ConfigConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
/**
@@ -31,6 +32,11 @@ import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG
+ SignConstants.DOT
+ ConfigConstants.FunctionModuleConstants.COMMON)
@EnableConfigurationProperties(CommonProperties.class)
@EnableConfigurationProperties({CommonProperties.class, VirtualThreadPropertiesBinding.class, SmsConfigBinding.class})
public class CommonConfig {
@Bean
public VirtualThreadProperties virtualThreadProperties(VirtualThreadPropertiesBinding binding) {
return binding.toRuntimeProperties();
}
}
@@ -0,0 +1,28 @@
/*
* 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.common.config;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Spring Boot binding adapter for {@link SmsConfig}.
*/
@ConfigurationProperties(prefix = "alerter.sms")
public class SmsConfigBinding extends SmsConfig {
}
@@ -0,0 +1,177 @@
/*
* 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.common.config;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.context.properties.bind.Name;
/**
* Spring Boot binding adapter for {@link VirtualThreadProperties}.
*/
@ConfigurationProperties(prefix = "hertzbeat.vthreads")
public record VirtualThreadPropertiesBinding(
@DefaultValue("true") boolean enabled,
PoolProperties collector,
PoolProperties common,
PoolProperties manager,
AlerterProperties alerter,
PoolProperties warehouse,
AsyncProperties async) {
@ConstructorBinding
public VirtualThreadPropertiesBinding {
VirtualThreadProperties runtimeProperties = new VirtualThreadProperties(enabled,
toRuntimePool(collector),
toRuntimePool(common),
toRuntimePool(manager),
toRuntimeAlerter(alerter),
toRuntimePool(warehouse),
toRuntimeAsync(async));
enabled = runtimeProperties.enabled();
collector = PoolProperties.fromRuntime(runtimeProperties.collector());
common = PoolProperties.fromRuntime(runtimeProperties.common());
manager = PoolProperties.fromRuntime(runtimeProperties.manager());
alerter = AlerterProperties.fromRuntime(runtimeProperties.alerter());
warehouse = PoolProperties.fromRuntime(runtimeProperties.warehouse());
async = AsyncProperties.fromRuntime(runtimeProperties.async());
}
public VirtualThreadProperties toRuntimeProperties() {
return new VirtualThreadProperties(enabled,
toRuntimePool(collector),
toRuntimePool(common),
toRuntimePool(manager),
toRuntimeAlerter(alerter),
toRuntimePool(warehouse),
toRuntimeAsync(async));
}
/**
* Pool-level binding model.
*/
public record PoolProperties(
@DefaultValue("UNBOUNDED_VT") AdmissionMode mode,
@DefaultValue("0") int maxConcurrentJobs) {
@ConstructorBinding
public PoolProperties {
mode = mode == null ? AdmissionMode.UNBOUNDED_VT : mode;
}
static PoolProperties fromRuntime(VirtualThreadProperties.PoolProperties runtimeProperties) {
return runtimeProperties == null ? null
: new PoolProperties(runtimeProperties.mode(), runtimeProperties.maxConcurrentJobs());
}
}
/**
* Alerter-specific binding model.
*/
public record AlerterProperties(
@Name("notify") PoolProperties notifyPool,
@DefaultValue("10") int periodicMaxConcurrentJobs,
QueueProperties logWorker,
QueueProperties reduce,
QueueProperties windowEvaluator,
@DefaultValue("4") int notifyMaxConcurrentPerChannel) {
@ConstructorBinding
public AlerterProperties {
}
static AlerterProperties fromRuntime(VirtualThreadProperties.AlerterProperties runtimeProperties) {
return runtimeProperties == null ? null
: new AlerterProperties(
PoolProperties.fromRuntime(runtimeProperties.notifyPool()),
runtimeProperties.periodicMaxConcurrentJobs(),
QueueProperties.fromRuntime(runtimeProperties.logWorker()),
QueueProperties.fromRuntime(runtimeProperties.reduce()),
QueueProperties.fromRuntime(runtimeProperties.windowEvaluator()),
runtimeProperties.notifyMaxConcurrentPerChannel());
}
}
/**
* Queue-preserving binding model.
*/
public record QueueProperties(
@DefaultValue("0") int maxConcurrentJobs,
@DefaultValue("0") int queueCapacity) {
@ConstructorBinding
public QueueProperties {
}
static QueueProperties fromRuntime(VirtualThreadProperties.QueueProperties runtimeProperties) {
return runtimeProperties == null ? null
: new QueueProperties(runtimeProperties.maxConcurrentJobs(), runtimeProperties.queueCapacity());
}
}
/**
* Async executor binding model.
*/
public record AsyncProperties(
@DefaultValue("true") boolean enabled,
@DefaultValue("256") int concurrencyLimit,
@DefaultValue("true") boolean rejectWhenLimitReached,
@DefaultValue("5000") long taskTerminationTimeout) {
@ConstructorBinding
public AsyncProperties {
}
static AsyncProperties fromRuntime(VirtualThreadProperties.AsyncProperties runtimeProperties) {
return runtimeProperties == null ? null
: new AsyncProperties(runtimeProperties.enabled(), runtimeProperties.concurrencyLimit(),
runtimeProperties.rejectWhenLimitReached(), runtimeProperties.taskTerminationTimeout());
}
}
private static VirtualThreadProperties.PoolProperties toRuntimePool(PoolProperties poolProperties) {
return poolProperties == null ? null
: new VirtualThreadProperties.PoolProperties(poolProperties.mode(), poolProperties.maxConcurrentJobs());
}
private static VirtualThreadProperties.AlerterProperties toRuntimeAlerter(AlerterProperties alerterProperties) {
return alerterProperties == null ? null
: new VirtualThreadProperties.AlerterProperties(
toRuntimePool(alerterProperties.notifyPool()),
alerterProperties.periodicMaxConcurrentJobs(),
toRuntimeQueue(alerterProperties.logWorker()),
toRuntimeQueue(alerterProperties.reduce()),
toRuntimeQueue(alerterProperties.windowEvaluator()),
alerterProperties.notifyMaxConcurrentPerChannel());
}
private static VirtualThreadProperties.QueueProperties toRuntimeQueue(QueueProperties queueProperties) {
return queueProperties == null ? null
: new VirtualThreadProperties.QueueProperties(queueProperties.maxConcurrentJobs(),
queueProperties.queueCapacity());
}
private static VirtualThreadProperties.AsyncProperties toRuntimeAsync(AsyncProperties asyncProperties) {
return asyncProperties == null ? null
: new VirtualThreadProperties.AsyncProperties(asyncProperties.enabled(),
asyncProperties.concurrencyLimit(), asyncProperties.rejectWhenLimitReached(),
asyncProperties.taskTerminationTimeout());
}
}
@@ -1,87 +0,0 @@
/*
* 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.common.entity.job;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
/**
* ssh tunnel
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SshTunnel implements CommonRequestProtocol, Protocol {
/**
* enable ssh tunnel
*/
private String enable = "false";
/**
* IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
*/
private String host;
/**
* Peer host port
*/
private String port = "22";
/**
* TIME OUT PERIOD
*/
private String timeout = "6000";
/**
* UserName
*/
private String username;
/**
* Password (optional)
*/
private String password;
/**
* Private key (optional)
*/
private String privateKey;
/**
* private key passphrase (optional)
*/
private String privateKeyPassphrase;
/**
* share connection session
*/
private String shareConnection = "true";
@Override
public boolean isInvalid() {
// todo add
return false;
}
}
@@ -18,13 +18,19 @@
package org.apache.hertzbeat.common.support;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.concurrent.BackgroundTaskExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -32,24 +38,55 @@ import org.springframework.stereotype.Component;
*/
@Component
@Slf4j
public class CommonThreadPool implements DisposableBean {
public class CommonThreadPool implements BackgroundTaskExecutor, DisposableBean {
private ThreadPoolExecutor workerExecutor;
private final ManagedExecutor workerExecutor;
private final ManagedExecutor longRunningExecutor;
public CommonThreadPool() {
initWorkExecutor();
this(VirtualThreadProperties.defaults());
}
private void initWorkExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("common executor has uncaughtException.");
@Autowired
public CommonThreadPool(VirtualThreadProperties virtualThreadProperties) {
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
this.workerExecutor = createWorkerExecutor(properties);
this.longRunningExecutor = createLongRunningExecutor(properties, workerExecutor);
}
private ManagedExecutor createWorkerExecutor(VirtualThreadProperties properties) {
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
log.error("common executor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
};
if (properties.enabled()) {
VirtualThreadProperties.PoolProperties poolProperties = properties.common();
return ManagedExecutors.newVirtualExecutor("common-worker", "common-worker-",
poolProperties.mode(), poolProperties.maxConcurrentJobs(), handler);
}
return ManagedExecutors.wrap("common-worker", createLegacyExecutor(handler));
}
private ManagedExecutor createLongRunningExecutor(VirtualThreadProperties properties, ManagedExecutor fallback) {
if (!properties.enabled()) {
return fallback;
}
return ManagedExecutors.newPlatformExecutor("common-long-running", "common-long-running-",
(thread, throwable) -> {
log.error("common longRunningExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
});
}
private ExecutorService createLegacyExecutor(Thread.UncaughtExceptionHandler handler) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(handler)
.setDaemon(true)
.setNameFormat("common-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(1,
return new ThreadPoolExecutor(1,
Integer.MAX_VALUE,
10,
TimeUnit.SECONDS,
@@ -67,10 +104,20 @@ public class CommonThreadPool implements DisposableBean {
workerExecutor.execute(runnable);
}
/**
* Run a long-lived task outside of the short-task execution lane.
*
* @param runnable task
*/
public void executeLongRunning(Runnable runnable) {
longRunningExecutor.execute(runnable);
}
@Override
public void destroy() throws Exception {
if (workerExecutor != null) {
workerExecutor.shutdownNow();
workerExecutor.close();
if (longRunningExecutor != workerExecutor) {
longRunningExecutor.close();
}
}
}
@@ -1,169 +0,0 @@
/*
* 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.common.util;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.http.conn.util.InetAddressUtils;
import org.springframework.util.StringUtils;
/**
* ipv4 ipv6 domain util.
*/
@Slf4j
public final class IpDomainUtil {
private static final Pattern DOMAIN_PATTERN =
Pattern.compile("^[-\\w]+(\\.[-\\w]+)*$");
private static final String LOCALHOST = "localhost";
/**
* HTTP header schema.
*/
private static final Pattern DOMAIN_SCHEMA = Pattern.compile("^([hH][tT]{2}[pP]://|[hH][tT]{2}[pP][sS]://){1}[^\\s]*");
private IpDomainUtil() {
}
/**
* whether it is ip or domain.
* @param ipDomain ip domain string
* @return true-yes false-no
*/
public static boolean validateIpDomain(String ipDomain) {
if (ipDomain == null || !StringUtils.hasText(ipDomain)) {
return false;
}
ipDomain = ipDomain.trim();
if (LOCALHOST.equalsIgnoreCase(ipDomain)) {
return true;
}
if (InetAddressUtils.isIPv4Address(ipDomain)) {
return true;
}
if (InetAddressUtils.isIPv6Address(ipDomain)) {
return true;
}
return DOMAIN_PATTERN.matcher(ipDomain).matches();
}
/**
* if domain or ip has http / https schema.
* @param domainIp host
* @return true or false
*/
public static boolean isHasSchema(String domainIp) {
if (domainIp == null || !StringUtils.hasText(domainIp)) {
return false;
}
return DOMAIN_SCHEMA.matcher(domainIp).matches();
}
/**
* if instance has the port with mark
* @param instance instance ip:port
* @return true if has
*/
public static boolean isHasPortWithMark(String instance) {
if (instance == null || !StringUtils.hasText(instance)) {
return false;
}
String[] parts = instance.split(SignConstants.DOUBLE_MARK);
if (parts.length >= 2) {
String port = parts[parts.length - 1];
return CommonUtil.isNumeric(port);
}
return false;
}
/**
* get localhost IP.
* @return ip
*/
public static String getLocalhostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
InetAddress ip;
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = allNetInterfaces.nextElement();
if (!netInterface.isLoopback() && !netInterface.isVirtual() && netInterface.isUp()) {
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
ip = addresses.nextElement();
if (ip instanceof Inet4Address) {
return ip.getHostAddress();
}
}
}
}
} catch (Exception e) {
log.warn(e.getMessage());
}
return null;
}
/**
* check IP address type.
* @param ipDomain ip domain
* @return IP address type
*/
public static String checkIpAddressType(String ipDomain){
if (StringUtils.hasText(ipDomain) && InetAddressUtils.isIPv6Address(ipDomain)) {
return NetworkConstants.IPV6;
}
return NetworkConstants.IPV4;
}
/**
* get current local host name.
* @return hostname
*/
public static String getCurrentHostName() {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
return inetAddress.getHostName();
} catch (UnknownHostException e) {
return null;
}
}
/**
* check port is valid.
* @return true if valid
*/
public static boolean validPort(String portStr) {
if (portStr == null || portStr.trim().isEmpty()) {
return false;
}
try {
int port = Integer.parseInt(portStr);
return port >= 0 && port <= 65535;
} catch (NumberFormatException e) {
return false;
}
}
}
@@ -0,0 +1,50 @@
/*
* 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.common.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class SmsConfigBindingTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(BindingConfig.class);
@Test
void bindsRuntimeSmsConfigWithoutSpringAnnotationsOnModel() {
contextRunner.withPropertyValues(
"alerter.sms.enable=true",
"alerter.sms.type=smslocal",
"alerter.sms.smslocal.api-key=test-key")
.run(context -> {
SmsConfig smsConfig = context.getBean(SmsConfig.class);
assertTrue(smsConfig.isEnable());
assertEquals("smslocal", smsConfig.getType());
assertEquals("test-key", smsConfig.getSmslocal().getApiKey());
});
}
@EnableConfigurationProperties(SmsConfigBinding.class)
static class BindingConfig {
}
}
@@ -0,0 +1,114 @@
/*
* 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.common.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
class VirtualThreadPropertiesTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(BindingConfig.class);
@Test
void defaultsRemainSafeWithoutExternalConfiguration() {
VirtualThreadProperties properties = VirtualThreadProperties.defaults();
assertTrue(properties.enabled());
assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.collector().mode());
assertEquals(512, properties.collector().maxConcurrentJobs());
assertEquals(AdmissionMode.UNBOUNDED_VT, properties.common().mode());
assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.manager().mode());
assertEquals(10, properties.manager().maxConcurrentJobs());
assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.alerter().notifyPool().mode());
assertEquals(64, properties.alerter().notifyPool().maxConcurrentJobs());
assertEquals(10, properties.alerter().periodicMaxConcurrentJobs());
assertEquals(10, properties.alerter().logWorker().maxConcurrentJobs());
assertEquals(1000, properties.alerter().logWorker().queueCapacity());
assertEquals(2, properties.alerter().reduce().maxConcurrentJobs());
assertEquals(0, properties.alerter().reduce().queueCapacity());
assertEquals(2, properties.alerter().windowEvaluator().maxConcurrentJobs());
assertEquals(0, properties.alerter().windowEvaluator().queueCapacity());
assertEquals(4, properties.alerter().notifyMaxConcurrentPerChannel());
assertEquals(AdmissionMode.UNBOUNDED_VT, properties.warehouse().mode());
assertTrue(properties.async().enabled());
assertEquals(256, properties.async().concurrencyLimit());
assertTrue(properties.async().rejectWhenLimitReached());
assertEquals(5000L, properties.async().taskTerminationTimeout());
}
@Test
void collectorModeOnlyBindingRetainsDefaultConcurrency() {
contextRunner.withPropertyValues("hertzbeat.vthreads.collector.mode=LIMIT_AND_REJECT")
.run(context -> {
VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class);
assertEquals(AdmissionMode.LIMIT_AND_REJECT, properties.collector().mode());
assertEquals(512, properties.collector().maxConcurrentJobs());
});
}
@Test
void collectorModeOverrideUsesConfiguredMode() {
contextRunner.withPropertyValues("hertzbeat.vthreads.collector.mode=LIMIT_AND_BLOCK")
.run(context -> {
VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class);
assertEquals(AdmissionMode.LIMIT_AND_BLOCK, properties.collector().mode());
assertEquals(512, properties.collector().maxConcurrentJobs());
});
}
@Test
void notifyModeOnlyBindingRetainsDefaultConcurrency() {
contextRunner.withPropertyValues("hertzbeat.vthreads.alerter.notify.mode=LIMIT_AND_BLOCK")
.run(context -> {
VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class);
assertEquals(AdmissionMode.LIMIT_AND_BLOCK, properties.alerter().notifyPool().mode());
assertEquals(64, properties.alerter().notifyPool().maxConcurrentJobs());
});
}
@Test
void logWorkerQueueOnlyBindingRetainsDefaultConcurrency() {
contextRunner.withPropertyValues("hertzbeat.vthreads.alerter.log-worker.queue-capacity=32")
.run(context -> {
VirtualThreadProperties properties = context.getBean(VirtualThreadProperties.class);
assertEquals(10, properties.alerter().logWorker().maxConcurrentJobs());
assertEquals(32, properties.alerter().logWorker().queueCapacity());
});
}
@EnableConfigurationProperties(VirtualThreadPropertiesBinding.class)
static class BindingConfig {
@Bean
VirtualThreadProperties virtualThreadProperties(VirtualThreadPropertiesBinding binding) {
return binding.toRuntimeProperties();
}
}
}
@@ -17,92 +17,92 @@
package org.apache.hertzbeat.common.support;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hertzbeat.common.concurrent.AdmissionMode;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/**
* test for {@link CommonThreadPool}
* Test for {@link CommonThreadPool}.
*/
class CommonThreadPoolTest {
private CommonThreadPool commonThreadPool;
private ThreadPoolExecutor executorMock;
@BeforeEach
public void setUp() throws Exception {
@AfterEach
void tearDown() throws Exception {
if (commonThreadPool != null) {
commonThreadPool.destroy();
}
}
@Test
void testExecuteRunsOnVirtualThread() throws Exception {
commonThreadPool = new CommonThreadPool();
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(false);
Field workerExecutorField = CommonThreadPool.class.getDeclaredField("workerExecutor");
workerExecutorField.setAccessible(true);
executorMock = mock(ThreadPoolExecutor.class);
workerExecutorField.set(commonThreadPool, executorMock);
commonThreadPool.execute(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
public void testExecuteTask() {
void testExecuteLongRunningRunsOnPlatformThread() throws Exception {
commonThreadPool = new CommonThreadPool();
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean virtualThread = new AtomicBoolean(true);
Runnable task = mock(Runnable.class);
commonThreadPool.execute(task);
verify(executorMock).execute(task);
commonThreadPool.executeLongRunning(() -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertFalse(virtualThread.get());
}
@Test
public void testExecuteTaskThrowsEx() {
void testExecuteRejectsWhenConcurrencyLimitReached() throws Exception {
VirtualThreadProperties properties = new VirtualThreadProperties(
true,
VirtualThreadProperties.PoolProperties.collectorDefaults(),
new VirtualThreadProperties.PoolProperties(AdmissionMode.LIMIT_AND_REJECT, 1),
VirtualThreadProperties.PoolProperties.managerDefaults(),
VirtualThreadProperties.AlerterProperties.defaults(),
VirtualThreadProperties.PoolProperties.warehouseDefaults(),
VirtualThreadProperties.AsyncProperties.defaults());
commonThreadPool = new CommonThreadPool(properties);
Runnable task = mock(Runnable.class);
doThrow(RejectedExecutionException.class).when(executorMock).execute(task);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
commonThreadPool.execute(() -> {
started.countDown();
try {
release.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
assertTrue(started.await(5, TimeUnit.SECONDS));
assertThrows(
RejectedExecutionException.class,
() -> commonThreadPool.execute(task)
);
try {
assertThrows(RejectedExecutionException.class, () -> commonThreadPool.execute(() -> {
}));
} finally {
release.countDown();
}
}
@Test
public void testDestroy() throws Exception {
commonThreadPool.destroy();
verify(executorMock).shutdownNow();
}
@Test
public void testDestroyWithNull() throws Exception {
Field workerExecutorField = CommonThreadPool.class.getDeclaredField("workerExecutor");
workerExecutorField.setAccessible(true);
workerExecutorField.set(commonThreadPool, null);
commonThreadPool.destroy();
}
@Test
public void testInitialization() throws Exception {
CommonThreadPool pool = new CommonThreadPool();
Field workerExecutorField = CommonThreadPool.class.getDeclaredField("workerExecutor");
workerExecutorField.setAccessible(true);
ThreadPoolExecutor workerExecutor = (ThreadPoolExecutor) workerExecutorField.get(pool);
assertNotNull(workerExecutor);
assertEquals(1, workerExecutor.getCorePoolSize());
assertEquals(Integer.MAX_VALUE, workerExecutor.getMaximumPoolSize());
assertEquals(10, workerExecutor.getKeepAliveTime(TimeUnit.SECONDS));
assertTrue(workerExecutor.getQueue() instanceof SynchronousQueue);
}
}
@@ -61,11 +61,11 @@ public class LogSseManager {
t.setDaemon(true);
return t;
});
private final ExecutorService senderPool = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "sse-sender");
t.setDaemon(true);
return t;
});
private final ExecutorService senderPool = Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name("sse-sender-", 0)
.uncaughtExceptionHandler((thread, throwable) ->
log.error("SSE sender has uncaughtException.", throwable))
.factory());
private final AtomicLong queueSize = new AtomicLong(0);
public LogSseManager() {
@@ -26,7 +26,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -35,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -88,6 +91,24 @@ class LogSseManagerTest {
);
}
@Test
void shouldSendBatchOnVirtualThread() throws IOException, InterruptedException {
SseEmitter mockEmitter = mock(SseEmitter.class);
AtomicBoolean virtualThread = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
doAnswer(invocation -> {
virtualThread.set(Thread.currentThread().isVirtual());
latch.countDown();
return null;
}).when(mockEmitter).send(any(SseEmitter.SseEventBuilder.class));
subscribeClient(CLIENT_ID, null, mockEmitter);
logSseManager.broadcast(createLogEntry("INFO", "virtual-thread-send"));
assertTrue(latch.await(1, TimeUnit.SECONDS));
assertTrue(virtualThread.get());
}
@Test
void shouldNotBroadcastLogWhenFilterDoesNotMatch() throws IOException, InterruptedException {
// Given: A client with a filter for "ERROR" logs
@@ -172,6 +193,16 @@ class LogSseManagerTest {
});
}
@Test
void shouldDropLogsWhenQueueSizeLimitReached() {
for (int i = 0; i < 10_001; i++) {
logSseManager.broadcast(createLogEntry("INFO", "log-" + i));
}
assertEquals(10_000, logSseManager.getQueueSize());
assertEquals(10_000, logSseManager.getLogQueue().size());
}
/**
* Helper method to create a subscriber and inject a mock emitter for testing
*/
@@ -190,4 +221,4 @@ class LogSseManagerTest {
.body(body)
.build();
}
}
}
@@ -78,7 +78,7 @@ public class ServiceDiscoveryWorker implements InitializingBean {
@Override
public void afterPropertiesSet() {
workerPool.executeJob(new SdUpdateTask());
workerPool.executeLongRunning(new SdUpdateTask());
}
private class SdUpdateTask implements Runnable {
@@ -30,11 +30,13 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.StatusPageComponent;
@@ -45,6 +47,8 @@ import org.apache.hertzbeat.manager.dao.MonitorDao;
import org.apache.hertzbeat.manager.dao.StatusPageComponentDao;
import org.apache.hertzbeat.manager.dao.StatusPageHistoryDao;
import org.apache.hertzbeat.manager.dao.StatusPageOrgDao;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;
@@ -53,7 +57,7 @@ import org.springframework.stereotype.Component;
*/
@Component
@Slf4j
public class CalculateStatus {
public class CalculateStatus implements DisposableBean {
private static final int DEFAULT_CALCULATE_INTERVAL_TIME = 300;
@@ -67,108 +71,60 @@ public class CalculateStatus {
private final int intervals;
private final ScheduledExecutorService calculateScheduler;
private final ScheduledExecutorService combineHistoryScheduler;
private final ExecutorService calculateExecutor;
private final ExecutorService combineHistoryExecutor;
private final ScheduledDispatchTask calculateTask;
private final ScheduledDispatchTask combineHistoryTask;
public CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao,
StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao,
MonitorDao monitorDao) {
this(statusPageOrgDao, statusPageComponentDao, statusProperties, statusPageHistoryDao, monitorDao,
VirtualThreadProperties.defaults(), true);
}
@Autowired
public CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao,
StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao,
MonitorDao monitorDao, VirtualThreadProperties virtualThreadProperties) {
this(statusPageOrgDao, statusPageComponentDao, statusProperties, statusPageHistoryDao, monitorDao,
virtualThreadProperties, true);
}
CalculateStatus(StatusPageOrgDao statusPageOrgDao, StatusPageComponentDao statusPageComponentDao,
StatusProperties statusProperties, StatusPageHistoryDao statusPageHistoryDao,
MonitorDao monitorDao, VirtualThreadProperties virtualThreadProperties, boolean autoStart) {
this.statusPageOrgDao = statusPageOrgDao;
this.monitorDao = monitorDao;
this.statusPageComponentDao = statusPageComponentDao;
this.statusPageHistoryDao = statusPageHistoryDao;
intervals = statusProperties.getCalculate() == null ? DEFAULT_CALCULATE_INTERVAL_TIME : statusProperties.getCalculate().getInterval();
startCalculate();
startCombineHistory();
this.calculateScheduler = createScheduler("status-page-calculate-%d", "Status calculate has uncaughtException.");
this.combineHistoryScheduler = createScheduler("status-page-history-%d", "History combine has uncaughtException.");
this.calculateExecutor = createVirtualExecutor(virtualThreadProperties, "status-page-calculate-vt-",
"Status calculate worker has uncaughtException.");
this.combineHistoryExecutor = createVirtualExecutor(virtualThreadProperties, "status-page-history-vt-",
"History combine worker has uncaughtException.");
this.calculateTask = new ScheduledDispatchTask(calculateExecutor, this::runCalculate);
this.combineHistoryTask = new ScheduledDispatchTask(combineHistoryExecutor, this::runCombineHistory);
if (autoStart) {
startCalculate();
startCombineHistory();
}
}
private void startCalculate() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Status calculate has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("status-page-calculate-%d")
.build();
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
scheduledExecutor.scheduleAtFixedRate(() -> {
log.info("start to calculate status page state");
try {
// calculate component state from tag bind monitors status
List<StatusPageOrg> statusPageOrgList = statusPageOrgDao.findAll();
for (StatusPageOrg statusPageOrg : statusPageOrgList) {
long orgId = statusPageOrg.getId();
List<StatusPageComponent> pageComponentList = statusPageComponentDao.findByOrgId(orgId);
Set<Byte> stateSet = new HashSet<>(8);
for (StatusPageComponent component : pageComponentList) {
byte state;
if (component.getMethod() == CommonConstants.STATUS_PAGE_CALCULATE_METHOD_MANUAL) {
state = component.getConfigState();
} else {
Map<String, String> labels = component.getLabels();
if (labels == null || labels.isEmpty()) {
continue;
}
Specification<Monitor> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// create every label condition
labels.forEach((key, value) -> {
String pattern = String.format("%%\"%s\":\"%s\"%%", key, value);
predicates.add(criteriaBuilder.like(root.get("labels"), pattern));
});
// use or connect them
return criteriaBuilder.or(predicates.toArray(new Predicate[0]));
};
List<Monitor> monitorList = monitorDao.findAll(specification);
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN;
for (Monitor monitor : monitorList) {
if (monitor.getStatus() == CommonConstants.MONITOR_DOWN_CODE) {
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL;
break;
} else if (monitor.getStatus() == CommonConstants.MONITOR_UP_CODE) {
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL;
}
}
}
stateSet.add(state);
component.setState(state);
statusPageComponentDao.save(component);
// insert component state history
StatusPageHistory statusPageHistory = StatusPageHistory.builder()
.componentId(component.getId())
.state(state)
.timestamp(System.currentTimeMillis())
.build();
statusPageHistoryDao.save(statusPageHistory);
}
stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN);
if (stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL)) {
if (stateSet.contains(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL)) {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_SOME_ABNORMAL);
} else {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_ABNORMAL);
}
} else {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_NORMAL);
}
statusPageOrg.setGmtUpdate(LocalDateTime.now());
statusPageOrgDao.save(statusPageOrg);
}
} catch (Exception e) {
log.error("status page calculate component state error: {}", e.getMessage(), e);
}
}, 5, intervals, TimeUnit.SECONDS);
calculateScheduler.scheduleAtFixedRate(this::dispatchCalculate, 5, intervals, TimeUnit.SECONDS);
}
private void startCombineHistory() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("History combine has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("status-page-calculate-%d")
.build();
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
// combine history every day at 1:00 AM
LocalDateTime now = LocalDateTime.now();
LocalDateTime nextRun = now.withHour(1).withMinute(0).withSecond(0);
@@ -176,64 +132,8 @@ public class CalculateStatus {
nextRun = nextRun.plusDays(1);
}
long delay = Duration.between(now, nextRun).toMillis();
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
// combine pre day status history to one record
LocalDateTime nowTime = LocalDateTime.now();
ZoneOffset zoneOffset = ZoneId.systemDefault().getRules().getOffset(Instant.now());
LocalDateTime midnight = nowTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime preNight = midnight.minusDays(1);
long midnightTimestamp = midnight.toInstant(zoneOffset).toEpochMilli();
long preNightTimestamp = preNight.toInstant(zoneOffset).toEpochMilli();
List<StatusPageHistory> statusPageHistoryList = statusPageHistoryDao
.findStatusPageHistoriesByTimestampBetween(preNightTimestamp, midnightTimestamp);
Map<Long, StatusPageHistory> statusPageHistoryMap = new HashMap<>(8);
for (StatusPageHistory statusPageHistory : statusPageHistoryList) {
statusPageHistory.setNormal(0);
statusPageHistory.setAbnormal(0);
statusPageHistory.setUnknowing(0);
if (statusPageHistoryMap.containsKey(statusPageHistory.getComponentId())) {
StatusPageHistory history = statusPageHistoryMap.get(statusPageHistory.getComponentId());
if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) {
history.setAbnormal(history.getAbnormal() + intervals);
} else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) {
history.setUnknowing(history.getUnknowing() + intervals);
} else {
history.setNormal(history.getNormal() + intervals);
}
statusPageHistoryMap.put(statusPageHistory.getComponentId(), history);
} else {
if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) {
statusPageHistory.setAbnormal(intervals);
} else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) {
statusPageHistory.setUnknowing(intervals);
} else {
statusPageHistory.setNormal(intervals);
}
statusPageHistoryMap.put(statusPageHistory.getComponentId(), statusPageHistory);
}
}
statusPageHistoryDao.deleteAll(statusPageHistoryList);
for (StatusPageHistory history : statusPageHistoryMap.values()) {
double total = history.getNormal() + history.getAbnormal() + history.getUnknowing();
double uptime = 0;
if (total > 0) {
uptime = (double) history.getNormal() / total;
}
history.setUptime(uptime);
if (history.getAbnormal() > 0) {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL);
} else if (history.getNormal() > 0) {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL);
} else {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN);
}
statusPageHistoryDao.save(history);
}
} catch (Exception e) {
log.error("status page combine history error: {}", e.getMessage(), e);
}
}, delay, TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS);
combineHistoryScheduler.scheduleAtFixedRate(this::dispatchCombineHistory, delay,
TimeUnit.DAYS.toMillis(1), TimeUnit.MILLISECONDS);
}
/**
@@ -243,4 +143,246 @@ public class CalculateStatus {
public int getCalculateStatusIntervals() {
return intervals;
}
void dispatchCalculate() {
calculateTask.dispatch();
}
void dispatchCombineHistory() {
combineHistoryTask.dispatch();
}
@Override
public void destroy() {
calculateScheduler.shutdownNow();
combineHistoryScheduler.shutdownNow();
if (calculateExecutor != null) {
calculateExecutor.shutdownNow();
}
if (combineHistoryExecutor != null) {
combineHistoryExecutor.shutdownNow();
}
}
private void runCalculate() {
log.info("start to calculate status page state");
try {
// calculate component state from tag bind monitors status
List<StatusPageOrg> statusPageOrgList = statusPageOrgDao.findAll();
for (StatusPageOrg statusPageOrg : statusPageOrgList) {
long orgId = statusPageOrg.getId();
List<StatusPageComponent> pageComponentList = statusPageComponentDao.findByOrgId(orgId);
Set<Byte> stateSet = new HashSet<>(8);
for (StatusPageComponent component : pageComponentList) {
byte state;
if (component.getMethod() == CommonConstants.STATUS_PAGE_CALCULATE_METHOD_MANUAL) {
state = component.getConfigState();
} else {
Map<String, String> labels = component.getLabels();
if (labels == null || labels.isEmpty()) {
continue;
}
Specification<Monitor> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// create every label condition
labels.forEach((key, value) -> {
String pattern = String.format("%%\"%s\":\"%s\"%%", key, value);
predicates.add(criteriaBuilder.like(root.get("labels"), pattern));
});
// use or connect them
return criteriaBuilder.or(predicates.toArray(new Predicate[0]));
};
List<Monitor> monitorList = monitorDao.findAll(specification);
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN;
for (Monitor monitor : monitorList) {
if (monitor.getStatus() == CommonConstants.MONITOR_DOWN_CODE) {
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL;
break;
} else if (monitor.getStatus() == CommonConstants.MONITOR_UP_CODE) {
state = CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL;
}
}
}
stateSet.add(state);
component.setState(state);
statusPageComponentDao.save(component);
// insert component state history
StatusPageHistory statusPageHistory = StatusPageHistory.builder()
.componentId(component.getId())
.state(state)
.timestamp(System.currentTimeMillis())
.build();
statusPageHistoryDao.save(statusPageHistory);
}
stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN);
if (stateSet.remove(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL)) {
if (stateSet.contains(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL)) {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_SOME_ABNORMAL);
} else {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_ABNORMAL);
}
} else {
statusPageOrg.setState(CommonConstants.STATUS_PAGE_ORG_STATE_ALL_NORMAL);
}
statusPageOrg.setGmtUpdate(LocalDateTime.now());
statusPageOrgDao.save(statusPageOrg);
}
} catch (Exception e) {
log.error("status page calculate component state error: {}", e.getMessage(), e);
}
}
private void runCombineHistory() {
try {
// combine pre day status history to one record
LocalDateTime nowTime = LocalDateTime.now();
ZoneOffset zoneOffset = ZoneId.systemDefault().getRules().getOffset(Instant.now());
LocalDateTime midnight = nowTime.withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDateTime preNight = midnight.minusDays(1);
long midnightTimestamp = midnight.toInstant(zoneOffset).toEpochMilli();
long preNightTimestamp = preNight.toInstant(zoneOffset).toEpochMilli();
List<StatusPageHistory> statusPageHistoryList = statusPageHistoryDao
.findStatusPageHistoriesByTimestampBetween(preNightTimestamp, midnightTimestamp);
Map<Long, StatusPageHistory> statusPageHistoryMap = new HashMap<>(8);
for (StatusPageHistory statusPageHistory : statusPageHistoryList) {
statusPageHistory.setNormal(0);
statusPageHistory.setAbnormal(0);
statusPageHistory.setUnknowing(0);
if (statusPageHistoryMap.containsKey(statusPageHistory.getComponentId())) {
StatusPageHistory history = statusPageHistoryMap.get(statusPageHistory.getComponentId());
if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) {
history.setAbnormal(history.getAbnormal() + intervals);
} else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) {
history.setUnknowing(history.getUnknowing() + intervals);
} else {
history.setNormal(history.getNormal() + intervals);
}
statusPageHistoryMap.put(statusPageHistory.getComponentId(), history);
} else {
if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL) {
statusPageHistory.setAbnormal(intervals);
} else if (statusPageHistory.getState() == CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN) {
statusPageHistory.setUnknowing(intervals);
} else {
statusPageHistory.setNormal(intervals);
}
statusPageHistoryMap.put(statusPageHistory.getComponentId(), statusPageHistory);
}
}
statusPageHistoryDao.deleteAll(statusPageHistoryList);
for (StatusPageHistory history : statusPageHistoryMap.values()) {
double total = history.getNormal() + history.getAbnormal() + history.getUnknowing();
double uptime = 0;
if (total > 0) {
uptime = (double) history.getNormal() / total;
}
history.setUptime(uptime);
if (history.getAbnormal() > 0) {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_ABNORMAL);
} else if (history.getNormal() > 0) {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_NORMAL);
} else {
history.setState(CommonConstants.STATUS_PAGE_COMPONENT_STATE_UNKNOWN);
}
statusPageHistoryDao.save(history);
}
} catch (Exception e) {
log.error("status page combine history error: {}", e.getMessage(), e);
}
}
private ScheduledExecutorService createScheduler(String threadNameFormat, String errorMessage) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error(errorMessage);
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat(threadNameFormat)
.build();
return Executors.newSingleThreadScheduledExecutor(threadFactory);
}
private ExecutorService createVirtualExecutor(VirtualThreadProperties virtualThreadProperties, String threadPrefix,
String errorMessage) {
VirtualThreadProperties properties =
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
if (!properties.enabled()) {
return null;
}
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
.name(threadPrefix, 0)
.uncaughtExceptionHandler((thread, throwable) -> {
log.error(errorMessage);
log.error(throwable.getMessage(), throwable);
})
.factory());
}
private static final class ScheduledDispatchTask {
private final ExecutorService executorService;
private final Runnable task;
private final Object lock = new Object();
private boolean running;
private int pendingRuns;
private ScheduledDispatchTask(ExecutorService executorService, Runnable task) {
this.executorService = executorService;
this.task = task;
}
private void dispatch() {
if (executorService == null) {
task.run();
return;
}
synchronized (lock) {
if (running) {
pendingRuns++;
return;
}
running = true;
}
submit();
}
private void submit() {
boolean submitted = false;
try {
executorService.execute(() -> {
try {
task.run();
} finally {
onComplete();
}
});
submitted = true;
} finally {
if (!submitted) {
synchronized (lock) {
running = false;
pendingRuns = 0;
}
}
}
}
private void onComplete() {
boolean shouldRunAgain;
synchronized (lock) {
if (pendingRuns > 0) {
pendingRuns--;
shouldRunAgain = true;
} else {
running = false;
shouldRunAgain = false;
}
}
if (shouldRunAgain) {
submit();
}
}
}
}
@@ -17,8 +17,8 @@
package org.apache.hertzbeat.manager.component.validator;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
/**
* Parameter validator interface
@@ -40,5 +40,5 @@ public interface ParamValidator {
* @param param parameter actual value
* @throws IllegalArgumentException if validation fails
*/
void validate(ParamDefine paramDefine, Param param) throws IllegalArgumentException;
void validate(ParamDefineInfo paramDefine, MonitorParam param) throws IllegalArgumentException;
}
@@ -17,8 +17,8 @@
package org.apache.hertzbeat.manager.component.validator;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -35,7 +35,7 @@ public class ParamValidatorManager {
this.validators = validators;
}
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
for (ParamValidator validator : validators) {
if (validator.support(paramDefine.getType())) {
validator.validate(paramDefine, param);
@@ -17,9 +17,9 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -33,7 +33,7 @@ public class ArrayParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
String[] arrays = param.getParamValue().split(",");
if (arrays.length == 0) {
throw new IllegalArgumentException("Param field " + paramDefine.getField() + " value "
@@ -17,9 +17,9 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -33,7 +33,7 @@ public class BooleanParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
String booleanValue = param.getParamValue();
if (!"true".equalsIgnoreCase(booleanValue) && !"false".equalsIgnoreCase(booleanValue)) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
@@ -17,9 +17,9 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -40,7 +40,7 @@ public class HostParamValidatorAdapter implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
if (!hostValidator.isValid(param.getParamValue(), null)) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
+ param.getParamValue() + " is invalid host value.");
@@ -18,10 +18,10 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import tools.jackson.core.type.TypeReference;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -35,7 +35,7 @@ public class JsonParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
if (JsonUtil.fromJson(param.getParamValue(), new TypeReference<>() {
}) == null) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
@@ -18,10 +18,10 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.IntervalExpressionUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -35,7 +35,7 @@ public class NumberParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
Double doubleValue = org.apache.hertzbeat.common.util.CommonUtil.parseStrDouble(param.getParamValue());
if (doubleValue == null) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type "
@@ -17,9 +17,9 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -35,11 +35,11 @@ public class OptionParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
List<ParamDefine.Option> options = paramDefine.getOptions();
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
List<ParamDefineInfo.OptionInfo> options = paramDefine.getOptions();
boolean invalid = true;
if (options != null) {
for (ParamDefine.Option option : options) {
for (ParamDefineInfo.OptionInfo option : options) {
if (param.getParamValue().equalsIgnoreCase(option.getValue())) {
invalid = false;
break;
@@ -18,10 +18,10 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -35,7 +35,7 @@ public class PasswordParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
String passwordValue = param.getParamValue();
if (!AesUtil.isCiphertext(passwordValue)) {
passwordValue = AesUtil.aesEncode(passwordValue);
@@ -17,9 +17,9 @@
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.apache.hertzbeat.manager.pojo.dto.MonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.springframework.stereotype.Component;
/**
@@ -33,7 +33,7 @@ public class TextParamValidator implements ParamValidator {
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
public void validate(ParamDefineInfo paramDefine, MonitorParam param) {
Short limit = paramDefine.getLimit();
if (limit != null && param.getParamValue().length() > limit) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type "
@@ -27,10 +27,10 @@ import java.util.Locale;
import java.util.Map;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.ResponseUtil;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDefineDto;
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
import org.apache.hertzbeat.manager.service.AppService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -64,7 +64,7 @@ public class AppController {
@GetMapping(path = "/{app}/params")
@Operation(summary = "The structure of the input parameters required to specify the monitoring type according to the app query",
description = "The structure of the input parameters required to specify the monitoring type according to the app query")
public ResponseEntity<Message<List<ParamDefine>>> queryAppParamDefines(
public ResponseEntity<Message<List<ParamDefineInfo>>> queryAppParamDefines(
@Parameter(description = "en: Monitoring type name", example = "api") @PathVariable("app") final String app) {
return ResponseUtil.handle(() -> appService.getAppParamDefines(app.toLowerCase()));
}
@@ -23,9 +23,9 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.common.entity.dto.CollectorSummary;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.util.ResponseUtil;
import org.apache.hertzbeat.manager.pojo.dto.CollectorSummary;
import org.apache.hertzbeat.manager.service.CollectorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -22,10 +22,12 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.manager.pojo.dto.MonitorInfo;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -53,7 +55,7 @@ public class MonitorsController {
@GetMapping
@Operation(summary = "Obtain a list of monitoring information based on query filter items",
description = "Obtain a list of monitoring information based on query filter items")
public ResponseEntity<Message<Page<Monitor>>> getMonitors(
public ResponseEntity<Message<Page<MonitorInfo>>> getMonitors(
@Parameter(description = "Monitor ID", example = "6565463543") @RequestParam(required = false) final List<Long> ids,
@Parameter(description = "Monitor Type", example = "linux") @RequestParam(required = false) final String app,
@Parameter(description = "Monitor Status 0:no monitor,1:usable,2:disabled,9:all status", example = "1") @RequestParam(required = false) final Byte status,
@@ -64,15 +66,19 @@ public class MonitorsController {
@Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
@Parameter(description = "Number of list pagination ", example = "8") @RequestParam(defaultValue = "8") int pageSize) {
Page<Monitor> monitorPage = monitorService.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
return ResponseEntity.ok(Message.success(monitorPage));
Page<MonitorInfo> responsePage = monitorPage == null ? Page.empty() : monitorPage.map(MonitorInfo::fromEntity);
return ResponseEntity.ok(Message.success(responsePage));
}
@GetMapping(path = "/{app}")
@Operation(summary = "Filter all acquired monitoring information lists of the specified monitoring type according to the query",
description = "Filter all acquired monitoring information lists of the specified monitoring type according to the query")
public ResponseEntity<Message<List<Monitor>>> getAppMonitors(
public ResponseEntity<Message<List<MonitorInfo>>> getAppMonitors(
@Parameter(description = "en: Monitoring type", example = "linux") @PathVariable(required = false) final String app) {
return ResponseEntity.ok(Message.success(monitorService.getAppMonitors(app)));
List<Monitor> monitors = monitorService.getAppMonitors(app);
List<MonitorInfo> response = monitors == null ? Collections.emptyList()
: monitors.stream().map(MonitorInfo::fromEntity).toList();
return ResponseEntity.ok(Message.success(response));
}
@@ -25,8 +25,8 @@ import java.util.HashSet;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.PluginUpload;
import org.apache.hertzbeat.common.entity.manager.PluginMetadata;
import org.apache.hertzbeat.manager.pojo.dto.PluginUpload;
import org.apache.hertzbeat.manager.pojo.dto.PluginParam;
import org.apache.hertzbeat.manager.pojo.dto.PluginParametersVO;
import org.apache.hertzbeat.manager.service.PluginService;
@@ -28,9 +28,9 @@ import java.util.List;
import jakarta.validation.Valid;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.manager.StatusPageComponent;
import org.apache.hertzbeat.common.entity.manager.StatusPageIncident;
import org.apache.hertzbeat.common.entity.manager.StatusPageOrg;
import org.apache.hertzbeat.manager.pojo.dto.StatusPageComponentInfo;
import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo;
import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo;
import org.apache.hertzbeat.manager.service.StatusPageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -57,8 +57,8 @@ public class StatusPageController {
@GetMapping("/org")
@Operation(summary = "Query Status Page Organization")
public ResponseEntity<Message<StatusPageOrg>> queryStatusPageOrg() {
StatusPageOrg statusPageOrg = statusPageService.queryStatusPageOrg();
public ResponseEntity<Message<StatusPageOrgInfo>> queryStatusPageOrg() {
StatusPageOrgInfo statusPageOrg = statusPageService.queryStatusPageOrg();
if (statusPageOrg == null) {
return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE, "Status Page Organization Not Found"));
}
@@ -67,28 +67,27 @@ public class StatusPageController {
@PostMapping("/org")
@Operation(summary = "Save and Update Query Status Page Organization")
public ResponseEntity<Message<StatusPageOrg>> saveStatusPageOrg(@Valid @RequestBody StatusPageOrg statusPageOrg) {
StatusPageOrg org = statusPageService.saveStatusPageOrg(statusPageOrg);
public ResponseEntity<Message<StatusPageOrgInfo>> saveStatusPageOrg(@Valid @RequestBody StatusPageOrgInfo statusPageOrg) {
StatusPageOrgInfo org = statusPageService.saveStatusPageOrg(statusPageOrg);
return ResponseEntity.ok(Message.success(org));
}
@GetMapping("/component")
@Operation(summary = "Query Status Page Components")
public ResponseEntity<Message<List<StatusPageComponent>>> queryStatusPageComponent() {
List<StatusPageComponent> statusPageComponents = statusPageService.queryStatusPageComponents();
return ResponseEntity.ok(Message.success(statusPageComponents));
public ResponseEntity<Message<List<StatusPageComponentInfo>>> queryStatusPageComponent() {
return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageComponents()));
}
@PostMapping("/component")
@Operation(summary = "Save Status Page Component")
public ResponseEntity<Message<Void>> newStatusPageComponent(@Valid @RequestBody StatusPageComponent statusPageComponent) {
public ResponseEntity<Message<Void>> newStatusPageComponent(@Valid @RequestBody StatusPageComponentInfo statusPageComponent) {
statusPageService.newStatusPageComponent(statusPageComponent);
return ResponseEntity.ok(Message.success("Add success"));
}
@PutMapping("/component")
@Operation(summary = "Update Status Page Component")
public ResponseEntity<Message<Void>> updateStatusPageComponent(@Valid @RequestBody StatusPageComponent statusPageComponent) {
public ResponseEntity<Message<Void>> updateStatusPageComponent(@Valid @RequestBody StatusPageComponentInfo statusPageComponent) {
statusPageService.updateStatusPageComponent(statusPageComponent);
return ResponseEntity.ok(Message.success("Update success"));
}
@@ -102,21 +101,20 @@ public class StatusPageController {
@GetMapping("/component/{id}")
@Operation(summary = "Query Status Page Component")
public ResponseEntity<Message<StatusPageComponent>> queryStatusPageComponent(@PathVariable("id") final long id) {
StatusPageComponent statusPageComponent = statusPageService.queryStatusPageComponent(id);
return ResponseEntity.ok(Message.success(statusPageComponent));
public ResponseEntity<Message<StatusPageComponentInfo>> queryStatusPageComponent(@PathVariable("id") final long id) {
return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageComponent(id)));
}
@PostMapping("/incident")
@Operation(summary = "Save Status Page Incident")
public ResponseEntity<Message<Void>> newStatusPageIncident(@Valid @RequestBody StatusPageIncident incident) {
public ResponseEntity<Message<Void>> newStatusPageIncident(@Valid @RequestBody StatusPageIncidentInfo incident) {
statusPageService.newStatusPageIncident(incident);
return ResponseEntity.ok(Message.success("Add success"));
}
@PutMapping("/incident")
@Operation(summary = "Update Status Page Incident")
public ResponseEntity<Message<Void>> updateStatusPageIncident(@Valid @RequestBody StatusPageIncident incident) {
public ResponseEntity<Message<Void>> updateStatusPageIncident(@Valid @RequestBody StatusPageIncidentInfo incident) {
statusPageService.updateStatusPageIncident(incident);
return ResponseEntity.ok(Message.success("Update success"));
}
@@ -130,20 +128,19 @@ public class StatusPageController {
@GetMapping("/incident/{id}")
@Operation(summary = "Get Status Page Incident")
public ResponseEntity<Message<StatusPageIncident>> queryStatusPageIncident(@PathVariable("id") final long id) {
StatusPageIncident incident = statusPageService.queryStatusPageIncident(id);
return ResponseEntity.ok(Message.success(incident));
public ResponseEntity<Message<StatusPageIncidentInfo>> queryStatusPageIncident(@PathVariable("id") final long id) {
return ResponseEntity.ok(Message.success(statusPageService.queryStatusPageIncident(id)));
}
@GetMapping("/incident")
@Operation(summary = "Query Status Page Incidents")
public ResponseEntity<Message<Page<StatusPageIncident>>> queryStatusPageIncident(
public ResponseEntity<Message<Page<StatusPageIncidentInfo>>> queryStatusPageIncident(
@Parameter(description = "Search-Target", example = "x") @RequestParam(required = false) String search,
@Parameter(description = "Start Time", example = "1756384301907") @RequestParam(required = false) Long startTime,
@Parameter(description = "End Time", example = "1756384301907") @RequestParam(required = false) Long endTime,
@Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
@Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) {
Page<StatusPageIncident> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize);
Page<StatusPageIncidentInfo> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize);
return ResponseEntity.ok(Message.success(incidents));
}
}
@@ -27,9 +27,9 @@ import java.util.List;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.manager.StatusPageIncident;
import org.apache.hertzbeat.common.entity.manager.StatusPageOrg;
import org.apache.hertzbeat.manager.pojo.dto.ComponentStatus;
import org.apache.hertzbeat.manager.pojo.dto.StatusPageIncidentInfo;
import org.apache.hertzbeat.manager.pojo.dto.StatusPageOrgInfo;
import org.apache.hertzbeat.manager.service.StatusPageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -52,8 +52,8 @@ public class StatusPagePublicController {
@GetMapping("/org")
@Operation(summary = "Query Status Page Organization")
public ResponseEntity<Message<StatusPageOrg>> queryStatusPageOrg() {
StatusPageOrg statusPageOrg = statusPageService.queryStatusPageOrg();
public ResponseEntity<Message<StatusPageOrgInfo>> queryStatusPageOrg() {
StatusPageOrgInfo statusPageOrg = statusPageService.queryStatusPageOrg();
if (statusPageOrg == null) {
return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE, "Status Page Organization Not Found"));
}
@@ -76,13 +76,13 @@ public class StatusPagePublicController {
@GetMapping("/incident")
@Operation(summary = "Query Status Page Incidents")
public ResponseEntity<Message<Page<StatusPageIncident>>> queryStatusPageIncident(
public ResponseEntity<Message<Page<StatusPageIncidentInfo>>> queryStatusPageIncident(
@Parameter(description = "Search-Target", example = "x") @RequestParam(required = false) String search,
@Parameter(description = "Start Time", example = "1756384301907") @RequestParam(required = false) Long startTime,
@Parameter(description = "End Time", example = "1756384301907") @RequestParam(required = false) Long endTime,
@Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex,
@Parameter(description = "Number of list pages", example = "10") @RequestParam(defaultValue = "10") int pageSize) {
Page<StatusPageIncident> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize);
Page<StatusPageIncidentInfo> incidents = statusPageService.queryStatusPageIncidents(search, startTime, endTime, pageIndex, pageSize);
return ResponseEntity.ok(Message.success(incidents));
}
}
@@ -0,0 +1,82 @@
/*
* 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.manager.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.Collector;
/**
* Collector info view.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "collector info")
public class CollectorInfo {
@Schema(title = "primary id", example = "2")
private Long id;
@Schema(title = "collector identity name", description = "collector identity name")
private String name;
@Schema(title = "collector ip", description = "collector remote ip")
private String ip;
@Schema(title = "collector version", description = "collector version")
private String version;
@Schema(title = "collector status: 0-online 1-offline")
private byte status;
@Schema(title = "collector mode: public or private")
private String mode;
@Schema(title = "The creator of this record", example = "tom")
private String creator;
@Schema(title = "This record was last modified by")
private String modifier;
@Schema(title = "This record creation time (millisecond timestamp)")
private LocalDateTime gmtCreate;
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
private LocalDateTime gmtUpdate;
public static CollectorInfo fromEntity(Collector collector) {
return CollectorInfo.builder()
.id(collector.getId())
.name(collector.getName())
.ip(collector.getIp())
.version(collector.getVersion())
.status(collector.getStatus())
.mode(collector.getMode())
.creator(collector.getCreator())
.modifier(collector.getModifier())
.gmtCreate(collector.getGmtCreate())
.gmtUpdate(collector.getGmtUpdate())
.build();
}
}
@@ -15,17 +15,16 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto;
package org.apache.hertzbeat.manager.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.Collector;
/**
* collector summary
* Collector summary view.
*/
@Data
@Builder
@@ -33,13 +32,13 @@ import org.apache.hertzbeat.common.entity.manager.Collector;
@NoArgsConstructor
@Schema(description = "collector summary")
public class CollectorSummary {
@Schema(description = "the collector info")
private Collector collector;
private CollectorInfo collector;
@Schema(description = "the number of monitors pinned in this collector")
private int pinMonitorNum;
@Schema(description = "the number of monitors dispatched in this collector")
private int dispatchMonitorNum;
}
@@ -17,26 +17,67 @@
package org.apache.hertzbeat.manager.pojo.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.StatusPageComponent;
import org.apache.hertzbeat.common.entity.manager.StatusPageHistory;
import java.util.Objects;
/**
* status page's component status dto
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Status Page's Component Status")
public class ComponentStatus {
public class ComponentStatus {
private StatusPageComponentInfo componentInfo;
private List<StatusPageHistoryInfo> historyItems;
@Schema(description = "Component Info")
private StatusPageComponent info;
@JsonProperty("info")
public StatusPageComponentInfo getComponentInfo() {
return componentInfo;
}
@JsonProperty("info")
public void setComponentInfo(StatusPageComponentInfo componentInfo) {
this.componentInfo = componentInfo;
}
@Schema(description = "Component History")
private List<StatusPageHistory> history;
@JsonProperty("history")
public List<StatusPageHistoryInfo> getHistoryItems() {
return historyItems;
}
@JsonProperty("history")
public void setHistoryItems(List<StatusPageHistoryInfo> historyItems) {
this.historyItems = historyItems;
}
@JsonIgnore
public StatusPageComponent getInfo() {
return componentInfo == null ? null : componentInfo.toEntity();
}
public void setInfo(StatusPageComponent info) {
this.componentInfo = StatusPageComponentInfo.fromEntity(info);
}
@JsonIgnore
public List<StatusPageHistory> getHistory() {
return historyItems == null ? null : historyItems.stream()
.filter(Objects::nonNull)
.map(StatusPageHistoryInfo::toEntity)
.toList();
}
public void setHistory(List<StatusPageHistory> history) {
this.historyItems = history == null ? null : history.stream()
.filter(Objects::nonNull)
.map(StatusPageHistoryInfo::fromEntity)
.toList();
}
}
@@ -19,45 +19,108 @@ package org.apache.hertzbeat.manager.pojo.dto;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import java.util.Objects;
/**
* Monitoring Information External Interaction Entities
*/
@Data
@Schema(description = "Monitoring information entities")
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorDto {
@Schema(description = "monitor content", accessMode = READ_WRITE)
@NotNull
@Valid
private Monitor monitor;
private MonitorInfo monitorInfo;
@Schema(description = "monitor params", accessMode = READ_WRITE)
@NotEmpty
@Valid
private List<Param> params;
private List<MonitorParam> paramInfos;
@Schema(description = "Monitor Metrics", accessMode = READ_ONLY)
private List<MetricsInfo> metrics;
@Schema(description = "pinned collector, default null if system dispatch", accessMode = READ_WRITE)
private String collector;
@Schema(description = "grafana dashboard")
private GrafanaDashboard grafanaDashboard;
@Schema(description = "monitor content", accessMode = READ_WRITE)
@JsonProperty("monitor")
public MonitorInfo getMonitorInfo() {
return monitorInfo;
}
@JsonProperty("monitor")
public void setMonitorInfo(MonitorInfo monitorInfo) {
this.monitorInfo = monitorInfo;
}
@Schema(description = "monitor params", accessMode = READ_WRITE)
@JsonProperty("params")
public List<MonitorParam> getParamInfos() {
return paramInfos;
}
@JsonProperty("params")
public void setParamInfos(List<MonitorParam> paramInfos) {
this.paramInfos = paramInfos;
}
@JsonIgnore
public Monitor getMonitor() {
return monitorInfo == null ? null : monitorInfo.toEntity();
}
public void setMonitor(Monitor monitor) {
this.monitorInfo = MonitorInfo.fromEntity(monitor);
}
@JsonIgnore
public List<Param> getParams() {
return paramInfos == null ? null : paramInfos.stream()
.filter(Objects::nonNull)
.map(MonitorParam::toEntity)
.toList();
}
public void setParams(List<Param> params) {
this.paramInfos = params == null ? null : params.stream()
.filter(Objects::nonNull)
.map(MonitorParam::fromEntity)
.toList();
}
@Schema(description = "Monitor Metrics", accessMode = READ_ONLY)
public List<MetricsInfo> getMetrics() {
return metrics;
}
public void setMetrics(List<MetricsInfo> metrics) {
this.metrics = metrics;
}
@Schema(description = "pinned collector, default null if system dispatch", accessMode = READ_WRITE)
public String getCollector() {
return collector;
}
public void setCollector(String collector) {
this.collector = collector;
}
@Schema(description = "grafana dashboard")
public GrafanaDashboard getGrafanaDashboard() {
return grafanaDashboard;
}
public void setGrafanaDashboard(GrafanaDashboard grafanaDashboard) {
this.grafanaDashboard = grafanaDashboard;
}
}
@@ -0,0 +1,134 @@
/*
* 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.manager.pojo.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.support.valid.HostValid;
/**
* Manager-side monitor DTO detached from JPA annotations.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MonitorInfo {
private Long id;
private Long jobId;
@Size(max = 100)
private String name;
@Size(max = 100)
private String app;
@Size(max = 100)
private String scrape;
@Size(max = 100)
@HostValid
private String instance;
@Min(10)
private Integer intervals;
@Size(max = 20)
private String scheduleType;
@Size(max = 100)
private String cronExpression;
@Min(0)
@Max(4)
private byte status;
private byte type;
private Map<String, String> labels;
private Map<String, String> annotations;
@Size(max = 255)
private String description;
private String creator;
private String modifier;
private LocalDateTime gmtCreate;
private LocalDateTime gmtUpdate;
public static MonitorInfo fromEntity(Monitor monitor) {
if (monitor == null) {
return null;
}
MonitorInfo info = new MonitorInfo();
info.setId(monitor.getId());
info.setJobId(monitor.getJobId());
info.setName(monitor.getName());
info.setApp(monitor.getApp());
info.setScrape(monitor.getScrape());
info.setInstance(monitor.getInstance());
info.setIntervals(monitor.getIntervals());
info.setScheduleType(monitor.getScheduleType());
info.setCronExpression(monitor.getCronExpression());
info.setStatus(monitor.getStatus());
info.setType(monitor.getType());
info.setLabels(monitor.getLabels());
info.setAnnotations(monitor.getAnnotations());
info.setDescription(monitor.getDescription());
info.setCreator(monitor.getCreator());
info.setModifier(monitor.getModifier());
info.setGmtCreate(monitor.getGmtCreate());
info.setGmtUpdate(monitor.getGmtUpdate());
return info;
}
public Monitor toEntity() {
Monitor monitor = new Monitor();
monitor.setId(id);
monitor.setJobId(jobId);
monitor.setName(name);
monitor.setApp(app);
monitor.setScrape(scrape);
monitor.setInstance(instance);
monitor.setIntervals(intervals);
monitor.setScheduleType(scheduleType);
monitor.setCronExpression(cronExpression);
monitor.setStatus(status);
monitor.setType(type);
monitor.setLabels(labels);
monitor.setAnnotations(annotations);
monitor.setDescription(description);
monitor.setCreator(creator);
monitor.setModifier(modifier);
monitor.setGmtCreate(gmtCreate);
monitor.setGmtUpdate(gmtUpdate);
return monitor;
}
}
@@ -0,0 +1,81 @@
/*
* 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.manager.pojo.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.Param;
/**
* Manager-side monitor param DTO detached from JPA annotations.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MonitorParam {
private Long id;
private Long monitorId;
@Size(max = 100)
@NotBlank(message = "field can not null")
private String field;
@Size(max = 8126)
private String paramValue;
@Min(0)
private byte type;
private LocalDateTime gmtCreate;
private LocalDateTime gmtUpdate;
public static MonitorParam fromEntity(Param param) {
if (param == null) {
return null;
}
MonitorParam monitorParam = new MonitorParam();
monitorParam.setId(param.getId());
monitorParam.setMonitorId(param.getMonitorId());
monitorParam.setField(param.getField());
monitorParam.setParamValue(param.getParamValue());
monitorParam.setType(param.getType());
monitorParam.setGmtCreate(param.getGmtCreate());
monitorParam.setGmtUpdate(param.getGmtUpdate());
return monitorParam;
}
public Param toEntity() {
Param param = new Param();
param.setId(id);
param.setMonitorId(monitorId);
param.setField(field);
param.setParamValue(paramValue);
param.setType(type);
param.setGmtCreate(gmtCreate);
param.setGmtUpdate(gmtUpdate);
return param;
}
}
@@ -21,7 +21,6 @@ import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
/**
* Parameters define transfer entities
@@ -33,5 +32,5 @@ public class ParamDefineDto {
private String app;
private List<ParamDefine> param;
private List<ParamDefineInfo> param;
}
@@ -0,0 +1,144 @@
/*
* 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.manager.pojo.dto;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.RuntimeParamDefine;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
/**
* Manager-side parameter definition DTO detached from JPA annotations.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ParamDefineInfo {
private Long id;
private String app;
private Map<String, String> name;
private String field;
private String type;
private boolean required;
private String defaultValue;
private String placeholder;
private String range;
private Short limit;
private List<OptionInfo> options;
private String keyAlias;
private String valueAlias;
private boolean hide;
private String creator;
private String modifier;
private LocalDateTime gmtCreate;
private LocalDateTime gmtUpdate;
private Map<String, List<Object>> depend;
public static ParamDefineInfo fromEntity(ParamDefine paramDefine) {
if (paramDefine == null) {
return null;
}
ParamDefineInfo info = new ParamDefineInfo();
info.setId(paramDefine.getId());
info.setApp(paramDefine.getApp());
info.setName(paramDefine.getName());
info.setField(paramDefine.getField());
info.setType(paramDefine.getType());
info.setRequired(paramDefine.isRequired());
info.setDefaultValue(paramDefine.getDefaultValue());
info.setPlaceholder(paramDefine.getPlaceholder());
info.setRange(paramDefine.getRange());
info.setLimit(paramDefine.getLimit());
info.setOptions(paramDefine.getOptions() == null ? null
: paramDefine.getOptions().stream().map(OptionInfo::fromEntity).toList());
info.setKeyAlias(paramDefine.getKeyAlias());
info.setValueAlias(paramDefine.getValueAlias());
info.setHide(paramDefine.isHide());
info.setCreator(paramDefine.getCreator());
info.setModifier(paramDefine.getModifier());
info.setGmtCreate(paramDefine.getGmtCreate());
info.setGmtUpdate(paramDefine.getGmtUpdate());
info.setDepend(paramDefine.getDepend());
return info;
}
public static ParamDefineInfo fromRuntime(RuntimeParamDefine runtimeParamDefine) {
if (runtimeParamDefine == null) {
return null;
}
ParamDefineInfo info = new ParamDefineInfo();
info.setApp(runtimeParamDefine.getApp());
info.setName(runtimeParamDefine.getName());
info.setField(runtimeParamDefine.getField());
info.setType(runtimeParamDefine.getType());
info.setRequired(runtimeParamDefine.isRequired());
info.setDefaultValue(runtimeParamDefine.getDefaultValue());
info.setPlaceholder(runtimeParamDefine.getPlaceholder());
info.setRange(runtimeParamDefine.getRange());
info.setLimit(runtimeParamDefine.getLimit());
info.setOptions(runtimeParamDefine.getOptions() == null ? null
: runtimeParamDefine.getOptions().stream().map(OptionInfo::fromRuntime).toList());
info.setKeyAlias(runtimeParamDefine.getKeyAlias());
info.setValueAlias(runtimeParamDefine.getValueAlias());
info.setHide(runtimeParamDefine.isHide());
info.setDepend(runtimeParamDefine.getDepend());
return info;
}
/**
* DTO version of parameter options.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class OptionInfo {
private String label;
private String value;
public static OptionInfo fromEntity(ParamDefine.Option option) {
return option == null ? null : new OptionInfo(option.getLabel(), option.getValue());
}
public static OptionInfo fromRuntime(RuntimeParamDefine.Option option) {
return option == null ? null : new OptionInfo(option.getLabel(), option.getValue());
}
}
}

Some files were not shown because too many files have changed in this diff Show More