diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 9eac427992d..b3141dfc8d9 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -21,4 +21,4 @@ jobs: with: java-version: 11 - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn clean -B package --file pom.xml diff --git a/alerter/README.md b/alerter/README.md index 4b35a15e6ba..cf9a65783a7 100644 --- a/alerter/README.md +++ b/alerter/README.md @@ -1,4 +1,4 @@ -### alerter +### Alerter Processing data according to threshold rules to determine alarms, and alarm distribution functions. -根据告警规则配置信息,处理指标数据判断告警,告警分发。 \ No newline at end of file +根据告警规则配置信息,处理指标数据判断告警,告警分发。 diff --git a/collector/pom.xml b/collector/pom.xml index 85f05f89e4a..8dbbb2efd79 100644 --- a/collector/pom.xml +++ b/collector/pom.xml @@ -51,6 +51,11 @@ org.dromara.hertzbeat hertzbeat-common + + + org.dromara.hertzbeat + hertzbeat-remoting + org.springframework.boot diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/collect/mq/RocketmqSingleCollectImpl.java b/collector/src/main/java/org/dromara/hertzbeat/collector/collect/mq/RocketmqSingleCollectImpl.java index f8d5727a983..ed6c1b5f5a4 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/collect/mq/RocketmqSingleCollectImpl.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/collect/mq/RocketmqSingleCollectImpl.java @@ -2,6 +2,7 @@ import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -77,14 +78,14 @@ public RocketmqSingleCollectImpl() { Runtime runtime = Runtime.getRuntime(); int corePoolSize = Math.max(8, runtime.availableProcessors()); int maximumPoolSize = Math.max(16, runtime.availableProcessors()); - ThreadFactory threadFactory = new ThreadFactory() { - private final AtomicLong threadIndex = new AtomicLong(0); - - @Override - public Thread newThread(@NotNull Runnable r) { - return new Thread(r, "RocketMQCollectGroup_" + this.threadIndex.incrementAndGet()); - } - }; + 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()); } @@ -131,6 +132,7 @@ public String supportProtocol() { /** * 采集前置条件, 入参判断 + * * @param metrics 数据指标 */ private void preCheck(Metrics metrics) { @@ -144,6 +146,7 @@ private void preCheck(Metrics metrics) { /** * 创建DefaultMQAdminExt实体类; 这里有个小问题, 是否需要每次都重新创建 + * * @param metrics 数据指标 * @return DefaultMQAdminExt */ @@ -162,7 +165,8 @@ private DefaultMQAdminExt createMqAdminExt(Metrics metrics) { /** * 采集rocketmq数据 - * @param mqAdminExt rocketmq提供的远程调用类 + * + * @param mqAdminExt rocketmq提供的远程调用类 * @param rocketmqCollectData rocketmq数据采集类 * @throws Exception 远程调用异常 */ @@ -174,7 +178,8 @@ private void collectData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData rocke /** * 采集rocketmq的集群数据 - * @param mqAdminExt rocketmq提供的远程调用类 + * + * @param mqAdminExt rocketmq提供的远程调用类 * @param rocketmqCollectData rocketmq数据采集类 * @throws Exception 远程调用异常 */ @@ -244,7 +249,8 @@ private void collectClusterData(DefaultMQAdminExt mqAdminExt, RocketmqCollectDat /** * 采集rocketmq的消费者数据 - * @param mqAdminExt rocketmq提供的远程调用类 + * + * @param mqAdminExt rocketmq提供的远程调用类 * @param rocketmqCollectData rocketmq数据采集类 * @throws Exception 远程调用异常 */ @@ -273,8 +279,7 @@ private void collectConsumerData(DefaultMQAdminExt mqAdminExt, RocketmqCollectDa ConsumeStats consumeStats = null; try { consumeStats = mqAdminExt.examineConsumeStats(consumerGroup); - } - catch (Exception e) { + } catch (Exception e) { log.warn("examineConsumeStats exception to consumerGroup {}, response [{}]", consumerGroup, e.getMessage()); } if (consumeStats != null) { @@ -285,8 +290,7 @@ private void collectConsumerData(DefaultMQAdminExt mqAdminExt, RocketmqCollectDa ConsumerConnection consumerConnection = null; try { consumerConnection = mqAdminExt.examineConsumerConnectionInfo(consumerGroup); - } - catch (Exception e) { + } catch (Exception e) { log.warn("examineConsumeStats exception to consumerGroup {}, response [{}]", consumerGroup, e.getMessage()); } if (consumerConnection != null) { @@ -312,8 +316,7 @@ private void collectConsumerData(DefaultMQAdminExt mqAdminExt, RocketmqCollectDa } /** - * - * @param mqAdminExt rocketmq提供的远程调用类 + * @param mqAdminExt rocketmq提供的远程调用类 * @param rocketmqCollectData rocketmq数据采集类 * @throws Exception 远程调用异常 */ @@ -343,10 +346,11 @@ private void collectTopicData(DefaultMQAdminExt mqAdminExt, RocketmqCollectData /** * 采集数据填充到builder + * * @param rocketmqCollectData rocketmq数据采集类 - * @param builder metrics data builder - * @param aliasFields 字段别名 - * @param parseScript JSON的base path + * @param builder metrics data builder + * @param aliasFields 字段别名 + * @param parseScript JSON的base path */ private void fillBuilder(RocketmqCollectData rocketmqCollectData, CollectRep.MetricsData.Builder builder, List aliasFields, String parseScript) { String dataJson = JSONObject.toJSONString(rocketmqCollectData); diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CollectDataDispatch.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CollectDataDispatch.java index b1fe3f1ece9..83280301e69 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CollectDataDispatch.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CollectDataDispatch.java @@ -25,9 +25,6 @@ /** * Collection data scheduler interface * 采集数据调度器接口 - * - * - * */ public interface CollectDataDispatch { diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CommonDispatcher.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CommonDispatcher.java index 5651762545c..21481c6850b 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CommonDispatcher.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/CommonDispatcher.java @@ -17,7 +17,6 @@ package org.dromara.hertzbeat.collector.dispatch; -import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonElement; import lombok.AllArgsConstructor; @@ -34,16 +33,17 @@ import org.dromara.hertzbeat.common.entity.job.Metrics; import org.dromara.hertzbeat.common.entity.message.CollectRep; import org.dromara.hertzbeat.common.queue.CommonDataQueue; +import org.springframework.beans.factory.DisposableBean; import org.springframework.stereotype.Component; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.IntStream; /** * Indicator group collection task and response data scheduler @@ -53,7 +53,7 @@ */ @Component @Slf4j -public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatch { +public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatch, DisposableBean { /** * Metric group collection task timeout value @@ -89,6 +89,10 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc private final List unitConvertList; + private final ThreadPoolExecutor poolExecutor; + + private final WorkerPool workerPool; + public CommonDispatcher(MetricsCollectorQueue jobRequestQueue, TimerDispatch timerDispatch, CommonDataQueue commonDataQueue, @@ -98,78 +102,93 @@ public CommonDispatcher(MetricsCollectorQueue jobRequestQueue, this.jobRequestQueue = jobRequestQueue; this.timerDispatch = timerDispatch; this.unitConvertList = unitConvertList; - ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 2, 1, + this.workerPool = workerPool; + this.metricsTimeoutMonitorMap = new ConcurrentHashMap<>(16); + poolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> { Thread thread = new Thread(r); thread.setDaemon(true); return thread; }); - //增加线程池延迟10秒钟优雅停机 - MoreExecutors.addDelayedShutdownHook(poolExecutor, 10, TimeUnit.SECONDS); - // Pull the indicator group collection task from the task queue and put it into the thread pool for execution - // 从任务队列拉取指标组采集任务放入线程池执行 - poolExecutor.execute(() -> { - Thread.currentThread().setName("metrics-task-dispatcher"); - while (!Thread.currentThread().isInterrupted()) { - MetricsCollect metricsCollect = null; - try { - metricsCollect = jobRequestQueue.getJob(); - if (metricsCollect != null) { - workerPool.executeJob(metricsCollect); - } - } catch (RejectedExecutionException rejected) { - log.info("[Dispatcher]-the worker pool is full, reject this metrics task, " + - "sleep and put in queue again."); + this.start(); + } + + public void start() { + try { + // Pull the indicator group collection task from the task queue and put it into the thread pool for execution + // 从任务队列拉取指标组采集任务放入线程池执行 + poolExecutor.execute(() -> { + Thread.currentThread().setName("metrics-task-dispatcher"); + while (!Thread.currentThread().isInterrupted()) { + MetricsCollect metricsCollect = null; try { - Thread.sleep(1000); + metricsCollect = jobRequestQueue.getJob(); if (metricsCollect != null) { - // 在队列里的优先级增大 - metricsCollect.setRunPriority((byte) (metricsCollect.getRunPriority() + 1)); - jobRequestQueue.addJob(metricsCollect); + workerPool.executeJob(metricsCollect); } - } catch (InterruptedException ignored) { + } catch (RejectedExecutionException rejected) { + log.info("[Dispatcher]-the worker pool is full, reject this metrics task, " + + "sleep and put in queue again."); + try { + Thread.sleep(1000); + if (metricsCollect != null) { + // 在队列里的优先级增大 + metricsCollect.setRunPriority((byte) (metricsCollect.getRunPriority() + 1)); + jobRequestQueue.addJob(metricsCollect); + } + } catch (InterruptedException ignored) { + log.info("[Dispatcher]-metrics-task-dispatcher has been interrupt when sleep to close."); + Thread.currentThread().interrupt(); + } + } catch (InterruptedException interruptedException) { + log.info("[Dispatcher]-metrics-task-dispatcher has been interrupt to close."); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("[Dispatcher]-{}.", e.getMessage(), e); } - } catch (Exception e) { - log.error("[Dispatcher]-{}.", e.getMessage(), e); } - } - }); - // Monitoring indicator group collection task execution t - // 监控指标组采集任务执行时间 - metricsTimeoutMonitorMap = new ConcurrentHashMap<>(128); - poolExecutor.execute(() -> { - Thread.currentThread().setName("metrics-task-monitor"); - while (!Thread.currentThread().isInterrupted()) { - try { - // Detect whether the collection unit of each indicator group has timed out for 4 minutes, and if it times out, it will be discarded and an exception will be returned. - // 检测每个指标组采集单元是否超时4分钟,超时则丢弃并返回异常 - long deadline = System.currentTimeMillis() - DURATION_TIME; - for (Map.Entry entry : metricsTimeoutMonitorMap.entrySet()) { - MetricsTime metricsTime = entry.getValue(); - if (metricsTime.getStartTime() < deadline) { - // Metric group collection timeout 指标组采集超时 - WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task(); - CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() - .setId(timerJob.getJob().getMonitorId()) - .setApp(timerJob.getJob().getApp()) - .setMetrics(metricsTime.getMetrics().getName()) - .setPriority(metricsTime.getMetrics().getPriority()) - .setTime(System.currentTimeMillis()) - .setCode(CollectRep.Code.TIMEOUT).setMsg("collect timeout").build(); - log.error("[Collect Timeout]: \n{}", metricsData); - if (metricsData.getPriority() == 0) { - dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData); + }); + // Monitoring indicator group collection task execution t + // 监控指标组采集任务执行时间 + poolExecutor.execute(() -> { + Thread.currentThread().setName("metrics-task-monitor"); + while (!Thread.currentThread().isInterrupted()) { + try { + // Detect whether the collection unit of each indicator group has timed out for 4 minutes, and if it times out, it will be discarded and an exception will be returned. + // 检测每个指标组采集单元是否超时4分钟,超时则丢弃并返回异常 + long deadline = System.currentTimeMillis() - DURATION_TIME; + for (Map.Entry entry : metricsTimeoutMonitorMap.entrySet()) { + MetricsTime metricsTime = entry.getValue(); + if (metricsTime.getStartTime() < deadline) { + // Metric group collection timeout 指标组采集超时 + WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task(); + CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() + .setId(timerJob.getJob().getMonitorId()) + .setApp(timerJob.getJob().getApp()) + .setMetrics(metricsTime.getMetrics().getName()) + .setPriority(metricsTime.getMetrics().getPriority()) + .setTime(System.currentTimeMillis()) + .setCode(CollectRep.Code.TIMEOUT).setMsg("collect timeout").build(); + log.error("[Collect Timeout]: \n{}", metricsData); + if (metricsData.getPriority() == 0) { + dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData); + } + metricsTimeoutMonitorMap.remove(entry.getKey()); } - metricsTimeoutMonitorMap.remove(entry.getKey()); } + Thread.sleep(20000); + } catch (InterruptedException interruptedException) { + log.info("[Dispatcher]-metrics-task-monitor has been interrupt to close."); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("[Task Monitor]-{}.", e.getMessage(), e); } - Thread.sleep(20000); - } catch (Exception e) { - log.error("[Monitor]-{}.", e.getMessage(), e); } - } - }); + }); + } catch (Exception e) { + log.error("Common Dispatcher error: {}.", e.getMessage(), e); + } } @Override @@ -320,19 +339,31 @@ private List> getConfigmapFromPreCollectData(CollectRep.M if (metricsData.getValuesCount() <= 0 || metricsData.getFieldsCount() <= 0) { return null; } - return CollectionUtils.emptyIfNull(metricsData.getValuesList()) - .stream() - .filter(ele -> ele.getColumnsCount() == metricsData.getFieldsCount()) - .map(ele -> IntStream.range(0, metricsData.getFieldsList().size()) - .boxed() - .map(index -> { - String value = ele.getColumns(index); - CollectRep.Field field = metricsData.getFieldsList().get(index); - return new Configmap(field.getName(), value, Integer.valueOf(field.getType()).byteValue()); - }).collect(Collectors.toMap(Configmap::getKey, item -> item, (a, b) -> b))) - .collect(Collectors.toList()); + List> mapList = new LinkedList<>(); + for (CollectRep.ValueRow valueRow : metricsData.getValuesList()) { + if (valueRow.getColumnsCount() != metricsData.getFieldsCount()) { + continue; + } + Map configmapMap = new HashMap<>(valueRow.getColumnsCount()); + int index = 0; + for (CollectRep.Field field : metricsData.getFieldsList()) { + String value = valueRow.getColumns(index); + index++; + Configmap configmap = new Configmap(field.getName(), value, Integer.valueOf(field.getType()).byteValue()); + configmapMap.put(field.getName(), configmap); + } + mapList.add(configmapMap); + } + return mapList; } - + + @Override + public void destroy() throws Exception { + if (poolExecutor != null) { + poolExecutor.shutdownNow(); + } + } + @Data @AllArgsConstructor private static class MetricsTime { diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchConstants.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchConstants.java index f9a2ffe5ab2..57358c308b1 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchConstants.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchConstants.java @@ -19,9 +19,6 @@ /** * dispatch constant 常量 - * - * - * */ public interface DispatchConstants { diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchProperties.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchProperties.java index 218e948a436..783becf841b 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchProperties.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/DispatchProperties.java @@ -23,9 +23,6 @@ /** * Schedule Distribution Task Configuration Properties * 调度分发任务配置属性 - * - * - * */ @Component @ConfigurationProperties(prefix = "collector.dispatch") diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollect.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollect.java index 998eab73500..b2e10b18335 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollect.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollect.java @@ -40,9 +40,6 @@ /** * Index group collection * 指标组采集 - * - * - * */ @Slf4j @Data diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollectorQueue.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollectorQueue.java index 26961ba3e9f..1374acd8efb 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollectorQueue.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsCollectorQueue.java @@ -26,9 +26,6 @@ /** * queue of jobs to run * 待运行的job队列 - * - * - * */ @Component @Slf4j diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsTaskDispatch.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsTaskDispatch.java index 206c5ccc215..0188f68c850 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsTaskDispatch.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/MetricsTaskDispatch.java @@ -22,9 +22,6 @@ /** * Metric group collection task scheduler interface * 指标组采集任务调度器接口 - * - * - * */ public interface MetricsTaskDispatch { diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/WorkerPool.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/WorkerPool.java index 6383dca979f..68e97ca124d 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/WorkerPool.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/WorkerPool.java @@ -31,9 +31,6 @@ /** * Collection task worker thread pool * 采集任务工作线程池 - * - * - * */ @Component @Slf4j diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/CollectServer.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/CollectServer.java new file mode 100644 index 00000000000..6150eb15aec --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/CollectServer.java @@ -0,0 +1,139 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance; + + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.channel.Channel; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.collector.dispatch.DispatchProperties; +import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.CollectCyclicDataProcessor; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.CollectOneTimeDataProcessor; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.GoCloseProcessor; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.GoOfflineProcessor; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProcessor; +import org.dromara.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor; +import org.dromara.hertzbeat.common.entity.dto.CollectorInfo; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.CommonThreadPool; +import org.dromara.hertzbeat.common.util.IpDomainUtil; +import org.dromara.hertzbeat.common.util.JsonUtil; +import org.dromara.hertzbeat.remoting.RemotingClient; +import org.dromara.hertzbeat.remoting.event.NettyEventListener; +import org.dromara.hertzbeat.remoting.netty.NettyClientConfig; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * collect server + */ +@Component +@ConditionalOnProperty(prefix = "collector.dispatch.entrance.netty", + name = "enabled", havingValue = "true") +@Slf4j +public class CollectServer { + + private final CollectJobService collectJobService; + + private RemotingClient remotingClient; + + private ScheduledExecutorService scheduledExecutor; + + public CollectServer(final CollectJobService collectJobService, + final DispatchProperties properties, + final CommonThreadPool threadPool) { + 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"); + } + DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty(); + if (nettyProperties.getManagerIp() == null || nettyProperties.getManagerPort() == 0) { + throw new IllegalArgumentException("please config dispatch entrance netty master ip and port"); + } + this.collectJobService = collectJobService; + this.collectJobService.setCollectServer(this); + + this.init(properties, threadPool); + this.remotingClient.start(); + } + + private void init(final DispatchProperties properties, final CommonThreadPool threadPool) { + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty(); + nettyClientConfig.setServerIp(nettyProperties.getManagerIp()); + nettyClientConfig.setServerPort(nettyProperties.getManagerPort()); + this.remotingClient = new NettyRemotingClient(nettyClientConfig, new CollectNettyEventListener(), threadPool); + + this.remotingClient.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor()); + this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(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)); + } + + public void shutdown() { + this.scheduledExecutor.shutdownNow(); + + this.remotingClient.shutdown(); + } + + public CollectJobService getCollectJobService() { + return collectJobService; + } + + public void sendMsg(final ClusterMsg.Message message) { + this.remotingClient.sendMsg(message); + } + + public class CollectNettyEventListener implements NettyEventListener { + + @Override + public void onChannelActive(Channel channel) { + String identity = CollectServer.this.collectJobService.getCollectorIdentity(); + CollectorInfo collectorInfo = CollectorInfo.builder() + .name(identity) + .ip(IpDomainUtil.getLocalhostIp()) + // todo more info + .build(); + // send online message + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setIdentity(identity) + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setMsg(JsonUtil.toJson(collectorInfo)) + .build(); + CollectServer.this.sendMsg(message); + + if (scheduledExecutor == null) { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("HeartBeat Scheduler has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("heartbeat-worker-%d") + .build(); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); + // schedule send heartbeat message + scheduledExecutor.scheduleAtFixedRate(() -> { + ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder() + .setIdentity(identity) + .setType(ClusterMsg.MessageType.HEARTBEAT) + .build(); + CollectServer.this.sendMsg(heartbeat); + log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis()); + }, 5, 5, TimeUnit.SECONDS); + } + } + + @Override + public void onChannelIdle(Channel channel) { + log.info("handle idle event triggered. collector is going offline."); + } + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java index d291a198db1..7ca538adc34 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/internal/CollectJobService.java @@ -17,11 +17,10 @@ package org.dromara.hertzbeat.collector.dispatch.entrance.internal; -import io.netty.channel.Channel; import org.dromara.hertzbeat.collector.dispatch.DispatchProperties; import org.dromara.hertzbeat.collector.dispatch.WorkerPool; +import org.dromara.hertzbeat.collector.dispatch.entrance.CollectServer; import org.dromara.hertzbeat.collector.dispatch.timer.TimerDispatch; -import org.dromara.hertzbeat.common.entity.dto.CollectorInfo; import org.dromara.hertzbeat.common.entity.job.Job; import org.dromara.hertzbeat.common.entity.message.ClusterMsg; import org.dromara.hertzbeat.common.entity.message.CollectRep; @@ -36,8 +35,6 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** @@ -51,20 +48,20 @@ public class CollectJobService { private static final String COLLECTOR_STR = "-collector"; - + private final TimerDispatch timerDispatch; - + private final WorkerPool workerPool; - + private String collectorIdentity = null; - - private volatile Channel collectorChannel = null; - + + private CollectServer collectServer; + public CollectJobService(TimerDispatch timerDispatch, DispatchProperties properties, WorkerPool workerPool) { this.timerDispatch = timerDispatch; this.workerPool = workerPool; - if (properties != null && properties.getEntrance() != null - && properties.getEntrance().getNetty() != null && properties.getEntrance().getNetty().isEnabled()) { + if (properties != null && properties.getEntrance() != null + && properties.getEntrance().getNetty() != null && properties.getEntrance().getNetty().isEnabled()) { String collectorName = properties.getEntrance().getNetty().getIdentity(); if (StringUtils.hasText(collectorName)) { collectorIdentity = collectorName; @@ -75,7 +72,7 @@ public CollectJobService(TimerDispatch timerDispatch, DispatchProperties propert } } } - + /** * Execute a one-time collection task and get the collected data response * 执行一次性采集任务,获取采集数据响应 @@ -103,14 +100,13 @@ public void response(List responseMetrics) { } return metricsData; } - + /** * Execute a one-time collection task and send the collected data response * * @param oneTimeJob Collect task details 采集任务详情 - * @param channel channel */ - public void collectSyncJobData(Job oneTimeJob, Channel channel) { + public void collectSyncOneTimeJobData(Job oneTimeJob) { workerPool.executeJob(() -> { List metricsDataList = this.collectSyncJobData(oneTimeJob); List jsons = new ArrayList<>(metricsDataList.size()); @@ -121,9 +117,12 @@ public void collectSyncJobData(Job oneTimeJob, Channel channel) { } } String response = JsonUtil.toJson(jsons); - channel.writeAndFlush(ClusterMsg.Message.newBuilder() - .setMsg(response) - .setType(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA).build()); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setMsg(response) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA) + .build(); + this.collectServer.sendMsg(message); }); } @@ -148,50 +147,28 @@ public void cancelAsyncCollectJob(Long jobId) { timerDispatch.deleteJob(jobId, true); } } - - /** - * collector online - * @param channel message channel - */ - public void collectorGoOnline(Channel channel) { - collectorChannel = channel; - CollectorInfo collectorInfo = CollectorInfo.builder() - .name(collectorIdentity) - .ip(IpDomainUtil.getLocalhostIp()) - // todo more info - .build(); - String msg = JsonUtil.toJson(collectorInfo); - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setIdentity(collectorIdentity) - .setType(ClusterMsg.MessageType.GO_ONLINE) - .setMsg(msg) - .build(); - channel.writeAndFlush(message); - // start a thread to send heartbeat to cluster server periodically - ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - scheduledExecutor.scheduleAtFixedRate(() -> { - if (collectorChannel.isActive()) { - ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder() - .setIdentity(collectorIdentity) - .setType(ClusterMsg.MessageType.HEARTBEAT) - .build(); - collectorChannel.writeAndFlush(heartbeat); - log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis()); - } - }, 5, 5, TimeUnit.SECONDS); - } - + /** * send async collect response data + * * @param metricsData collect data */ public void sendAsyncCollectData(CollectRep.MetricsData metricsData) { String data = ProtoJsonUtil.toJsonStr(metricsData); - ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder() - .setIdentity(collectorIdentity) - .setMsg(data) - .setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA) - .build(); - collectorChannel.writeAndFlush(heartbeat); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setIdentity(collectorIdentity) + .setMsg(data) + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA) + .build(); + this.collectServer.sendMsg(message); + } + + public String getCollectorIdentity() { + return collectorIdentity; + } + + public void setCollectServer(CollectServer collectServer) { + this.collectServer = collectServer; } } diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ClientInboundMessageHandler.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ClientInboundMessageHandler.java deleted file mode 100644 index 15d48d5606a..00000000000 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ClientInboundMessageHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.dromara.hertzbeat.collector.dispatch.entrance.netty; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; -import org.dromara.hertzbeat.common.entity.job.Job; -import org.dromara.hertzbeat.common.entity.message.ClusterMsg; -import org.dromara.hertzbeat.common.util.JsonUtil; - -import java.util.List; - -/** - * netty inbound collector message handler - * - */ -@Slf4j -public class ClientInboundMessageHandler extends SimpleChannelInboundHandler { - - private final CollectJobService collectJobService; - - public ClientInboundMessageHandler(CollectJobService collectJobService) { - this.collectJobService = collectJobService; - } - - @Override - protected void channelRead0(ChannelHandlerContext channelHandlerContext, ClusterMsg.Message message) throws Exception { - Channel channel = channelHandlerContext.channel(); - switch (message.getType()) { - case HEARTBEAT: - log.info("collector receive manager server response heartbeat, time: {}. ", System.currentTimeMillis()); - break; - case ISSUE_CYCLIC_TASK: - Job job = JsonUtil.fromJson(message.getMsg(), Job.class); - if (job == null) { - log.error("collector receive cyclic task job is null"); - return; - } - collectJobService.addAsyncCollectJob(job); - break; - case ISSUE_ONE_TIME_TASK: - Job oneTimeJob = JsonUtil.fromJson(message.getMsg(), Job.class); - collectJobService.collectSyncJobData(oneTimeJob, channel); - break; - case DELETE_CYCLIC_TASK: - TypeReference> typeReference = new TypeReference<>() {}; - List jobs = JsonUtil.fromJson(message.getMsg(), typeReference); - if (jobs != null && !jobs.isEmpty()) { - for (Long jobId : jobs) { - collectJobService.cancelAsyncCollectJob(jobId); - } - } - break; - } - } - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - Channel channel = ctx.channel(); - // go online to cluster master - collectJobService.collectorGoOnline(channel); - } -} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/CollectServer.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/CollectServer.java deleted file mode 100644 index 77fb45080f1..00000000000 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/CollectServer.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.dromara.hertzbeat.collector.dispatch.entrance.netty; - -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hertzbeat.collector.dispatch.DispatchProperties; -import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; -import org.dromara.hertzbeat.common.support.CommonThreadPool; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -/** - * collect server for cluster - * - */ -@Component -@ConditionalOnProperty(prefix = "collector.dispatch.entrance.netty", - name = "enabled", havingValue = "true") -@Slf4j -public class CollectServer { - - private final CollectJobService collectJobService; - - private final CommonThreadPool commonThreadPool; - - public CollectServer(DispatchProperties properties, CollectJobService jobService, CommonThreadPool threadPool) throws Exception { - 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"); - } - DispatchProperties.EntranceProperties.NettyProperties nettyProperties = properties.getEntrance().getNetty(); - if (nettyProperties.getManagerIp() == null || nettyProperties.getManagerPort() == 0) { - throw new IllegalArgumentException("please config dispatch entrance netty master ip and port"); - } - this.collectJobService = jobService; - this.commonThreadPool = threadPool; - collectorClientStartup(nettyProperties); - } - - private void collectorClientStartup(DispatchProperties.EntranceProperties.NettyProperties properties) throws Exception { - commonThreadPool.execute(() -> { - EventLoopGroup workerGroup = new NioEventLoopGroup(); - Bootstrap b = new Bootstrap(); - b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); - b.group(workerGroup) - .channel(NioSocketChannel.class) - .handler(new ProtoClientInitializer(collectJobService)); - Channel channel = null; - boolean first = true; - while (first || channel == null || !channel.isActive()) { - first = false; - try { - channel = b.connect(properties.getManagerIp(), properties.getManagerPort()).sync().channel(); - channel.closeFuture().sync(); - } catch (InterruptedException ignored) { - log.error("collector shutdown now!"); - } catch (Exception e2) { - log.error("collector connect cluster server error: {}. try after 10s." , e2.getMessage()); - try { - Thread.sleep(10000); - } catch (InterruptedException ignored) {} - } - } - workerGroup.shutdownGracefully(); - }); - } - - -} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ProtoClientInitializer.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ProtoClientInitializer.java deleted file mode 100644 index 2df8f693739..00000000000 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/netty/ProtoClientInitializer.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.dromara.hertzbeat.collector.dispatch.entrance.netty; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.compression.ZlibCodecFactory; -import io.netty.handler.codec.compression.ZlibWrapper; -import io.netty.handler.codec.protobuf.ProtobufDecoder; -import io.netty.handler.codec.protobuf.ProtobufEncoder; -import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; -import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; -import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; -import org.dromara.hertzbeat.common.entity.message.ClusterMsg; - -/** - * netty client initializer - * - */ -public class ProtoClientInitializer extends ChannelInitializer { - - private final CollectJobService collectJobService; - - public ProtoClientInitializer(CollectJobService collectJobService) { - this.collectJobService = collectJobService; - } - - @Override - protected void initChannel(SocketChannel socketChannel) throws Exception { - ChannelPipeline pipeline = socketChannel.pipeline(); - // zip - pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); - pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); - // protocol buf encode decode - pipeline.addLast(new ProtobufVarint32FrameDecoder()); - pipeline.addLast(new ProtobufDecoder(ClusterMsg.Message.getDefaultInstance())); - pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); - pipeline.addLast(new ProtobufEncoder()); - // message handler - pipeline.addLast(new ClientInboundMessageHandler(collectJobService)); - } -} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java new file mode 100644 index 00000000000..7cc3e07274f --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectCyclicDataProcessor.java @@ -0,0 +1,32 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.dromara.hertzbeat.common.entity.job.Job; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.util.JsonUtil; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle cyclic data message + */ +@Slf4j +public class CollectCyclicDataProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + + public CollectCyclicDataProcessor(CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + Job job = JsonUtil.fromJson(message.getMsg(), Job.class); + if (job == null) { + log.error("collector receive cyclic task job is null"); + return null; + } + collectServer.getCollectJobService().addAsyncCollectJob(job); + return null; + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java new file mode 100644 index 00000000000..f0436ac704e --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/CollectOneTimeDataProcessor.java @@ -0,0 +1,26 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import org.dromara.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.dromara.hertzbeat.common.entity.job.Job; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.util.JsonUtil; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle one-time collect data response message + */ +public class CollectOneTimeDataProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + + public CollectOneTimeDataProcessor(final CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + Job oneTimeJob = JsonUtil.fromJson(message.getMsg(), Job.class); + collectServer.getCollectJobService().collectSyncOneTimeJobData(oneTimeJob); + return null; + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java new file mode 100644 index 00000000000..598a43dba94 --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoCloseProcessor.java @@ -0,0 +1,37 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.collector.dispatch.entrance.CollectServer; +import org.dromara.hertzbeat.collector.dispatch.timer.TimerDispatch; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.SpringContextHolder; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; +import org.springframework.boot.SpringApplication; + +/** + * handle collector close message + * 注: 这里会关闭采集任务, 同时断开与Manager的连接 + */ +@Slf4j +public class GoCloseProcessor implements NettyRemotingProcessor { + private final CollectServer collectServer; + private TimerDispatch timerDispatch; + + public GoCloseProcessor(final CollectServer collectServer) { + this.collectServer = collectServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + if (this.timerDispatch == null) { + this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class); + } + this.timerDispatch.goOffline(); + this.collectServer.shutdown(); + SpringApplication.exit(SpringContextHolder.getApplicationContext(), () -> 0); + SpringContextHolder.shutdown(); + log.info("receive offline message and close success"); + return null; + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java new file mode 100644 index 00000000000..4efb8f338c9 --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOfflineProcessor.java @@ -0,0 +1,33 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.collector.dispatch.timer.TimerDispatch; +import org.dromara.hertzbeat.common.constants.CommonConstants; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.SpringContextHolder; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector offline message + * 注: 这里不关闭与Manager的连接, 只是关闭采集功能 + */ +@Slf4j +public class GoOfflineProcessor implements NettyRemotingProcessor { + + private 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"); + return ClusterMsg.Message.newBuilder() + .setIdentity(message.getIdentity()) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(String.valueOf(CommonConstants.SUCCESS_CODE)) + .build(); + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java new file mode 100644 index 00000000000..2f7c885ae9c --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/GoOnlineProcessor.java @@ -0,0 +1,33 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.collector.dispatch.timer.TimerDispatch; +import org.dromara.hertzbeat.common.constants.CommonConstants; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.SpringContextHolder; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector online message + * 注: 这里不是重新打开与Manager的连接, 也做不到, 只是重新开启采集功能 + */ +@Slf4j +public class GoOnlineProcessor implements NettyRemotingProcessor { + + private TimerDispatch timerDispatch; + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + if (this.timerDispatch == null) { + this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class); + } + timerDispatch.goOnline(); + log.info("receive online message and close success"); + return ClusterMsg.Message.newBuilder() + .setIdentity(message.getIdentity()) + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(String.valueOf(CommonConstants.SUCCESS_CODE)) + .build(); + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java new file mode 100644 index 00000000000..4b58b307bd5 --- /dev/null +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/entrance/processor/HeartbeatProcessor.java @@ -0,0 +1,18 @@ +package org.dromara.hertzbeat.collector.dispatch.entrance.processor; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle heartbeat message + */ +@Slf4j +public class HeartbeatProcessor implements NettyRemotingProcessor { + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + log.info("collector receive manager server response heartbeat, time: {}. ", System.currentTimeMillis()); + return null; + } +} diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/export/NettyDataQueue.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/export/NettyDataQueue.java index 23c49284e25..688c6e5fa4f 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/export/NettyDataQueue.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/export/NettyDataQueue.java @@ -22,15 +22,9 @@ import org.dromara.hertzbeat.common.entity.alerter.Alert; import org.dromara.hertzbeat.common.entity.message.CollectRep; import org.dromara.hertzbeat.common.queue.CommonDataQueue; -import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - /** * for collector instance * send collect response data by netty diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatch.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatch.java index 7c3c3442adc..a178b47d924 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatch.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatch.java @@ -60,6 +60,16 @@ public interface TimerDispatch { * 是否是周期性任务,true是, false为临时性任务 */ void deleteJob(long jobId, boolean isCyclic); + + /** + * job dispatcher go online + */ + void goOnline(); + + /** + * job dispatcher go offline + */ + void goOffline(); /** * 一次性同步采集任务采集结果通知监听器 diff --git a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatcher.java b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatcher.java index 74b9d634998..35cf845d5ee 100644 --- a/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatcher.java +++ b/collector/src/main/java/org/dromara/hertzbeat/collector/dispatch/timer/TimerDispatcher.java @@ -17,45 +17,54 @@ package org.dromara.hertzbeat.collector.dispatch.timer; +import lombok.extern.slf4j.Slf4j; import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; import org.dromara.hertzbeat.common.entity.job.Job; import org.dromara.hertzbeat.common.entity.message.CollectRep; +import org.springframework.beans.factory.DisposableBean; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** - * + * job timer dispatcher * */ @Component -public class TimerDispatcher implements TimerDispatch { +@Slf4j +public class TimerDispatcher implements TimerDispatch, DisposableBean { /** * time round schedule * 时间轮调度 */ - private Timer wheelTimer; + private final Timer wheelTimer; /** * Existing periodic scheduled tasks * 已存在的周期性调度任务 */ - private Map currentCyclicTaskMap; + private final Map currentCyclicTaskMap; /** * Existing temporary scheduled tasks * 已存在的临时性调度任务 */ - private Map currentTempTaskMap; + private final Map currentTempTaskMap; /** * One-time task response listener holds * 一次性任务响应监听器持有 * jobId - listener */ - private Map eventListeners; - + private final Map eventListeners; + + /** + * is dispatcher online running + */ + private final AtomicBoolean started; + public TimerDispatcher() { this.wheelTimer = new HashedWheelTimer(r -> { Thread ret = new Thread(r, "wheelTimer"); @@ -64,11 +73,16 @@ public TimerDispatcher() { }, 1, TimeUnit.SECONDS, 512); this.currentCyclicTaskMap = new ConcurrentHashMap<>(64); this.currentTempTaskMap = new ConcurrentHashMap<>(8); - eventListeners = new ConcurrentHashMap<>(8); + this.eventListeners = new ConcurrentHashMap<>(8); + this.started = new AtomicBoolean(true); } @Override public void addJob(Job addJob, CollectResponseEventListener eventListener) { + if (!this.started.get()) { + log.warn("Collector is offline, can not dispatch collect jobs."); + return; + } WheelTimerTask timerJob = new WheelTimerTask(addJob); if (addJob.isCyclic()) { Timeout timeout = wheelTimer.newTimeout(timerJob, addJob.getInterval(), TimeUnit.SECONDS); @@ -82,6 +96,10 @@ public void addJob(Job addJob, CollectResponseEventListener eventListener) { @Override public void cyclicJob(WheelTimerTask timerTask, long interval, TimeUnit timeUnit) { + if (!this.started.get()) { + log.warn("Collector is offline, can not dispatch collect jobs."); + return; + } Long jobId = timerTask.getJob().getId(); // 判断此周期性job是否已经被取消 if (currentCyclicTaskMap.containsKey(jobId)) { @@ -104,7 +122,26 @@ public void deleteJob(long jobId, boolean isCyclic) { } } } - + + @Override + public void goOnline() { + currentCyclicTaskMap.forEach((key, value) -> value.cancel()); + currentCyclicTaskMap.clear(); + currentTempTaskMap.forEach((key, value) -> value.cancel()); + currentTempTaskMap.clear(); + started.set(true); + } + + @Override + public void goOffline() { + started.set(false); + currentCyclicTaskMap.forEach((key, value) -> value.cancel()); + currentCyclicTaskMap.clear(); + currentTempTaskMap.forEach((key, value) -> value.cancel()); + currentTempTaskMap.clear(); + } + + @Override public void responseSyncJobData(long jobId, List metricsDataTemps) { currentTempTaskMap.remove(jobId); @@ -113,4 +150,9 @@ public void responseSyncJobData(long jobId, List metrics eventListener.response(metricsDataTemps); } } + + @Override + public void destroy() throws Exception { + this.wheelTimer.stop(); + } } diff --git a/collector/src/main/resources/application.yml b/collector/src/main/resources/application.yml index 71524ae4111..860e982d280 100644 --- a/collector/src/main/resources/application.yml +++ b/collector/src/main/resources/application.yml @@ -14,11 +14,14 @@ # limitations under the License. server: port: 1159 + shutdown: graceful spring: application: name: ${HOSTNAME:@hertzbeat-collector@}${PID} profiles: active: cluster + lifecycle: + timeout-per-shutdown-phase: 10s jackson: default-property-inclusion: ALWAYS # need to disable spring boot mongodb auto config, or default mongodb connection tried and failed... diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/dto/Message.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/dto/Message.java index 7cc9d68fa77..ed8eb9543ea 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/entity/dto/Message.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/dto/Message.java @@ -71,6 +71,10 @@ public static Message success(T data) { return new Message<>(data); } + public static Message successWithData(T data) { + return new Message<>(data); + } + private Message() { } diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/message/ClusterMsg.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/message/ClusterMsg.java index 8d549108371..cba4fca2988 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/entity/message/ClusterMsg.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/message/ClusterMsg.java @@ -87,6 +87,14 @@ public enum MessageType * RESPONSE_CYCLIC_TASK_DATA = 7; */ RESPONSE_CYCLIC_TASK_DATA(7), + /** + *
+         * collector go close to master
+         * 
+ * + * GO_CLOSE = 8; + */ + GO_CLOSE(8), UNRECOGNIZED(-1), ; @@ -154,6 +162,14 @@ public enum MessageType * RESPONSE_CYCLIC_TASK_DATA = 7; */ public static final int RESPONSE_CYCLIC_TASK_DATA_VALUE = 7; + /** + *
+         * collector go close to master
+         * 
+ * + * GO_CLOSE = 8; + */ + public static final int GO_CLOSE_VALUE = 8; public final int getNumber() { @@ -196,6 +212,8 @@ public static MessageType forNumber(int value) { return RESPONSE_ONE_TIME_TASK_DATA; case 7: return RESPONSE_CYCLIC_TASK_DATA; + case 8: + return GO_CLOSE; default: return null; } @@ -256,6 +274,136 @@ private MessageType(int value) { // @@protoc_insertion_point(enum_scope:org.dromara.hertzbeat.common.entity.message.MessageType) } + /** + * Protobuf enum {@code org.dromara.hertzbeat.common.entity.message.Direction} + */ + public enum Direction + implements com.google.protobuf.ProtocolMessageEnum { + /** + *
+         * request message
+         * 
+ * + * REQUEST = 0; + */ + REQUEST(0), + /** + *
+         * request response
+         * 
+ * + * RESPONSE = 1; + */ + RESPONSE(1), + UNRECOGNIZED(-1), + ; + + /** + *
+         * request message
+         * 
+ * + * REQUEST = 0; + */ + public static final int REQUEST_VALUE = 0; + /** + *
+         * request response
+         * 
+ * + * RESPONSE = 1; + */ + public static final int RESPONSE_VALUE = 1; + + + public final int getNumber() { + if (this == UNRECOGNIZED) { + throw new IllegalArgumentException( + "Can't get the number of an unknown enum value."); + } + return value; + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @Deprecated + public static Direction valueOf(int value) { + return forNumber(value); + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + */ + public static Direction forNumber(int value) { + switch (value) { + case 0: + return REQUEST; + case 1: + return RESPONSE; + default: + return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + + private static final com.google.protobuf.Internal.EnumLiteMap< + Direction> internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public Direction findValueByNumber(int number) { + return Direction.forNumber(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + if (this == UNRECOGNIZED) { + throw new IllegalStateException( + "Can't get the descriptor of an unrecognized enum value."); + } + return getDescriptor().getValues().get(ordinal()); + } + + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return ClusterMsg.getDescriptor().getEnumTypes().get(1); + } + + private static final Direction[] VALUES = values(); + + public static Direction valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + if (desc.getIndex() == -1) { + return UNRECOGNIZED; + } + return VALUES[desc.getIndex()]; + } + + private final int value; + + private Direction(int value) { + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:org.dromara.hertzbeat.common.entity.message.Direction) + } + public interface MessageOrBuilder extends // @@protoc_insertion_point(interface_extends:org.dromara.hertzbeat.common.entity.message.Message) com.google.protobuf.MessageOrBuilder { @@ -283,12 +431,34 @@ public interface MessageOrBuilder extends com.google.protobuf.ByteString getIdentityBytes(); + /** + *
+         * message direction
+         * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The enum numeric value on the wire for direction. + */ + int getDirectionValue(); + + /** + *
+         * message direction
+         * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The direction. + */ + Direction getDirection(); + /** *
          * message type
          * 
* - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The enum numeric value on the wire for type. */ @@ -299,7 +469,7 @@ public interface MessageOrBuilder extends * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The type. */ @@ -310,7 +480,7 @@ public interface MessageOrBuilder extends * message content * * - * string msg = 3; + * string msg = 4; * * @return The msg. */ @@ -321,7 +491,7 @@ public interface MessageOrBuilder extends * message content * * - * string msg = 3; + * string msg = 4; * * @return The bytes for msg. */ @@ -345,6 +515,7 @@ private Message(com.google.protobuf.GeneratedMessageV3.Builder builder) { private Message() { identity_ = ""; + direction_ = 0; type_ = 0; msg_ = ""; } @@ -389,10 +560,16 @@ private Message( case 16: { int rawValue = input.readEnum(); + direction_ = rawValue; + break; + } + case 24: { + int rawValue = input.readEnum(); + type_ = rawValue; break; } - case 26: { + case 34: { String s = input.readStringRequireUtf8(); msg_ = s; @@ -483,7 +660,40 @@ public String getIdentity() { } } - public static final int TYPE_FIELD_NUMBER = 2; + public static final int DIRECTION_FIELD_NUMBER = 2; + private int direction_; + + /** + *
+         * message direction
+         * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The enum numeric value on the wire for direction. + */ + @Override + public int getDirectionValue() { + return direction_; + } + + /** + *
+         * message direction
+         * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The direction. + */ + @Override + public Direction getDirection() { + @SuppressWarnings("deprecation") + Direction result = Direction.valueOf(direction_); + return result == null ? Direction.UNRECOGNIZED : result; + } + + public static final int TYPE_FIELD_NUMBER = 3; private int type_; /** @@ -491,7 +701,7 @@ public String getIdentity() { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The enum numeric value on the wire for type. */ @@ -505,7 +715,7 @@ public int getTypeValue() { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The type. */ @@ -516,7 +726,7 @@ public MessageType getType() { return result == null ? MessageType.UNRECOGNIZED : result; } - public static final int MSG_FIELD_NUMBER = 3; + public static final int MSG_FIELD_NUMBER = 4; private volatile Object msg_; /** @@ -524,7 +734,7 @@ public MessageType getType() { * message content * * - * string msg = 3; + * string msg = 4; * * @return The msg. */ @@ -547,7 +757,7 @@ public String getMsg() { * message content * * - * string msg = 3; + * string msg = 4; * * @return The bytes for msg. */ @@ -584,11 +794,14 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(identity_)) { com.google.protobuf.GeneratedMessageV3.writeString(output, 1, identity_); } + if (direction_ != Direction.REQUEST.getNumber()) { + output.writeEnum(2, direction_); + } if (type_ != MessageType.HEARTBEAT.getNumber()) { - output.writeEnum(2, type_); + output.writeEnum(3, type_); } if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(msg_)) { - com.google.protobuf.GeneratedMessageV3.writeString(output, 3, msg_); + com.google.protobuf.GeneratedMessageV3.writeString(output, 4, msg_); } unknownFields.writeTo(output); } @@ -602,12 +815,16 @@ public int getSerializedSize() { if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(identity_)) { size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, identity_); } + if (direction_ != Direction.REQUEST.getNumber()) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(2, direction_); + } if (type_ != MessageType.HEARTBEAT.getNumber()) { size += com.google.protobuf.CodedOutputStream - .computeEnumSize(2, type_); + .computeEnumSize(3, type_); } if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(msg_)) { - size += com.google.protobuf.GeneratedMessageV3.computeStringSize(3, msg_); + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(4, msg_); } size += unknownFields.getSerializedSize(); memoizedSize = size; @@ -626,6 +843,7 @@ public boolean equals(final Object obj) { if (!getIdentity() .equals(other.getIdentity())) return false; + if (direction_ != other.direction_) return false; if (type_ != other.type_) return false; if (!getMsg() .equals(other.getMsg())) return false; @@ -642,6 +860,8 @@ public int hashCode() { hash = (19 * hash) + getDescriptor().hashCode(); hash = (37 * hash) + IDENTITY_FIELD_NUMBER; hash = (53 * hash) + getIdentity().hashCode(); + hash = (37 * hash) + DIRECTION_FIELD_NUMBER; + hash = (53 * hash) + direction_; hash = (37 * hash) + TYPE_FIELD_NUMBER; hash = (53 * hash) + type_; hash = (37 * hash) + MSG_FIELD_NUMBER; @@ -800,6 +1020,8 @@ public Builder clear() { super.clear(); identity_ = ""; + direction_ = 0; + type_ = 0; msg_ = ""; @@ -831,6 +1053,7 @@ public Message build() { public Message buildPartial() { Message result = new Message(this); result.identity_ = identity_; + result.direction_ = direction_; result.type_ = type_; result.msg_ = msg_; onBuilt(); @@ -891,6 +1114,9 @@ public Builder mergeFrom(Message other) { identity_ = other.identity_; onChanged(); } + if (other.direction_ != 0) { + setDirectionValue(other.getDirectionValue()); + } if (other.type_ != 0) { setTypeValue(other.getTypeValue()); } @@ -1033,6 +1259,91 @@ public Builder setIdentityBytes( return this; } + private int direction_ = 0; + + /** + *
+             * message direction
+             * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The enum numeric value on the wire for direction. + */ + @Override + public int getDirectionValue() { + return direction_; + } + + /** + *
+             * message direction
+             * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @param value The enum numeric value on the wire for direction to set. + * @return This builder for chaining. + */ + public Builder setDirectionValue(int value) { + + direction_ = value; + onChanged(); + return this; + } + + /** + *
+             * message direction
+             * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return The direction. + */ + @Override + public Direction getDirection() { + @SuppressWarnings("deprecation") + Direction result = Direction.valueOf(direction_); + return result == null ? Direction.UNRECOGNIZED : result; + } + + /** + *
+             * message direction
+             * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @param value The direction to set. + * @return This builder for chaining. + */ + public Builder setDirection(Direction value) { + if (value == null) { + throw new NullPointerException(); + } + + direction_ = value.getNumber(); + onChanged(); + return this; + } + + /** + *
+             * message direction
+             * 
+ * + * .org.dromara.hertzbeat.common.entity.message.Direction direction = 2; + * + * @return This builder for chaining. + */ + public Builder clearDirection() { + + direction_ = 0; + onChanged(); + return this; + } + private int type_ = 0; /** @@ -1040,7 +1351,7 @@ public Builder setIdentityBytes( * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The enum numeric value on the wire for type. */ @@ -1054,7 +1365,7 @@ public int getTypeValue() { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @param value The enum numeric value on the wire for type to set. * @return This builder for chaining. @@ -1071,7 +1382,7 @@ public Builder setTypeValue(int value) { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return The type. */ @@ -1087,7 +1398,7 @@ public MessageType getType() { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @param value The type to set. * @return This builder for chaining. @@ -1107,7 +1418,7 @@ public Builder setType(MessageType value) { * message type * * - * .org.dromara.hertzbeat.common.entity.message.MessageType type = 2; + * .org.dromara.hertzbeat.common.entity.message.MessageType type = 3; * * @return This builder for chaining. */ @@ -1125,7 +1436,7 @@ public Builder clearType() { * message content * * - * string msg = 3; + * string msg = 4; * * @return The msg. */ @@ -1147,7 +1458,7 @@ public String getMsg() { * message content * * - * string msg = 3; + * string msg = 4; * * @return The bytes for msg. */ @@ -1170,7 +1481,7 @@ public String getMsg() { * message content * * - * string msg = 3; + * string msg = 4; * * @param value The msg to set. * @return This builder for chaining. @@ -1191,7 +1502,7 @@ public Builder setMsg( * message content * * - * string msg = 3; + * string msg = 4; * * @return This builder for chaining. */ @@ -1207,7 +1518,7 @@ public Builder clearMsg() { * message content * * - * string msg = 3; + * string msg = 4; * * @param value The bytes for msg to set. * @return This builder for chaining. @@ -1295,15 +1606,18 @@ public Message getDefaultInstanceForType() { static { String[] descriptorData = { "\n\021cluster_msg.proto\022+org.dromara.hertzbe" + - "at.common.entity.message\"p\n\007Message\022\020\n\010i" + - "dentity\030\001 \001(\t\022F\n\004type\030\002 \001(\01628.org.dromar" + - "a.hertzbeat.common.entity.message.Messag" + - "eType\022\013\n\003msg\030\003 \001(\t*\303\001\n\013MessageType\022\r\n\tHE" + - "ARTBEAT\020\000\022\r\n\tGO_ONLINE\020\001\022\016\n\nGO_OFFLINE\020\002" + - "\022\025\n\021ISSUE_CYCLIC_TASK\020\003\022\026\n\022DELETE_CYCLIC" + - "_TASK\020\004\022\027\n\023ISSUE_ONE_TIME_TASK\020\005\022\037\n\033RESP" + - "ONSE_ONE_TIME_TASK_DATA\020\006\022\035\n\031RESPONSE_CY" + - "CLIC_TASK_DATA\020\007b\006proto3" + "at.common.entity.message\"\273\001\n\007Message\022\020\n\010" + + "identity\030\001 \001(\t\022I\n\tdirection\030\002 \001(\01626.org." + + "dromara.hertzbeat.common.entity.message." + + "Direction\022F\n\004type\030\003 \001(\01628.org.dromara.he" + + "rtzbeat.common.entity.message.MessageTyp" + + "e\022\013\n\003msg\030\004 \001(\t*\321\001\n\013MessageType\022\r\n\tHEARTB" + + "EAT\020\000\022\r\n\tGO_ONLINE\020\001\022\016\n\nGO_OFFLINE\020\002\022\025\n\021" + + "ISSUE_CYCLIC_TASK\020\003\022\026\n\022DELETE_CYCLIC_TAS" + + "K\020\004\022\027\n\023ISSUE_ONE_TIME_TASK\020\005\022\037\n\033RESPONSE" + + "_ONE_TIME_TASK_DATA\020\006\022\035\n\031RESPONSE_CYCLIC" + + "_TASK_DATA\020\007\022\014\n\010GO_CLOSE\020\010*&\n\tDirection\022" + + "\013\n\007REQUEST\020\000\022\014\n\010RESPONSE\020\001b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -1314,7 +1628,7 @@ public Message getDefaultInstanceForType() { internal_static_org_dromara_hertzbeat_common_entity_message_Message_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( internal_static_org_dromara_hertzbeat_common_entity_message_Message_descriptor, - new String[]{"Identity", "Type", "Msg",}); + new String[]{"Identity", "Direction", "Type", "Msg",}); } // @@protoc_insertion_point(outer_class_scope) diff --git a/common/src/main/java/org/dromara/hertzbeat/common/support/SpringContextHolder.java b/common/src/main/java/org/dromara/hertzbeat/common/support/SpringContextHolder.java index 1ce0eb2df26..5a82c091eab 100644 --- a/common/src/main/java/org/dromara/hertzbeat/common/support/SpringContextHolder.java +++ b/common/src/main/java/org/dromara/hertzbeat/common/support/SpringContextHolder.java @@ -20,6 +20,7 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Component; /** @@ -31,10 +32,15 @@ public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; + + private static ConfigurableApplicationContext configurableApplicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { set(applicationContext); + if (applicationContext instanceof ConfigurableApplicationContext) { + configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; + } } private static void set(ApplicationContext applicationContext) { @@ -56,10 +62,22 @@ public static T getBean(Class tClass) { assertApplicationContext(); return (T) applicationContext.getBean(tClass); } + + public static void shutdown() { + assertApplicationContext(); + configurableApplicationContext.close(); + } + + public static boolean isActive() { + if (configurableApplicationContext == null) { + return false; + } + return configurableApplicationContext.isActive(); + } private static void assertApplicationContext() { - if (null == SpringContextHolder.applicationContext) { - throw new RuntimeException("applicationContext为空,请检查是否注入springContextHolder"); + if (null == applicationContext || null == configurableApplicationContext) { + throw new RuntimeException("applicationContext is null, please inject the springContextHolder"); } } } diff --git a/common/src/main/message/cluster_msg.proto b/common/src/main/message/cluster_msg.proto index fd1e291ee12..af200b033b0 100644 --- a/common/src/main/message/cluster_msg.proto +++ b/common/src/main/message/cluster_msg.proto @@ -22,10 +22,12 @@ message Message { // collector identity string identity = 1; + // message direction + Direction direction = 2; // message type - MessageType type = 2; + MessageType type = 3; // message content - string msg = 3; + string msg = 4; } enum MessageType @@ -46,4 +48,13 @@ enum MessageType RESPONSE_ONE_TIME_TASK_DATA = 6; // response cyclic collect data RESPONSE_CYCLIC_TASK_DATA = 7; + // collector go close to master + GO_CLOSE = 8; +} + +enum Direction { + // request message + REQUEST = 0; + // request response + RESPONSE = 1; } diff --git a/manager/pom.xml b/manager/pom.xml index cfcfb736ae7..bf0a6be8840 100644 --- a/manager/pom.xml +++ b/manager/pom.xml @@ -60,6 +60,11 @@ org.dromara.hertzbeat hertzbeat-collector
+ + + org.dromara.hertzbeat + hertzbeat-remoting + org.springframework.boot diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java b/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java index 581d0d7c520..4c54e037e0e 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java @@ -23,7 +23,6 @@ import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.scheduling.annotation.EnableScheduling; import javax.annotation.PostConstruct; @@ -34,7 +33,6 @@ @SpringBootApplication(exclude = MailSenderAutoConfiguration.class) @EnableJpaAuditing -@EnableScheduling @EnableJpaRepositories(basePackages = {"org.dromara.hertzbeat"}) @EntityScan(basePackages = {"org.dromara.hertzbeat"}) public class Manager { diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/controller/AppController.java b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/AppController.java index 51315be174f..191611c1057 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/controller/AppController.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/AppController.java @@ -74,7 +74,7 @@ public ResponseEntity> queryAppDefine( public ResponseEntity> queryAppDefineYml( @Parameter(description = "en: Monitoring type name,zh: 监控类型名称", example = "api") @PathVariable("app") final String app) { String defineContent = appService.getMonitorDefineFileContent(app); - return ResponseEntity.ok(Message.success(defineContent)); + return ResponseEntity.ok(Message.successWithData(defineContent)); } @DeleteMapping(path = "/{app}/define/yml") @@ -95,9 +95,7 @@ public ResponseEntity> newAppDefineYml(@Valid @RequestBody Monitor try { for (String riskyToken : RISKY_STR_ARR) { if (defineDto.getDefine().contains(riskyToken)) { - return ResponseEntity.ok(Message.builder() - .code(CommonConstants.FAIL_CODE) - .msg("can not has malicious remote script").build()); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "can not has malicious remote script")); } } appService.applyMonitorDefineYml(defineDto.getDefine(), false); @@ -113,9 +111,7 @@ public ResponseEntity> updateAppDefineYml(@Valid @RequestBody Moni try { for (String riskyToken : RISKY_STR_ARR) { if (defineDto.getDefine().contains(riskyToken)) { - return ResponseEntity.ok(Message.builder() - .code(CommonConstants.FAIL_CODE) - .msg("can not has malicious remote script").build()); + return ResponseEntity.ok(Message.fail(FAIL_CODE, "can not has malicious remote script")); } } appService.applyMonitorDefineYml(defineDto.getDefine(), true); diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/controller/CollectorController.java b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/CollectorController.java index af462f44cf5..2099eda11a6 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/controller/CollectorController.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/CollectorController.java @@ -23,19 +23,24 @@ import org.dromara.hertzbeat.common.entity.dto.CollectorSummary; import org.dromara.hertzbeat.common.entity.dto.Message; import org.dromara.hertzbeat.common.entity.manager.Collector; +import org.dromara.hertzbeat.manager.netty.ManageServer; import org.dromara.hertzbeat.manager.service.CollectorService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.persistence.criteria.Predicate; +import java.util.List; + import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; /** @@ -49,6 +54,9 @@ public class CollectorController { @Autowired private CollectorService collectorService; + + @Autowired(required = false) + private ManageServer manageServer; @GetMapping @Operation(summary = "Get a list of collectors based on query filter items", @@ -73,4 +81,37 @@ public ResponseEntity>> getCollectors( Message> message = Message.success(receivers); return ResponseEntity.ok(message); } + + @PutMapping("/online") + @Operation(summary = "Online collectors") + public ResponseEntity> onlineCollector( + @Parameter(description = "collector name", example = "demo-collector") + @RequestParam(required = false) List collectors) { + if (collectors != null) { + collectors.forEach(collector -> + this.manageServer.getCollectorAndJobScheduler().onlineCollector(collector)); + } + return ResponseEntity.ok(Message.success("Online success")); + } + + @PutMapping("/offline") + @Operation(summary = "Offline collectors") + public ResponseEntity> offlineCollector( + @Parameter(description = "collector name", example = "demo-collector") + @RequestParam(required = false) List collectors) { + if (collectors != null) { + collectors.forEach(collector -> this.manageServer.getCollectorAndJobScheduler().offlineCollector(collector)); + } + return ResponseEntity.ok(Message.success("Offline success")); + } + + @DeleteMapping + @Operation(summary = "Delete collectors") + public ResponseEntity> deleteCollector( + @Parameter(description = "collector name | 采集器名称", example = "demo-collector") + @RequestParam(required = false) List collectors) { + this.collectorService.deleteRegisteredCollector(collectors); + return ResponseEntity.ok(Message.success("Delete success")); + } + } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/dao/CollectorDao.java b/manager/src/main/java/org/dromara/hertzbeat/manager/dao/CollectorDao.java index 5a95c067c1a..157cf62e021 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/dao/CollectorDao.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/dao/CollectorDao.java @@ -20,6 +20,7 @@ import org.dromara.hertzbeat.common.entity.manager.Collector; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; import java.util.Optional; @@ -38,4 +39,11 @@ public interface CollectorDao extends JpaRepository, JpaSpecifi * @return collector */ Optional findCollectorByName(String name); + + /** + * delete collector by name + * @param collector collector name + */ + @Modifying + void deleteCollectorByName(String collector); } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/ManageServer.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/ManageServer.java new file mode 100644 index 00000000000..da734bcffa7 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/ManageServer.java @@ -0,0 +1,165 @@ +package org.dromara.hertzbeat.manager.netty; + +import com.google.common.collect.Lists; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.CommonThreadPool; +import org.dromara.hertzbeat.manager.netty.process.CollectCyclicDataResponseProcessor; +import org.dromara.hertzbeat.manager.netty.process.CollectOneTimeDataResponseProcessor; +import org.dromara.hertzbeat.manager.netty.process.CollectorOfflineProcessor; +import org.dromara.hertzbeat.manager.netty.process.CollectorOnlineProcessor; +import org.dromara.hertzbeat.manager.netty.process.HeartbeatProcessor; +import org.dromara.hertzbeat.manager.scheduler.CollectorAndJobScheduler; +import org.dromara.hertzbeat.manager.scheduler.SchedulerProperties; +import org.dromara.hertzbeat.remoting.RemotingServer; +import org.dromara.hertzbeat.remoting.event.NettyEventListener; +import org.dromara.hertzbeat.remoting.netty.NettyHook; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingServer; +import org.dromara.hertzbeat.remoting.netty.NettyServerConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * manage server + */ +@Component +@ConditionalOnProperty(prefix = "scheduler.server", + name = "enabled", havingValue = "true") +@Slf4j +public class ManageServer { + + private final CollectorAndJobScheduler collectorAndJobScheduler; + + private ScheduledExecutorService channelSchedule; + + private RemotingServer remotingServer; + + private final Map clientChannelTable = new ConcurrentHashMap<>(16); + + public ManageServer(final SchedulerProperties schedulerProperties, + final CollectorAndJobScheduler collectorAndJobScheduler, + final CommonThreadPool threadPool) { + this.collectorAndJobScheduler = collectorAndJobScheduler; + this.collectorAndJobScheduler.setManageServer(this); + + this.init(schedulerProperties, threadPool); + this.start(); + } + + private void init(final SchedulerProperties schedulerProperties, final CommonThreadPool threadPool) { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setPort(schedulerProperties.getServer().getPort()); + NettyEventListener nettyEventListener = new ManageNettyEventListener(); + this.remotingServer = new NettyRemotingServer(nettyServerConfig, nettyEventListener, threadPool); + + // register hook + this.remotingServer.registerHook(Lists.newArrayList(new NettyHook() { + @Override + public void doBeforeRequest(ChannelHandlerContext ctx, ClusterMsg.Message message) { + ManageServer.this.clientChannelTable.put(message.getIdentity(), ctx.channel()); + } + })); + + // register processor + this.remotingServer.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new CollectorOnlineProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new CollectorOfflineProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA, new CollectOneTimeDataResponseProcessor(this)); + this.remotingServer.registerProcessor(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA, new CollectCyclicDataResponseProcessor()); + + this.channelSchedule = Executors.newSingleThreadScheduledExecutor(); + } + + public void start() { + this.remotingServer.start(); + + this.channelSchedule.scheduleAtFixedRate(() -> { + ManageServer.this.clientChannelTable.forEach((collector, channel) -> { + if (!channel.isActive()) { + channel.closeFuture(); + ManageServer.this.clientChannelTable.remove(collector); + ManageServer.this.collectorAndJobScheduler.collectorGoOffline(collector); + } + }); + }, 10, 3, TimeUnit.SECONDS); + } + + public void shutdown() { + this.remotingServer.shutdown(); + + this.channelSchedule.shutdownNow(); + } + + public CollectorAndJobScheduler getCollectorAndJobScheduler() { + return collectorAndJobScheduler; + } + + public Channel getChannel(final String identity) { + Channel channel = this.clientChannelTable.get(identity); + if (channel == null || !channel.isActive()) { + this.clientChannelTable.remove(identity); + log.error("client {} offline now", identity); + } + return channel; + } + + public void closeChannel(final String identity) { + Channel channel = this.getChannel(identity); + if (channel != null) { + this.collectorAndJobScheduler.collectorGoOffline(identity); + ClusterMsg.Message message = ClusterMsg.Message.newBuilder().setType(ClusterMsg.MessageType.GO_CLOSE).build(); + this.remotingServer.sendMsg(channel, message); + this.clientChannelTable.remove(identity); + log.info("close collect client success, identity: {}", identity); + } + } + + public boolean isChannelExist(final String identity) { + Channel channel = this.clientChannelTable.get(identity); + return channel != null && channel.isActive(); + } + + public boolean sendMsg(final String identityId, final ClusterMsg.Message message) { + Channel channel = this.getChannel(identityId); + if (channel != null) { + this.remotingServer.sendMsg(channel, message); + return true; + } + return false; + } + + public ClusterMsg.Message sendMsgSync(final String identityId, final ClusterMsg.Message message) { + Channel channel = this.getChannel(identityId); + if (channel != null) { + return this.remotingServer.sendMsgSync(channel, message, 3000); + } + return null; + } + + public class ManageNettyEventListener implements NettyEventListener { + + @Override + public void onChannelIdle(Channel channel) { + String identity = null; + for (Map.Entry entry : ManageServer.this.clientChannelTable.entrySet()) { + if (entry.getValue().equals(channel)) { + identity = entry.getKey(); + break; + } + } + if (identity != null) { + ManageServer.this.clientChannelTable.remove(identity); + ManageServer.this.collectorAndJobScheduler.collectorGoOffline(identity); + log.info("handle idle event triggered. the client {} is going offline.", identity); + } + } + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectCyclicDataResponseProcessor.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectCyclicDataResponseProcessor.java new file mode 100644 index 00000000000..a3def41673c --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectCyclicDataResponseProcessor.java @@ -0,0 +1,32 @@ +package org.dromara.hertzbeat.manager.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.entity.message.CollectRep; +import org.dromara.hertzbeat.common.queue.CommonDataQueue; +import org.dromara.hertzbeat.common.queue.impl.KafkaCommonDataQueue; +import org.dromara.hertzbeat.common.support.SpringContextHolder; +import org.dromara.hertzbeat.common.util.ProtoJsonUtil; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle cyclic data response message + */ +@Slf4j +public class CollectCyclicDataResponseProcessor implements NettyRemotingProcessor { + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + CommonDataQueue dataQueue = SpringContextHolder.getBean(CommonDataQueue.class); + if (dataQueue instanceof KafkaCommonDataQueue) { + log.error("netty receiver collector response collect data, but common data queue is kafka, please enable inMemory data queue."); + return null; + } + CollectRep.MetricsData metricsData = (CollectRep.MetricsData) ProtoJsonUtil.toProtobuf(message.getMsg(), + CollectRep.MetricsData.newBuilder()); + if (metricsData != null) { + dataQueue.sendMetricsData(metricsData); + } + return null; + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectOneTimeDataResponseProcessor.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectOneTimeDataResponseProcessor.java new file mode 100644 index 00000000000..ff0a7a4377f --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectOneTimeDataResponseProcessor.java @@ -0,0 +1,48 @@ +package org.dromara.hertzbeat.manager.netty.process; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.entity.message.CollectRep; +import org.dromara.hertzbeat.common.util.JsonUtil; +import org.dromara.hertzbeat.common.util.ProtoJsonUtil; +import org.dromara.hertzbeat.manager.netty.ManageServer; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +import java.util.ArrayList; +import java.util.List; + +/** + * handle one-time collect data response message + */ +@Slf4j +public class CollectOneTimeDataResponseProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public CollectOneTimeDataResponseProcessor(ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + TypeReference> typeReference = new TypeReference<>() { + }; + List jsonArr = JsonUtil.fromJson(message.getMsg(), typeReference); + if (jsonArr == null) { + log.error("netty receive response one time task data parse null error"); + return null; + } + List metricsDataList = new ArrayList<>(jsonArr.size()); + for (String str : jsonArr) { + CollectRep.MetricsData metricsData = (CollectRep.MetricsData) ProtoJsonUtil.toProtobuf(str, + CollectRep.MetricsData.newBuilder()); + if (metricsData != null) { + metricsDataList.add(metricsData); + } + } + this.manageServer.getCollectorAndJobScheduler().collectSyncJobResponse(metricsDataList); + return null; + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOfflineProcessor.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOfflineProcessor.java new file mode 100644 index 00000000000..3028059c6a1 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOfflineProcessor.java @@ -0,0 +1,28 @@ +package org.dromara.hertzbeat.manager.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.manager.netty.ManageServer; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector offline message + */ +@Slf4j +public class CollectorOfflineProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public CollectorOfflineProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String collector = message.getIdentity(); + log.info("the collector {} actively requests to go offline.", collector); + this.manageServer.getCollectorAndJobScheduler().collectorGoOffline(collector); + return null; + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOnlineProcessor.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOnlineProcessor.java new file mode 100644 index 00000000000..99f71596b1a --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/CollectorOnlineProcessor.java @@ -0,0 +1,30 @@ +package org.dromara.hertzbeat.manager.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.dto.CollectorInfo; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.util.JsonUtil; +import org.dromara.hertzbeat.manager.netty.ManageServer; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle collector online message + */ +@Slf4j +public class CollectorOnlineProcessor implements NettyRemotingProcessor { + private final ManageServer manageServer; + + public CollectorOnlineProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String collector = message.getIdentity(); + log.info("the collector {} actively requests to go online.", collector); + CollectorInfo collectorInfo = JsonUtil.fromJson(message.getMsg(), CollectorInfo.class); + this.manageServer.getCollectorAndJobScheduler().collectorGoOnline(collector, collectorInfo); + return null; + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/HeartbeatProcessor.java b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/HeartbeatProcessor.java new file mode 100644 index 00000000000..714d26b6a40 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/netty/process/HeartbeatProcessor.java @@ -0,0 +1,36 @@ +package org.dromara.hertzbeat.manager.netty.process; + +import io.netty.channel.ChannelHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.manager.netty.ManageServer; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * handle heartbeat message + */ +@Slf4j +public class HeartbeatProcessor implements NettyRemotingProcessor { + + private final ManageServer manageServer; + + public HeartbeatProcessor(final ManageServer manageServer) { + this.manageServer = manageServer; + } + + @Override + public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) { + String identity = message.getIdentity(); + boolean isChannelExist = this.manageServer.isChannelExist(identity); + if (!isChannelExist) { + log.info("the collector {} has reconnected and to go online.", identity); + this.manageServer.getCollectorAndJobScheduler().collectorGoOnline(identity); + } + if (log.isDebugEnabled()) { + log.debug("server receive collector heartbeat"); + } + return ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.HEARTBEAT) + .build(); + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/AssignJobs.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/AssignJobs.java index e2898653f26..773731a95e2 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/AssignJobs.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/AssignJobs.java @@ -142,6 +142,5 @@ public void clear() { log.warn("assign jobs is not empty, maybe there are jobs not assigned"); jobs.clear(); } - jobs = null; } } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorAndJobScheduler.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorAndJobScheduler.java index 0f638d026f1..744e5f82d39 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorAndJobScheduler.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorAndJobScheduler.java @@ -1,6 +1,5 @@ package org.dromara.hertzbeat.manager.scheduler; -import io.netty.channel.Channel; import lombok.extern.slf4j.Slf4j; import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectJobService; import org.dromara.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener; @@ -21,9 +20,9 @@ import org.dromara.hertzbeat.manager.dao.CollectorMonitorBindDao; import org.dromara.hertzbeat.manager.dao.MonitorDao; import org.dromara.hertzbeat.manager.dao.ParamDao; +import org.dromara.hertzbeat.manager.netty.ManageServer; import org.dromara.hertzbeat.manager.service.AppService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -37,47 +36,46 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** - * collector service + * collector service + * * */ @Component @AutoConfigureAfter(value = {SchedulerProperties.class}) @Slf4j -public class CollectorAndJobScheduler implements CollectorScheduling, CollectJobScheduling, CommandLineRunner { - - private final Map collectorChannelMap = new ConcurrentHashMap<>(16); - +public class CollectorAndJobScheduler implements CollectorScheduling, CollectJobScheduling { + private final Map jobContentCache = new ConcurrentHashMap<>(16); - + private final Map eventListeners = new ConcurrentHashMap<>(16); - + @Autowired private CollectorDao collectorDao; - + @Autowired private CollectorMonitorBindDao collectorMonitorBindDao; - + @Autowired - private ConsistentHash consistentHash; - + private ConsistentHash consistentHash; + @Autowired private CollectJobService collectJobService; - + @Autowired private AppService appService; - + @Autowired private MonitorDao monitorDao; - + @Autowired private ParamDao paramDao; + private ManageServer manageServer; + @Override public void collectorGoOnline(String identity) { Optional collectorOptional = collectorDao.findCollectorByName(identity); @@ -103,7 +101,7 @@ public void collectorGoOnline(String identity, CollectorInfo collectorInfo) { collector.setIp(collectorInfo.getIp()); } else { collector = Collector.builder().name(identity).ip(collectorInfo.getIp()) - .status(CommonConstants.COLLECTOR_STATUS_ONLINE).build(); + .status(CommonConstants.COLLECTOR_STATUS_ONLINE).build(); } collectorDao.save(collector); ConsistentHash.Node node = new ConsistentHash.Node(identity, collectorInfo.getIp(), System.currentTimeMillis(), null); @@ -124,11 +122,11 @@ public void collectorGoOnline(String identity, CollectorInfo collectorInfo) { appDefine.setTimestamp(System.currentTimeMillis()); List params = paramDao.findParamsByMonitorId(monitor.getId()); List configmaps = params.stream() - .map(param -> new Configmap(param.getField(), param.getValue(), - param.getType())).collect(Collectors.toList()); + .map(param -> new Configmap(param.getField(), param.getValue(), + param.getType())).collect(Collectors.toList()); List paramDefaultValue = appDefine.getParams().stream() - .filter(item -> StringUtils.hasText(item.getDefaultValue())) - .collect(Collectors.toList()); + .filter(item -> StringUtils.hasText(item.getDefaultValue())) + .collect(Collectors.toList()); paramDefaultValue.forEach(defaultVar -> { if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) { // todo type @@ -146,7 +144,7 @@ public void collectorGoOnline(String identity, CollectorInfo collectorInfo) { } } } - + @Override public void collectorGoOffline(String identity) { Optional collectorOptional = collectorDao.findCollectorByName(identity); @@ -159,7 +157,7 @@ public void collectorGoOffline(String identity) { reBalanceCollectorAssignJobs(); } } - + @Override public void reBalanceCollectorAssignJobs() { consistentHash.getAllNodes().entrySet().parallelStream().forEach(entry -> { @@ -175,6 +173,7 @@ public void reBalanceCollectorAssignJobs() { log.error("assigning job {} content is null.", addingJobId); continue; } + addedJobIds.add(addingJobId); collectJobService.addAsyncCollectJob(job); } assignJobs.addAssignJobs(addedJobIds); @@ -185,50 +184,68 @@ public void reBalanceCollectorAssignJobs() { assignJobs.clearRemovingJobs(); } } else { - Channel channel = collectorChannelMap.get(collectorName); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(collectorName); - log.error("channel: {} offline now, can not assign jobs.", collectorName); - } else { - if (!assignJobs.getAddingJobs().isEmpty()) { - Set addedJobIds = new HashSet<>(8); - for (Long addingJobId : assignJobs.getAddingJobs()) { - Job job = jobContentCache.get(addingJobId); - if (job == null) { - log.error("assigning job {} content is null.", addingJobId); - continue; - } - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(job)) - .build(); - channel.writeAndFlush(message); + if (!assignJobs.getAddingJobs().isEmpty()) { + Set addedJobIds = new HashSet<>(8); + for (Long addingJobId : assignJobs.getAddingJobs()) { + Job job = jobContentCache.get(addingJobId); + if (job == null) { + log.error("assigning job {} content is null.", addingJobId); + continue; } - assignJobs.addAssignJobs(addedJobIds); - assignJobs.removeAddingJobs(addedJobIds); - } - if (!assignJobs.getRemovingJobs().isEmpty()) { + addedJobIds.add(addingJobId); ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(assignJobs.getRemovingJobs())) - .build(); - channel.writeAndFlush(message); - assignJobs.clearRemovingJobs(); + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setMsg(JsonUtil.toJson(job)) + .build(); + this.manageServer.sendMsg(collectorName, message); } - } + assignJobs.addAssignJobs(addedJobIds); + assignJobs.removeAddingJobs(addedJobIds); + } + if (!assignJobs.getRemovingJobs().isEmpty()) { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setMsg(JsonUtil.toJson(assignJobs.getRemovingJobs())) + .build(); + this.manageServer.sendMsg(collectorName, message); + assignJobs.clearRemovingJobs(); + } } } }); } - + @Override - public void holdCollectorChannel(String identity, Channel channel) { - this.collectorChannelMap.put(identity, channel); + public boolean offlineCollector(String identity) { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_OFFLINE) + .setDirection(ClusterMsg.Direction.REQUEST) + .setIdentity(identity) + .build(); + ClusterMsg.Message response = this.manageServer.sendMsgSync(identity, message); + if (response == null || !String.valueOf(CommonConstants.SUCCESS_CODE).equals(response.getMsg())) { + return false; + } + log.info("send offline collector message to {} success", identity); + this.collectorGoOffline(identity); + return true; } @Override - public boolean isCollectorChannelExist(String identity) { - return this.collectorChannelMap.get(identity) != null; + public boolean onlineCollector(String identity) { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.GO_ONLINE) + .setDirection(ClusterMsg.Direction.REQUEST) + .setIdentity(identity) + .build(); + ClusterMsg.Message response = this.manageServer.sendMsgSync(identity, message); + if (response == null || !String.valueOf(CommonConstants.SUCCESS_CODE).equals(response.getMsg())) { + return false; + } + log.info("send online collector message to {} success", identity); + this.collectorGoOnline(identity); + return true; } @Override @@ -239,22 +256,25 @@ public List collectSyncJobData(Job job) { if (node == null) { log.error("there is no collector online to assign job."); CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() - .setCode(CollectRep.Code.FAIL) - .setMsg("no collector online to assign job") - .build(); + .setCode(CollectRep.Code.FAIL) + .setMsg("no collector online to assign job") + .build(); return Collections.singletonList(metricsData); } if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { return collectJobService.collectSyncJobData(job); } else { List metricsData = new LinkedList<>(); - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job {}.", node.getName(), job.getId()); - return metricsData; - } else { - CountDownLatch countDownLatch = new CountDownLatch(1); + CountDownLatch countDownLatch = new CountDownLatch(1); + + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(job)) + .build(); + boolean result = this.manageServer.sendMsg(node.getName(), message); + + if (result) { CollectResponseEventListener listener = new CollectResponseEventListener() { @Override public void response(List responseMetrics) { @@ -264,43 +284,39 @@ public void response(List responseMetrics) { countDownLatch.countDown(); } }; - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) - .setMsg(JsonUtil.toJson(job)) - .build(); - channel.writeAndFlush(message); eventListeners.put(job.getMonitorId(), listener); - try { - countDownLatch.await(120, TimeUnit.SECONDS); - } catch (Exception e) { - log.info("The sync task runs for 120 seconds with no response and returns"); - } - return metricsData; } + try { + countDownLatch.await(120, TimeUnit.SECONDS); + } catch (Exception e) { + log.info("The sync task runs for 120 seconds with no response and returns"); + } + return metricsData; } } - + @Override public List collectSyncJobData(Job job, String collector) { ConsistentHash.Node node = consistentHash.getNode(collector); if (node == null) { log.error("there is no collector online to assign job."); CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder() - .setCode(CollectRep.Code.FAIL) - .setMsg("the collector is offline and cannot assign job") - .build(); + .setCode(CollectRep.Code.FAIL) + .setMsg("the collector is offline and cannot assign job") + .build(); return Collections.singletonList(metricsData); } if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { return collectJobService.collectSyncJobData(job); } else { List metricsData = new LinkedList<>(); - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job {}.", node.getName(), job.getId()); - return metricsData; - } else { + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(job)) + .build(); + boolean result = this.manageServer.sendMsg(node.getName(), message); + if (result) { CountDownLatch countDownLatch = new CountDownLatch(1); CollectResponseEventListener listener = new CollectResponseEventListener() { @Override @@ -311,22 +327,17 @@ public void response(List responseMetrics) { countDownLatch.countDown(); } }; - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK) - .setMsg(JsonUtil.toJson(job)) - .build(); - channel.writeAndFlush(message); eventListeners.put(job.getMonitorId(), listener); try { countDownLatch.await(120, TimeUnit.SECONDS); } catch (Exception e) { log.info("The sync task runs for 120 seconds with no response and returns"); } - return metricsData; } + return metricsData; } } - + @Override public long addAsyncCollectJob(Job job) { long jobId = SnowFlakeIdGenerator.generateId(); @@ -342,21 +353,16 @@ public long addAsyncCollectJob(Job job) { if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { collectJobService.addAsyncCollectJob(job); } else { - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job monitor {}.", node.getName(), jobId); - } else { - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(job)) - .build(); - channel.writeAndFlush(message); - } + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(job)) + .build(); + this.manageServer.sendMsg(node.getName(), message); } return jobId; } - + @Override public long addAsyncCollectJob(Job job, String collector) { long jobId = SnowFlakeIdGenerator.generateId(); @@ -371,21 +377,16 @@ public long addAsyncCollectJob(Job job, String collector) { if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { collectJobService.addAsyncCollectJob(job); } else { - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job {}.", node.getName(), jobId); - } else { - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(job)) - .build(); - channel.writeAndFlush(message); - } + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(job)) + .build(); + this.manageServer.sendMsg(node.getName(), message); } return jobId; } - + @Override public long updateAsyncCollectJob(Job modifyJob) { // delete and add @@ -405,26 +406,22 @@ public long updateAsyncCollectJob(Job modifyJob) { collectJobService.addAsyncCollectJob(modifyJob); collectJobService.cancelAsyncCollectJob(preJobId); } else { - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job {}.", node.getName(), newJobId); - } else { - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(modifyJob)) - .build(); - channel.writeAndFlush(message); - ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(List.of(preJobId))) - .build(); - channel.writeAndFlush(deleteMessage); - } + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(modifyJob)) + .build(); + this.manageServer.sendMsg(node.getName(), message); + ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(List.of(preJobId))) + .build(); + this.manageServer.sendMsg(node.getName(), deleteMessage); } return newJobId; } - + @Override public long updateAsyncCollectJob(Job modifyJob, String collector) { // delete and add @@ -440,58 +437,49 @@ public long updateAsyncCollectJob(Job modifyJob, String collector) { } node.getAssignJobs().removePinnedJob(preJobId); node.getAssignJobs().addPinnedJob(newJobId); - + if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { collectJobService.addAsyncCollectJob(modifyJob); collectJobService.cancelAsyncCollectJob(preJobId); } else { - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not assign job {}.", node.getName(), newJobId); - } else { - ClusterMsg.Message message = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(modifyJob)) - .build(); - channel.writeAndFlush(message); - ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(List.of(preJobId))) - .build(); - channel.writeAndFlush(deleteMessage); - } + ClusterMsg.Message message = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(modifyJob)) + .build(); + this.manageServer.sendMsg(node.getName(), message); + ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(List.of(preJobId))) + .build(); + this.manageServer.sendMsg(node.getName(), deleteMessage); } return newJobId; } - + @Override public void cancelAsyncCollectJob(Long jobId) { - consistentHash.getAllNodes().entrySet().stream().filter( entry -> { + consistentHash.getAllNodes().entrySet().stream().filter(entry -> { ConsistentHash.Node node = entry.getValue(); AssignJobs assignJobs = node.getAssignJobs(); return assignJobs.getPinnedJobs().contains(jobId) - || assignJobs.getJobs().contains(jobId) || assignJobs.getAddingJobs().contains(jobId); + || assignJobs.getJobs().contains(jobId) || assignJobs.getAddingJobs().contains(jobId); }).findFirst().ifPresent(entry -> { ConsistentHash.Node node = entry.getValue(); if (CommonConstants.MAIN_COLLECTOR_NODE.equals(node.getName())) { collectJobService.cancelAsyncCollectJob(jobId); } else { - Channel channel = collectorChannelMap.get(node.getName()); - if (channel == null || !channel.isActive()) { - collectorChannelMap.remove(node.getName()); - log.error("channel: {} offline now, can not cancel job {}.", node.getName(), jobId); - } else { - ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() - .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) - .setMsg(JsonUtil.toJson(List.of(jobId))) - .build(); - channel.writeAndFlush(deleteMessage); - } + ClusterMsg.Message deleteMessage = ClusterMsg.Message.newBuilder() + .setType(ClusterMsg.MessageType.DELETE_CYCLIC_TASK) + .setDirection(ClusterMsg.Direction.REQUEST) + .setMsg(JsonUtil.toJson(List.of(jobId))) + .build(); + this.manageServer.sendMsg(node.getName(), deleteMessage); } }); } - + @Override public void collectSyncJobResponse(List metricsDataList) { if (metricsDataList.isEmpty()) { @@ -504,19 +492,9 @@ public void collectSyncJobResponse(List metricsDataList) eventListener.response(metricsDataList); } } - - @Override - public void run(String... args) throws Exception { - // start detect not active channel client - ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); - scheduledExecutor.scheduleAtFixedRate(() -> { - collectorChannelMap.forEach((collector, channel) -> { - if (!channel.isActive()) { - channel.closeFuture(); - collectorChannelMap.remove(collector); - collectorGoOffline(collector); - } - }); - }, 10, 3, TimeUnit.SECONDS); + + public void setManageServer(ManageServer manageServer) { + this.manageServer = manageServer; } + } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorScheduling.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorScheduling.java index 1b7cc10e13a..31667ba2cd8 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorScheduling.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/CollectorScheduling.java @@ -17,7 +17,6 @@ package org.dromara.hertzbeat.manager.scheduler; -import io.netty.channel.Channel; import org.dromara.hertzbeat.common.entity.dto.CollectorInfo; /** @@ -51,16 +50,16 @@ public interface CollectorScheduling { void reBalanceCollectorAssignJobs(); /** - * hold and update collector message channel map + * offline collector(stop collector collect operation) * @param identity collector identity name - * @param channel message channel + * @return true/false */ - void holdCollectorChannel(String identity, Channel channel); + boolean offlineCollector(String identity); /** - * judge collector message channel exist + * online collector(start collector collect operation) * @param identity collector identity name * @return true/false */ - boolean isCollectorChannelExist(String identity); + boolean onlineCollector(String identity); } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ConsistentHash.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ConsistentHash.java index 651d26b5711..3755fee5a17 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ConsistentHash.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ConsistentHash.java @@ -92,7 +92,7 @@ public void addNode(Node newNode) { Iterator iterator = dispatchJobCache.iterator(); while (iterator.hasNext()) { DispatchJob dispatchJob = iterator.next(); - dispatchJob(dispatchJob.dispatchHash, dispatchJob.jobId); + dispatchJob(dispatchJob.dispatchHash, dispatchJob.jobId, false); iterator.remove(); } } @@ -148,7 +148,7 @@ public Node removeNode(String name) { Iterator iterator = dispatchJobCache.iterator(); while (iterator.hasNext()) { DispatchJob dispatchJob = iterator.next(); - dispatchJob(dispatchJob.dispatchHash, dispatchJob.jobId); + dispatchJob(dispatchJob.dispatchHash, dispatchJob.jobId, false); iterator.remove(); } } @@ -193,7 +193,7 @@ public Node dispatchJob(String dispatchKey, Long jobId) { return null; } int dispatchHash = hash(dispatchKey); - return dispatchJob(dispatchHash, jobId); + return dispatchJob(dispatchHash, jobId, true); } /** @@ -216,9 +216,10 @@ public Node preDispatchJob(String dispatchKey) { * * @param dispatchHash 采集任务路由hash * @param jobId jobId + * @param isFlushed is has flush this job or wait to dispatch 此任务是否已被下发调度还是等待后续下发 * @return 采集器节点 */ - public Node dispatchJob(Integer dispatchHash, Long jobId) { + public Node dispatchJob(Integer dispatchHash, Long jobId, boolean isFlushed) { if (dispatchHash == null || hashCircle == null || hashCircle.isEmpty()) { log.warn("There is no available collector registered. Cache the job."); dispatchJobCache.add(new DispatchJob(dispatchHash, jobId)); @@ -231,7 +232,7 @@ public Node dispatchJob(Integer dispatchHash, Long jobId) { int virtualKey = ceilEntry.getKey(); Node curNode = ceilEntry.getValue(); - curNode.addJob(virtualKey, dispatchHash, jobId); + curNode.addJob(virtualKey, dispatchHash, jobId, isFlushed); return curNode; } @@ -345,7 +346,7 @@ public Node(String name, String ip, long uptime, Byte quality) { virtualNodeMap = new ConcurrentHashMap<>(VIRTUAL_NODE_DEFAULT_SIZE); } - private synchronized void addJob(Integer virtualNodeKey, Integer dispatchHash, Long jobId) { + private synchronized void addJob(Integer virtualNodeKey, Integer dispatchHash, Long jobId, boolean isFlushed) { if (virtualNodeMap == null) { virtualNodeMap = new ConcurrentHashMap<>(VIRTUAL_NODE_DEFAULT_SIZE); } @@ -354,7 +355,11 @@ private synchronized void addJob(Integer virtualNodeKey, Integer dispatchHash, L } Set virtualNodeJob = virtualNodeMap.computeIfAbsent(virtualNodeKey, k -> new HashSet<>(16)); virtualNodeJob.add(new Long[]{jobId, dispatchHash.longValue()}); - assignJobs.addAssignJob(jobId); + if (isFlushed) { + assignJobs.addAssignJob(jobId); + } else { + assignJobs.addAddingJob(jobId); + } } /** diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ManageServer.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ManageServer.java deleted file mode 100644 index 04ba05c8461..00000000000 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ManageServer.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.dromara.hertzbeat.manager.scheduler; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.epoll.Epoll; -import io.netty.channel.epoll.EpollEventLoopGroup; -import io.netty.channel.epoll.EpollServerSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hertzbeat.common.support.CommonThreadPool; -import org.dromara.hertzbeat.common.util.NetworkUtil; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -/** - * manage server for cluster - * - */ -@Component -@ConditionalOnProperty(prefix = "scheduler.server", - name = "enabled", havingValue = "true") -@Slf4j -public class ManageServer { - - - private final CollectorScheduling collectorScheduling; - - private final CollectJobScheduling collectJobScheduling; - - private final CommonThreadPool commonThreadPool; - - public ManageServer(SchedulerProperties schedulerProperties, CollectorScheduling collectorScheduling, - CollectJobScheduling collectJobScheduling, CommonThreadPool threadPool) throws Exception { - if (schedulerProperties == null || schedulerProperties.getServer() == null) { - log.error("init error, please config scheduler server props in application.yml"); - throw new IllegalArgumentException("please config scheduler server props"); - } - this.collectorScheduling = collectorScheduling; - this.collectJobScheduling = collectJobScheduling; - this.commonThreadPool = threadPool; - serverStartup(schedulerProperties); - } - - public void serverStartup(SchedulerProperties properties) { - commonThreadPool.execute(() -> { - Thread.currentThread().setName("cluster netty server"); - int port = properties.getServer().getPort(); - EventLoopGroup bossGroup; - EventLoopGroup workerGroup; - - if (this.useEpoll()) { - bossGroup = new EpollEventLoopGroup(1); - workerGroup = new EpollEventLoopGroup(); - } else { - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); - } - - try { - ServerBootstrap b = new ServerBootstrap(); - b.group(bossGroup, workerGroup) - .channel(this.useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(new ProtoServerInitializer(collectorScheduling, collectJobScheduling)); - b.bind(port).sync().channel().closeFuture().sync(); - } catch (Exception e) { - log.error(e.getMessage()); - throw new RuntimeException(e); - } finally { - bossGroup.shutdownGracefully(); - workerGroup.shutdownGracefully(); - } - }); - } - - private boolean useEpoll() { - return NetworkUtil.isLinuxPlatform() - && Epoll.isAvailable(); - } -} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ProtoServerInitializer.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ProtoServerInitializer.java deleted file mode 100644 index c4178ccfd2c..00000000000 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ProtoServerInitializer.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.dromara.hertzbeat.manager.scheduler; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.compression.ZlibCodecFactory; -import io.netty.handler.codec.compression.ZlibWrapper; -import io.netty.handler.codec.protobuf.ProtobufDecoder; -import io.netty.handler.codec.protobuf.ProtobufEncoder; -import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; -import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; -import io.netty.handler.timeout.IdleStateHandler; -import org.dromara.hertzbeat.common.entity.message.ClusterMsg; - -/** - * netty server initializer - * - */ -public class ProtoServerInitializer extends ChannelInitializer { - - private final CollectorScheduling collectorScheduling; - - private final CollectJobScheduling collectJobScheduling; - - public ProtoServerInitializer(CollectorScheduling collectorScheduling, CollectJobScheduling collectJobScheduling) { - super(); - this.collectorScheduling = collectorScheduling; - this.collectJobScheduling = collectJobScheduling; - } - - @Override - protected void initChannel(SocketChannel socketChannel) throws Exception { - ChannelPipeline pipeline = socketChannel.pipeline(); - // zip - pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); - pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); - // protocol buf encode decode - pipeline.addLast(new ProtobufVarint32FrameDecoder()); - pipeline.addLast(new ProtobufDecoder(ClusterMsg.Message.getDefaultInstance())); - pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); - pipeline.addLast(new ProtobufEncoder()); - // idle state - pipeline.addLast(new IdleStateHandler(0, 0, 30)); - // message handler - pipeline.addLast(new ServerInboundMessageHandler(collectorScheduling, collectJobScheduling)); - } -} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ServerInboundMessageHandler.java b/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ServerInboundMessageHandler.java deleted file mode 100644 index f747e517927..00000000000 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/scheduler/ServerInboundMessageHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.dromara.hertzbeat.manager.scheduler; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelId; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.timeout.IdleState; -import io.netty.handler.timeout.IdleStateEvent; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hertzbeat.common.entity.dto.CollectorInfo; -import org.dromara.hertzbeat.common.entity.message.ClusterMsg; -import org.dromara.hertzbeat.common.entity.message.CollectRep; -import org.dromara.hertzbeat.common.queue.CommonDataQueue; -import org.dromara.hertzbeat.common.queue.impl.KafkaCommonDataQueue; -import org.dromara.hertzbeat.common.support.SpringContextHolder; -import org.dromara.hertzbeat.common.util.JsonUtil; -import org.dromara.hertzbeat.common.util.ProtoJsonUtil; -import org.springframework.util.StringUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * netty inbound collector message handler - * - * - */ -@Slf4j -public class ServerInboundMessageHandler extends SimpleChannelInboundHandler { - - private final CollectorScheduling collectorScheduling; - - private final CollectJobScheduling collectJobScheduling; - - private final Map channelCollectorMap = new ConcurrentHashMap<>(8); - - public ServerInboundMessageHandler(CollectorScheduling collectorScheduling, CollectJobScheduling collectJobScheduling) { - super(); - this.collectorScheduling = collectorScheduling; - this.collectJobScheduling = collectJobScheduling; - } - - @Override - protected void channelRead0(ChannelHandlerContext channelHandlerContext, ClusterMsg.Message message) throws Exception { - Channel channel = channelHandlerContext.channel(); - String identity = message.getIdentity(); - boolean isCollectorChannelExist = collectorScheduling.isCollectorChannelExist(identity); - channelCollectorMap.put(channel.id(), identity); - collectorScheduling.holdCollectorChannel(identity, channel); - switch (message.getType()) { - case HEARTBEAT: - // 用于处理collector连接断开后重连 - if (!isCollectorChannelExist) { - log.info("the collector {} has reconnected and to go online.", identity); - collectorScheduling.collectorGoOnline(identity); - } - if (log.isDebugEnabled()) { - log.debug("server receive collector heartbeat"); - } - channel.writeAndFlush(ClusterMsg.Message.newBuilder().setType(ClusterMsg.MessageType.HEARTBEAT).build()); - break; - case GO_ONLINE: - log.info("the collector {} actively requests to go online.", identity); - CollectorInfo collectorInfo = JsonUtil.fromJson(message.getMsg(), CollectorInfo.class); - collectorScheduling.collectorGoOnline(identity, collectorInfo); - break; - case GO_OFFLINE: - log.info("the collector {} actively requests to go offline.", identity); - collectorScheduling.collectorGoOffline(identity); - break; - case RESPONSE_ONE_TIME_TASK_DATA: - try { - TypeReference> typeReference = new TypeReference<>() { - }; - List jsonArr = JsonUtil.fromJson(message.getMsg(), typeReference); - if (jsonArr == null) { - log.error("netty receive response one time task data parse null error"); - break; - } - List metricsDataList = new ArrayList<>(jsonArr.size()); - for (String str : jsonArr) { - CollectRep.MetricsData metricsData = (CollectRep.MetricsData) ProtoJsonUtil.toProtobuf(str, - CollectRep.MetricsData.newBuilder()); - if (metricsData != null) { - metricsDataList.add(metricsData); - } - } - collectJobScheduling.collectSyncJobResponse(metricsDataList); - } catch (Exception e) { - log.error("netty receive response one time task data error: {}." , e.getMessage(), e); - } - break; - case RESPONSE_CYCLIC_TASK_DATA: - CommonDataQueue dataQueue = SpringContextHolder.getBean(CommonDataQueue.class); - if (dataQueue instanceof KafkaCommonDataQueue) { - log.error("netty receiver collector response collect data, but common data queue is kafka, please enable inMemory data queue."); - return; - } - CollectRep.MetricsData metricsData = (CollectRep.MetricsData) ProtoJsonUtil.toProtobuf(message.getMsg(), - CollectRep.MetricsData.newBuilder()); - if (metricsData != null) { - dataQueue.sendMetricsData(metricsData); - } - break; - } - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { - IdleStateEvent event = (IdleStateEvent) evt; - if (event.state() == IdleState.ALL_IDLE) { - // collector timeout - ChannelId channelId = ctx.channel().id(); - String collector = channelCollectorMap.get(channelId); - if (StringUtils.hasText(collector)) { - log.info("all idle event triggered. the collector {} is going offline.", collector); - collectorScheduling.collectorGoOffline(collector); - } - ctx.channel().closeFuture(); - } - } -} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/service/CollectorService.java b/manager/src/main/java/org/dromara/hertzbeat/manager/service/CollectorService.java index e5af5bbf0ac..dc86f4bc763 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/service/CollectorService.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/service/CollectorService.java @@ -6,6 +6,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; +import java.util.List; + /** * collector service * @@ -21,4 +23,10 @@ public interface CollectorService { */ Page getCollectors(Specification specification, PageRequest pageRequest); + /** + * delete registered collectors + * @param collectors collector + */ + void deleteRegisteredCollector(List collectors); + } diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/CollectorServiceImpl.java b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/CollectorServiceImpl.java index b1e13a0bf38..d9653ba04bf 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/CollectorServiceImpl.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/CollectorServiceImpl.java @@ -3,6 +3,7 @@ import org.dromara.hertzbeat.common.entity.dto.CollectorSummary; import org.dromara.hertzbeat.common.entity.manager.Collector; import org.dromara.hertzbeat.manager.dao.CollectorDao; +import org.dromara.hertzbeat.manager.netty.ManageServer; import org.dromara.hertzbeat.manager.scheduler.AssignJobs; import org.dromara.hertzbeat.manager.scheduler.ConsistentHash; import org.dromara.hertzbeat.manager.service.CollectorService; @@ -12,6 +13,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.LinkedList; import java.util.List; @@ -21,6 +23,7 @@ * */ @Service +@Transactional(rollbackFor = Exception.class) public class CollectorServiceImpl implements CollectorService { @Autowired @@ -29,7 +32,11 @@ public class CollectorServiceImpl implements CollectorService { @Autowired private ConsistentHash consistentHash; + @Autowired(required = false) + private ManageServer manageServer; + @Override + @Transactional(readOnly = true) public Page getCollectors(Specification specification, PageRequest pageRequest) { Page collectors = collectorDao.findAll(specification, pageRequest); List collectorSummaryList = new LinkedList<>(); @@ -45,4 +52,16 @@ public Page getCollectors(Specification specificati } return new PageImpl<>(collectorSummaryList, pageRequest, collectors.getTotalElements()); } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRegisteredCollector(List collectors) { + if (collectors == null || collectors.isEmpty()) { + return; + } + collectors.forEach(collector -> { + this.manageServer.closeChannel(collector); + this.collectorDao.deleteCollectorByName(collector); + }); + } } diff --git a/manager/src/main/resources/application-test.yml b/manager/src/main/resources/application-test.yml index af12e27f894..1427913cb87 100644 --- a/manager/src/main/resources/application-test.yml +++ b/manager/src/main/resources/application-test.yml @@ -68,3 +68,8 @@ warehouse: memory: enabled: true init-size: 1024 + +scheduler: + server: + enabled: true + port: 1158 diff --git a/pom.xml b/pom.xml index 9f7600b5659..dc30ea3dfac 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ common collector warehouse + remoting @@ -94,6 +95,12 @@ hertzbeat-collector ${hertzbeat.version} + + + org.dromara.hertzbeat + hertzbeat-remoting + ${hertzbeat.version} + @@ -324,12 +331,12 @@ false - rulesets/java/ali-comment.xml + rulesets/java/ali-constant.xml - rulesets/java/ali-naming.xml + rulesets/java/ali-oop.xml rulesets/java/ali-orm.xml diff --git a/remoting/README.md b/remoting/README.md new file mode 100644 index 00000000000..c2c59fa1226 --- /dev/null +++ b/remoting/README.md @@ -0,0 +1,12 @@ +### Remoting + +使用 netty 框架实现的 server client 交互,分布式场景使用,主要用于 Manager 和 Collector 交互。 +The server client interaction implemented using the netty framework is mainly used for the interaction between Manager and Collector in a distributed scenario. + + +**实现参考 [RocketMQ](https://github.com/apache/rocketmq) 的 [remoting 模块](https://github.com/apache/rocketmq/tree/develop/remoting)** +**Implementation reference [remoting module](https://github.com/apache/rocketmq/tree/develop/remoting) of [RocketMQ](https://github.com/apache/rocketmq)** + +**Thanks to RocketMQ, Netty for the open source code.** + + diff --git a/remoting/pom.xml b/remoting/pom.xml new file mode 100644 index 00000000000..8ca8016affc --- /dev/null +++ b/remoting/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + org.dromara.hertzbeat + hertzbeat + 1.0 + + + hertzbeat-remoting + ${project.artifactId} + + + + io.netty + netty-all + 4.1.86.Final + + + + org.dromara.hertzbeat + hertzbeat-common + + + + diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingClient.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingClient.java new file mode 100644 index 00000000000..79560f135de --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingClient.java @@ -0,0 +1,49 @@ +/* + * 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.dromara.hertzbeat.remoting; + +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +/** + * remoting client interface + */ +public interface RemotingClient extends RemotingService { + + /** + * register remoting processor + * 根据不同的type注册不同的processor + * @param messageType type + * @param processor remoting processor + */ + void registerProcessor(final ClusterMsg.MessageType messageType, final NettyRemotingProcessor processor); + + /** + * send message to server + * @param request request message + */ + void sendMsg(final ClusterMsg.Message request); + + /** + * send message to server and receive server message + * @param request request message + * @param timeoutMillis timeout millis + * @return response message + */ + ClusterMsg.Message sendMsgSync(final ClusterMsg.Message request, final int timeoutMillis); +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingServer.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingServer.java new file mode 100644 index 00000000000..ec7fee4f9e7 --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingServer.java @@ -0,0 +1,57 @@ +/* + * 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.dromara.hertzbeat.remoting; + +import io.netty.channel.Channel; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.remoting.netty.NettyHook; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingProcessor; + +import java.util.List; + +/** + * remoting server interface + */ +public interface RemotingServer extends RemotingService { + + /** + * register remoting processor + * 根据不同的type注册不同的processor + * @param messageType type + * @param processor remoting processor + */ + void registerProcessor(final ClusterMsg.MessageType messageType, final NettyRemotingProcessor processor); + + /** + * send message to client + * @param channel client channel + * @param request request message + */ + void sendMsg(final Channel channel, final ClusterMsg.Message request); + + /** + * send message to client and receive client message + * @param channel client channel + * @param request request message + * @param timeoutMillis timeout millis + * @return response message + */ + ClusterMsg.Message sendMsgSync(final Channel channel, final ClusterMsg.Message request, final int timeoutMillis); + + void registerHook(List nettyHookList); +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingService.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingService.java new file mode 100644 index 00000000000..9836e3ab4ed --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/RemotingService.java @@ -0,0 +1,35 @@ +/* + * 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.dromara.hertzbeat.remoting; + +/** + * remoting service interface + */ +public interface RemotingService { + + /** + * start server + */ + void start(); + + /** + * shutdown server + */ + void shutdown(); + +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/event/NettyEventListener.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/event/NettyEventListener.java new file mode 100644 index 00000000000..42693d36215 --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/event/NettyEventListener.java @@ -0,0 +1,32 @@ +/* + * 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.dromara.hertzbeat.remoting.event; + +import io.netty.channel.Channel; + +/** + * listen NettyEvent, then handle something + */ +public interface NettyEventListener { + + default void onChannelActive(final Channel channel) { + } + + default void onChannelIdle(final Channel channel) { + } +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyClientConfig.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyClientConfig.java new file mode 100644 index 00000000000..2d4a12b3abb --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyClientConfig.java @@ -0,0 +1,33 @@ +/* + * 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.dromara.hertzbeat.remoting.netty; + +import lombok.Data; + +/** + * netty client config + */ +@Data +public class NettyClientConfig { + + private String serverIp; + + private int serverPort; + + private int connectTimeoutMillis = 10000; +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyHook.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyHook.java new file mode 100644 index 00000000000..cf9d52e2800 --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyHook.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hertzbeat.remoting.netty; + +import io.netty.channel.ChannelHandlerContext; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; + +/** + * hook interface, handle something before request processor + */ +public interface NettyHook { + + void doBeforeRequest(ChannelHandlerContext ctx, ClusterMsg.Message message); + +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingAbstract.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingAbstract.java new file mode 100644 index 00000000000..30941efe4cf --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingAbstract.java @@ -0,0 +1,149 @@ +/* + * 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.dromara.hertzbeat.remoting.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.epoll.Epoll; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.util.NetworkUtil; +import org.dromara.hertzbeat.remoting.RemotingService; +import org.dromara.hertzbeat.remoting.event.NettyEventListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * netty remote abstract + * 参考: org.apache.rocketmq.remoting.netty.NettyRemotingAbstract + */ +@Slf4j +public abstract class NettyRemotingAbstract implements RemotingService { + protected ConcurrentHashMap processorTable = new ConcurrentHashMap<>(); + + protected ConcurrentHashMap responseTable = new ConcurrentHashMap<>(); + + protected List nettyHookList = new ArrayList<>(); + + protected NettyEventListener nettyEventListener; + + protected NettyRemotingAbstract(NettyEventListener nettyEventListener) { + this.nettyEventListener = nettyEventListener; + } + + public void registerProcessor(final ClusterMsg.MessageType messageType, final NettyRemotingProcessor processor) { + this.processorTable.put(messageType, processor); + } + + protected void processReceiveMsg(ChannelHandlerContext ctx, ClusterMsg.Message message) { + if (ClusterMsg.Direction.REQUEST.equals(message.getDirection())) { + this.processRequestMsg(ctx, message); + } else { + this.processResponseMsg(message); + } + } + + protected void processRequestMsg(ChannelHandlerContext ctx, ClusterMsg.Message request) { + this.doBeforeRequest(ctx, request); + + NettyRemotingProcessor processor = this.processorTable.get(request.getType()); + if (processor == null) { + log.info("request type {} not supported", request.getType()); + return; + } + ClusterMsg.Message response = processor.handle(ctx, request); + if (response != null) { + ctx.writeAndFlush(response); + } + } + + private void doBeforeRequest(ChannelHandlerContext ctx, ClusterMsg.Message request) { + if (this.nettyHookList == null || this.nettyHookList.isEmpty()) { + return; + } + for (NettyHook nettyHook : this.nettyHookList) { + nettyHook.doBeforeRequest(ctx, request); + } + } + + protected void processResponseMsg(ClusterMsg.Message response) { + if (this.responseTable.containsKey(response.getIdentity())) { + ResponseFuture responseFuture = this.responseTable.get(response.getIdentity()); + responseFuture.putResponse(response); + } else { + log.warn("receive response not in responseTable, identity: {}", response.getIdentity()); + } + } + + protected void sendMsgImpl(final Channel channel, final ClusterMsg.Message request) { + channel.writeAndFlush(request).addListener(future -> { + if (!future.isSuccess()) { + log.warn("send request message failed. address: {}, ", channel.remoteAddress(), future.cause()); + } + }); + } + + protected ClusterMsg.Message sendMsgSyncImpl(final Channel channel, final ClusterMsg.Message request, final int timeoutMillis) { + final String identity = request.getIdentity(); + + try { + ResponseFuture responseFuture = new ResponseFuture(); + this.responseTable.put(identity, responseFuture); + channel.writeAndFlush(request).addListener(future -> { + if (!future.isSuccess()) { + responseTable.remove(identity); + log.warn("send request message failed. request: {}, address: {}, ", request, channel.remoteAddress(), future.cause()); + } + }); + ClusterMsg.Message response = responseFuture.waitResponse(timeoutMillis); + if (response == null) { + log.warn("get response message failed, message is null"); + } + return response; + } catch (InterruptedException e) { + log.warn("get response message failed, ", e); + } finally { + responseTable.remove(identity); + } + return null; + } + + protected void channelActive(ChannelHandlerContext ctx) { + if (this.nettyEventListener != null && ctx.channel().isActive()) { + this.nettyEventListener.onChannelActive(ctx.channel()); + } + } + + protected void channelIdle(ChannelHandlerContext ctx, Object evt) { + IdleStateEvent event = (IdleStateEvent) evt; + if (this.nettyEventListener != null && event.state() == IdleState.ALL_IDLE) { + ctx.channel().closeFuture(); + this.nettyEventListener.onChannelIdle(ctx.channel()); + } + } + + protected boolean useEpoll() { + return NetworkUtil.isLinuxPlatform() + && Epoll.isAvailable(); + } + +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingClient.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingClient.java new file mode 100644 index 00000000000..cdce6879f08 --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingClient.java @@ -0,0 +1,174 @@ +/* + * 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.dromara.hertzbeat.remoting.netty; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.protobuf.ProtobufDecoder; +import io.netty.handler.codec.protobuf.ProtobufEncoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.CommonThreadPool; +import org.dromara.hertzbeat.remoting.RemotingClient; +import org.dromara.hertzbeat.remoting.event.NettyEventListener; + +import java.util.concurrent.ThreadFactory; + +/** + * netty client + */ +@Slf4j +public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient { + + private final NettyClientConfig nettyClientConfig; + + private final CommonThreadPool threadPool; + + private final Bootstrap bootstrap = new Bootstrap(); + + private EventLoopGroup workerGroup; + + private Channel channel; + + public NettyRemotingClient(final NettyClientConfig nettyClientConfig, + final NettyEventListener nettyEventListener, + final CommonThreadPool threadPool) { + super(nettyEventListener); + this.nettyClientConfig = nettyClientConfig; + this.threadPool = threadPool; + } + + @Override + public void start() { + this.threadPool.execute(() -> { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("NettyClientWorker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("netty-client-worker-%d") + .build(); + this.workerGroup = new NioEventLoopGroup(threadFactory); + this.bootstrap.group(workerGroup) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.nettyClientConfig.getConnectTimeoutMillis()) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) throws Exception { + NettyRemotingClient.this.initChannel(channel); + } + }); + + this.channel = null; + boolean first = true; + while (!Thread.currentThread().isInterrupted() + && (first || this.channel == null || !this.channel.isActive())) { + first = false; + try { + this.channel = this.bootstrap + .connect(this.nettyClientConfig.getServerIp(), this.nettyClientConfig.getServerPort()) + .sync().channel(); + this.channel.closeFuture().sync(); + } catch (InterruptedException ignored) { + log.info("client shutdown now!"); + Thread.currentThread().interrupt(); + } catch (Exception e2) { + log.error("client connect to server error: {}. try after 10s.", e2.getMessage()); + try { + Thread.sleep(10000); + } catch (InterruptedException ignored) { + } + } + } + workerGroup.shutdownGracefully(); + }); + } + + private void initChannel(final SocketChannel channel) { + ChannelPipeline pipeline = channel.pipeline(); + // zip + pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); + pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + // protocol buf encode decode + pipeline.addLast(new ProtobufVarint32FrameDecoder()); + pipeline.addLast(new ProtobufDecoder(ClusterMsg.Message.getDefaultInstance())); + pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); + pipeline.addLast(new ProtobufEncoder()); + pipeline.addLast(new NettyClientHandler()); + + } + + @Override + public void shutdown() { + try { + if (this.channel != null) { + this.channel.close(); + } + + this.workerGroup.shutdownGracefully(); + + this.threadPool.destroy(); + + } catch (Exception e) { + log.error("netty client shutdown exception, ", e); + } + } + + @Override + public void sendMsg(final ClusterMsg.Message request) { + this.sendMsgImpl(this.channel, request); + } + + @Override + public ClusterMsg.Message sendMsgSync(ClusterMsg.Message request, int timeoutMillis) { + return this.sendMsgSyncImpl(this.channel, request, timeoutMillis); + } + + class NettyClientHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + NettyRemotingClient.this.channelActive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ClusterMsg.Message msg) throws Exception { + NettyRemotingClient.this.processReceiveMsg(ctx, msg); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + NettyRemotingClient.this.channelIdle(ctx, evt); + } + } +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingProcessor.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingProcessor.java new file mode 100644 index 00000000000..b35841d0d2e --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingProcessor.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hertzbeat.remoting.netty; + +import io.netty.channel.ChannelHandlerContext; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; + +/** + * netty remoting processor + */ +public interface NettyRemotingProcessor { + + ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message); + +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingServer.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingServer.java new file mode 100644 index 00000000000..0b54e100cdc --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyRemotingServer.java @@ -0,0 +1,192 @@ +/* + * 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.dromara.hertzbeat.remoting.netty; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.netty.handler.codec.protobuf.ProtobufDecoder; +import io.netty.handler.codec.protobuf.ProtobufEncoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.timeout.IdleStateHandler; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.CommonThreadPool; +import org.dromara.hertzbeat.remoting.RemotingServer; +import org.dromara.hertzbeat.remoting.event.NettyEventListener; + +import java.util.List; +import java.util.concurrent.ThreadFactory; + +/** + * netty server + */ +@Slf4j +public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer { + + private final NettyServerConfig nettyServerConfig; + + private final CommonThreadPool threadPool; + + private EventLoopGroup bossGroup; + + private EventLoopGroup workerGroup; + + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final NettyEventListener nettyEventListener, + final CommonThreadPool threadPool) { + super(nettyEventListener); + this.nettyServerConfig = nettyServerConfig; + this.threadPool = threadPool; + } + + @Override + public void start() { + + this.threadPool.execute(() -> { + int port = this.nettyServerConfig.getPort(); + ThreadFactory bossThreadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("NettyServerBoss has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("netty-server-boss-%d") + .build(); + ThreadFactory workerThreadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("NettyServerWorker has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("netty-server-worker-%d") + .build(); + if (this.useEpoll()) { + bossGroup = new EpollEventLoopGroup(bossThreadFactory); + workerGroup = new EpollEventLoopGroup(workerThreadFactory); + } else { + bossGroup = new NioEventLoopGroup(bossThreadFactory); + workerGroup = new NioEventLoopGroup(workerThreadFactory); + } + + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(this.useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_KEEPALIVE, false) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) throws Exception { + NettyRemotingServer.this.initChannel(channel); + } + }); + b.bind(port).sync().channel().closeFuture().sync(); + } catch (InterruptedException ignored) { + log.info("server shutdown now!"); + } catch (Exception e) { + log.error("Netty Server start exception, {}", e.getMessage()); + throw new RuntimeException(e); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + }); + } + + private void initChannel(final SocketChannel channel) { + ChannelPipeline pipeline = channel.pipeline(); + // zip + pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); + pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); + // protocol buf encode decode + pipeline.addLast(new ProtobufVarint32FrameDecoder()); + pipeline.addLast(new ProtobufDecoder(ClusterMsg.Message.getDefaultInstance())); + pipeline.addLast(new ProtobufVarint32LengthFieldPrepender()); + pipeline.addLast(new ProtobufEncoder()); + // idle state + pipeline.addLast(new IdleStateHandler(0, 0, 30)); + pipeline.addLast(new NettyServerHandler()); + } + + @Override + public void shutdown() { + try { + this.bossGroup.shutdownGracefully(); + + this.workerGroup.shutdownGracefully(); + + this.threadPool.destroy(); + + } catch (Exception e) { + log.error("Netty Server shutdown exception, ", e); + } + } + + @Override + public void sendMsg(final Channel channel, final ClusterMsg.Message request) { + this.sendMsgImpl(channel, request); + } + + @Override + public ClusterMsg.Message sendMsgSync(final Channel channel, final ClusterMsg.Message request, final int timeoutMillis) { + return this.sendMsgSyncImpl(channel, request, timeoutMillis); + } + + @Override + public void registerHook(List nettyHookList) { + this.nettyHookList.addAll(nettyHookList); + } + + @ChannelHandler.Sharable + public class NettyServerHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + NettyRemotingServer.this.channelActive(ctx); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ClusterMsg.Message msg) throws Exception { + NettyRemotingServer.this.processReceiveMsg(ctx, msg); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + NettyRemotingServer.this.channelIdle(ctx, evt); + } + } +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyServerConfig.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyServerConfig.java new file mode 100644 index 00000000000..51c3160297a --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/NettyServerConfig.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hertzbeat.remoting.netty; + +import lombok.Data; + +/** + * netty server config + */ +@Data +public class NettyServerConfig { + + private Integer port; + +} diff --git a/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/ResponseFuture.java b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/ResponseFuture.java new file mode 100644 index 00000000000..4a8bacbf908 --- /dev/null +++ b/remoting/src/main/java/org/dromara/hertzbeat/remoting/netty/ResponseFuture.java @@ -0,0 +1,44 @@ +/* + * 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.dromara.hertzbeat.remoting.netty; + +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * netty response future + */ +public class ResponseFuture { + + private final CountDownLatch countDownLatch = new CountDownLatch(1); + + private ClusterMsg.Message response; + + public ClusterMsg.Message waitResponse(final long timeoutMillis) throws InterruptedException { + this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); + return this.response; + } + + public void putResponse(final ClusterMsg.Message response) { + this.response = response; + this.countDownLatch.countDown(); + } + +} diff --git a/remoting/src/test/java/org/dromara/hertzbeat/remoting/RemotingServiceTest.java b/remoting/src/test/java/org/dromara/hertzbeat/remoting/RemotingServiceTest.java new file mode 100644 index 00000000000..03e50da60dc --- /dev/null +++ b/remoting/src/test/java/org/dromara/hertzbeat/remoting/RemotingServiceTest.java @@ -0,0 +1,120 @@ +package org.dromara.hertzbeat.remoting; + +import org.assertj.core.util.Lists; +import org.dromara.hertzbeat.common.entity.message.ClusterMsg; +import org.dromara.hertzbeat.common.support.CommonThreadPool; +import org.dromara.hertzbeat.remoting.netty.NettyClientConfig; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingClient; +import org.dromara.hertzbeat.remoting.netty.NettyRemotingServer; +import org.dromara.hertzbeat.remoting.netty.NettyServerConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * test NettyRemotingClient and NettyRemotingServer + */ +public class RemotingServiceTest { + + private final CommonThreadPool threadPool = new CommonThreadPool(); + + private RemotingServer remotingServer; + + private RemotingClient remotingClient; + + public RemotingServer createRemotingServer(int port) { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setPort(port); + // todo test NettyEventListener + RemotingServer server = new NettyRemotingServer(nettyServerConfig, null, threadPool); + server.start(); + return server; + } + + public RemotingClient createRemotingClient(int port) { + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyClientConfig.setServerIp("localhost"); + nettyClientConfig.setServerPort(port); + RemotingClient client = new NettyRemotingClient(nettyClientConfig, null, threadPool); + client.start(); + return client; + } + + @BeforeEach + public void setUp() throws InterruptedException { + int port = 10000 + (int) (Math.random() * 10000); + remotingServer = createRemotingServer(port); + Thread.sleep(1000); + remotingClient = createRemotingClient(port); + // todo waiting server and client start, 替换为更优雅的方式 + Thread.sleep(1000); + } + + @AfterEach + public void shutdown() { + this.remotingClient.shutdown(); + this.remotingServer.shutdown(); + } + + @Test + public void testSendMsg() { + final String msg = "hello world"; + + this.remotingServer.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, (ctx, message) -> { + Assertions.assertEquals(msg, message.getMsg()); + return null; + }); + + ClusterMsg.Message request = ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.HEARTBEAT) + .setMsg(msg) + .build(); + this.remotingClient.sendMsg(request); + } + + @Test + public void testSendMsgSync() { + final String requestMsg = "request"; + final String responseMsg = "response"; + + this.remotingServer.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, (ctx, message) -> { + Assertions.assertEquals(requestMsg, message.getMsg()); + return ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.RESPONSE) + .setMsg(responseMsg) + .build(); + }); + + ClusterMsg.Message request = ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.HEARTBEAT) + .setMsg(requestMsg) + .build(); + ClusterMsg.Message response = this.remotingClient.sendMsgSync(request, 3000); + Assertions.assertEquals(responseMsg, response.getMsg()); + } + + @Test + public void testNettyHook() { + this.remotingServer.registerHook(Lists.newArrayList( + (ctx, message) -> { + Assertions.assertEquals("hello world", message.getMsg()); + } + )); + + this.remotingServer.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, (ctx, message) -> + ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.RESPONSE) + .build()); + + ClusterMsg.Message request = ClusterMsg.Message.newBuilder() + .setDirection(ClusterMsg.Direction.REQUEST) + .setType(ClusterMsg.MessageType.HEARTBEAT) + .setMsg("hello world") + .build(); + this.remotingClient.sendMsg(request); + } + +} diff --git a/warehouse/README.md b/warehouse/README.md index 936fc038ba8..579d16d41b0 100644 --- a/warehouse/README.md +++ b/warehouse/README.md @@ -1,4 +1,4 @@ -### warehouse +### Warehouse Data management of collection index results, data placement, query, calculation and statistics. -采集指标结果数据管理,数据落盘,查询,计算统计。 \ No newline at end of file +采集指标结果数据管理,数据落盘,查询,计算统计。 diff --git a/warehouse/src/main/java/org/dromara/hertzbeat/warehouse/store/HistoryJpaDatabaseDataStorage.java b/warehouse/src/main/java/org/dromara/hertzbeat/warehouse/store/HistoryJpaDatabaseDataStorage.java index f2d2f209aa2..23a9cfba16d 100644 --- a/warehouse/src/main/java/org/dromara/hertzbeat/warehouse/store/HistoryJpaDatabaseDataStorage.java +++ b/warehouse/src/main/java/org/dromara/hertzbeat/warehouse/store/HistoryJpaDatabaseDataStorage.java @@ -17,6 +17,7 @@ package org.dromara.hertzbeat.warehouse.store; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.dromara.hertzbeat.common.entity.dto.Value; import org.dromara.hertzbeat.common.entity.message.CollectRep; import org.dromara.hertzbeat.common.entity.warehouse.History; @@ -29,7 +30,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.persistence.criteria.Predicate; @@ -39,6 +39,9 @@ import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; /** @@ -62,41 +65,51 @@ public HistoryJpaDatabaseDataStorage(WarehouseProperties properties, this.jpaProperties = properties.getStore().getJpa(); this.serverAvailable = true; this.historyDao = historyDao; + expiredDataCleaner(); } - - @Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS) + public void expiredDataCleaner() { - log.warn("[jpa-metrics-store]-start running expired data cleaner." + - "Please use time series db instead of jpa for better performance"); - String expireTimeStr = jpaProperties.getExpireTime(); - long expireTime = 0; - try { - if (NumberUtils.isParsable(expireTimeStr)) { - expireTime = NumberUtils.toLong(expireTimeStr); - expireTime = (ZonedDateTime.now().toEpochSecond() + expireTime) * 1000; - } else { - TemporalAmount temporalAmount = TimePeriodUtil.parseTokenTime(expireTimeStr); - ZonedDateTime dateTime = ZonedDateTime.now().minus(temporalAmount); - expireTime = dateTime.toEpochSecond() * 1000; - } - } catch (Exception e) { - log.error("expiredDataCleaner time error: {}. use default expire time to clean: 1h", e.getMessage()); - ZonedDateTime dateTime = ZonedDateTime.now().minus(Duration.ofHours(1)); - expireTime = dateTime.toEpochSecond() * 1000; - } - try { - int rows = historyDao.deleteHistoriesByTimeBefore(expireTime); - log.info("[jpa-metrics-store]-delete {} rows.", rows); - long total = historyDao.count(); - if (total > jpaProperties.getMaxHistoryRecordNum()) { - rows = historyDao.deleteOlderHistoriesRecord(jpaProperties.getMaxHistoryRecordNum() / 2); - log.warn("[jpa-metrics-store]-force delete {} rows due too many. Please use time series db instead of jpa for better performance.", rows); - } - } catch (Exception e) { - log.error("expiredDataCleaner database error: {}.", e.getMessage()); - log.error("try to truncate table hzb_history. Please use time series db instead of jpa for better performance."); - historyDao.truncateTable(); - } + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("Jpa metrics store has uncaughtException."); + log.error(throwable.getMessage(), throwable); }) + .setDaemon(true) + .setNameFormat("jpa-metrics-cleaner-%d") + .build(); + ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); + scheduledExecutor.scheduleAtFixedRate(() -> { + log.warn("[jpa-metrics-store]-start running expired data cleaner." + + "Please use time series db instead of jpa for better performance"); + String expireTimeStr = jpaProperties.getExpireTime(); + long expireTime = 0; + try { + if (NumberUtils.isParsable(expireTimeStr)) { + expireTime = NumberUtils.toLong(expireTimeStr); + expireTime = (ZonedDateTime.now().toEpochSecond() + expireTime) * 1000; + } else { + TemporalAmount temporalAmount = TimePeriodUtil.parseTokenTime(expireTimeStr); + ZonedDateTime dateTime = ZonedDateTime.now().minus(temporalAmount); + expireTime = dateTime.toEpochSecond() * 1000; + } + } catch (Exception e) { + log.error("expiredDataCleaner time error: {}. use default expire time to clean: 1h", e.getMessage()); + ZonedDateTime dateTime = ZonedDateTime.now().minus(Duration.ofHours(1)); + expireTime = dateTime.toEpochSecond() * 1000; + } + try { + int rows = historyDao.deleteHistoriesByTimeBefore(expireTime); + log.info("[jpa-metrics-store]-delete {} rows.", rows); + long total = historyDao.count(); + if (total > jpaProperties.getMaxHistoryRecordNum()) { + rows = historyDao.deleteOlderHistoriesRecord(jpaProperties.getMaxHistoryRecordNum() / 2); + log.warn("[jpa-metrics-store]-force delete {} rows due too many. Please use time series db instead of jpa for better performance.", rows); + } + } catch (Exception e) { + log.error("expiredDataCleaner database error: {}.", e.getMessage()); + log.error("try to truncate table hzb_history. Please use time series db instead of jpa for better performance."); + historyDao.truncateTable(); + } + }, 5, 30, TimeUnit.SECONDS); } @Override diff --git a/web-app/src/app/routes/alert/alert-center/alert-center.component.ts b/web-app/src/app/routes/alert/alert-center/alert-center.component.ts index 07740d25109..4e1ce2b0090 100644 --- a/web-app/src/app/routes/alert/alert-center/alert-center.component.ts +++ b/web-app/src/app/routes/alert/alert-center/alert-center.component.ts @@ -103,11 +103,11 @@ export class AlertCenterComponent implements OnInit { onDeleteAlerts() { if (this.checkedAlertIds == null || this.checkedAlertIds.size === 0) { - this.notifySvc.warning(this.i18nSvc.fanyi('alert.center.notify.no-delete'), ''); + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); return; } this.modal.confirm({ - nzTitle: this.i18nSvc.fanyi('alert.center.confirm.delete-batch'), + nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'), nzOkText: this.i18nSvc.fanyi('common.button.ok'), nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), nzOkDanger: true, diff --git a/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts b/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts index fa3dfc3916b..b9792560198 100644 --- a/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts +++ b/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts @@ -342,6 +342,9 @@ export class MonitorNewComponent implements OnInit { } getNumber(rangeString: string, index: number): number | undefined { + if (rangeString == undefined || rangeString == '' || rangeString.length <= index) { + return undefined; + } const rangeArray = JSON.parse(rangeString); return rangeArray[index]; } diff --git a/web-app/src/app/routes/setting/collector/collector.component.html b/web-app/src/app/routes/setting/collector/collector.component.html new file mode 100644 index 00000000000..ac761a07d8d --- /dev/null +++ b/web-app/src/app/routes/setting/collector/collector.component.html @@ -0,0 +1,144 @@ + + + +
+ + + + + + + + + + +
+ + + + + + {{ 'collector' | i18n }} + {{ 'collector.status' | i18n }} + {{ 'collector.task' | i18n }} + {{ 'collector.pinned' | i18n }} + {{ 'collector.dispatched' | i18n }} + {{ 'collector.ip' | i18n }} + {{ 'collector.start-time' | i18n }} + {{ 'common.edit' | i18n }} + + + + + + {{ data.collector.name }} + + + {{ data.collector.status == 0 ? ('monitor.collector.status.online' | i18n) : ('monitor.collector.status.offline' | i18n) }} + + + + + {{ data.pinMonitorNum + data.dispatchMonitorNum }} + + + + + {{ data.pinMonitorNum }} + + + + + {{ data.dispatchMonitorNum }} + + + {{ data.collector.ip }} + + {{ (data.collector.gmtUpdate | date : 'YYYY-MM-dd HH:mm:ss')?.trim() }} + + + + + + + + + + + {{ 'common.total' | i18n }} {{ total }} diff --git a/web-app/src/app/routes/setting/collector/collector.component.spec.ts b/web-app/src/app/routes/setting/collector/collector.component.spec.ts new file mode 100644 index 00000000000..657e0a17220 --- /dev/null +++ b/web-app/src/app/routes/setting/collector/collector.component.spec.ts @@ -0,0 +1,24 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectorComponent } from './collector.component'; + +describe('CollectorComponent', () => { + let component: CollectorComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [CollectorComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/routes/setting/collector/collector.component.ts b/web-app/src/app/routes/setting/collector/collector.component.ts new file mode 100644 index 00000000000..3366607f0fc --- /dev/null +++ b/web-app/src/app/routes/setting/collector/collector.component.ts @@ -0,0 +1,241 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { I18NService } from '@core'; +import { ALAIN_I18N_TOKEN } from '@delon/theme'; +import { NzModalService } from 'ng-zorro-antd/modal'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { NzTableQueryParams } from 'ng-zorro-antd/table'; + +import { CollectorSummary } from '../../../pojo/CollectorSummary'; +import { CollectorService } from '../../../service/collector.service'; + +@Component({ + selector: 'app-setting-collector', + templateUrl: './collector.component.html' +}) +export class CollectorComponent implements OnInit { + constructor( + private notifySvc: NzNotificationService, + private modal: NzModalService, + private collectorService: CollectorService, + @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService + ) {} + + pageIndex: number = 1; + pageSize: number = 8; + total: number = 0; + collectors!: CollectorSummary[]; + tableLoading: boolean = false; + checkedCollectors = new Set(); + // 搜索过滤相关属性 + search: string | undefined; + + ngOnInit(): void { + this.loadCollectorsTable(); + } + + sync() { + this.loadCollectorsTable(); + } + + loadCollectorsTable() { + this.tableLoading = true; + let collectorsInit$ = this.collectorService.queryCollectors(this.search, this.pageIndex - 1, this.pageSize).subscribe( + message => { + this.tableLoading = false; + this.checkedAll = false; + this.checkedCollectors.clear(); + if (message.code === 0) { + let page = message.data; + this.collectors = page.content; + this.pageIndex = page.number + 1; + this.total = page.totalElements; + } else { + console.warn(message.msg); + } + collectorsInit$.unsubscribe(); + }, + error => { + this.tableLoading = false; + collectorsInit$.unsubscribe(); + console.error(error.msg); + } + ); + } + + onDeleteCollectors() { + if (this.checkedCollectors == null || this.checkedCollectors.size === 0) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); + return; + } + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.deleteCollectors(this.checkedCollectors) + }); + } + + onGoOnlineCollectors() { + if (this.checkedCollectors == null || this.checkedCollectors.size === 0) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); + return; + } + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('collector.confirm.online-batch'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.goOnlineCollectors(this.checkedCollectors) + }); + } + + onGoOfflineCollectors() { + if (this.checkedCollectors == null || this.checkedCollectors.size === 0) { + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); + return; + } + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('collector.confirm.offline-batch'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.goOfflineCollectors(this.checkedCollectors) + }); + } + + onDeleteOneCollector(collector: string) { + let collectors = new Set(); + collectors.add(collector); + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('common.confirm.delete'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.deleteCollectors(collectors) + }); + } + + onGoOnlineOneCollector(collector: string) { + let collectors = new Set(); + collectors.add(collector); + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('collector.confirm.online'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.goOnlineCollectors(collectors) + }); + } + + onGoOfflineOneCollector(collector: string) { + let collectors = new Set(); + collectors.add(collector); + this.modal.confirm({ + nzTitle: this.i18nSvc.fanyi('collector.confirm.offline'), + nzOkText: this.i18nSvc.fanyi('common.button.ok'), + nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), + nzOkDanger: true, + nzOkType: 'primary', + nzClosable: false, + nzOnOk: () => this.goOfflineCollectors(collectors) + }); + } + + deleteCollectors(collectors: Set) { + this.tableLoading = true; + const deleteCollectors$ = this.collectorService.deleteCollector(collectors).subscribe( + message => { + deleteCollectors$.unsubscribe(); + if (message.code === 0) { + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), ''); + this.loadCollectorsTable(); + } else { + this.tableLoading = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), message.msg); + } + }, + error => { + this.tableLoading = false; + deleteCollectors$.unsubscribe(); + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), error.msg); + } + ); + } + + goOnlineCollectors(collectors: Set) { + this.tableLoading = true; + const onlineCollectors$ = this.collectorService.goOnlineCollector(collectors).subscribe( + message => { + onlineCollectors$.unsubscribe(); + if (message.code === 0) { + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.operate-success'), ''); + this.loadCollectorsTable(); + } else { + this.tableLoading = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.operate-fail'), message.msg); + } + }, + error => { + this.tableLoading = false; + onlineCollectors$.unsubscribe(); + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.operate-fail'), error.msg); + } + ); + } + + goOfflineCollectors(collectors: Set) { + this.tableLoading = true; + const offlineCollectors$ = this.collectorService.goOfflineCollector(collectors).subscribe( + message => { + offlineCollectors$.unsubscribe(); + if (message.code === 0) { + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.operate-success'), ''); + this.loadCollectorsTable(); + } else { + this.tableLoading = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.operate-fail'), message.msg); + } + }, + error => { + this.tableLoading = false; + offlineCollectors$.unsubscribe(); + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.operate-fail'), error.msg); + } + ); + } + + // begin: 列表多选分页逻辑 + checkedAll: boolean = false; + onAllChecked(checked: boolean) { + if (checked) { + this.collectors.forEach(collector => this.checkedCollectors.add(collector.collector.name)); + } else { + this.checkedCollectors.clear(); + } + } + onItemChecked(collector: string, checked: boolean) { + if (checked) { + this.checkedCollectors.add(collector); + } else { + this.checkedCollectors.delete(collector); + } + } + onTablePageChange(params: NzTableQueryParams) { + const { pageSize, pageIndex, sort, filter } = params; + this.pageIndex = pageIndex; + this.pageSize = pageSize; + this.loadCollectorsTable(); + } + // end: 列表多选分页逻辑 +} diff --git a/web-app/src/app/routes/setting/setting-routing.module.ts b/web-app/src/app/routes/setting/setting-routing.module.ts index 11fa8eb979d..cfb891f68c5 100644 --- a/web-app/src/app/routes/setting/setting-routing.module.ts +++ b/web-app/src/app/routes/setting/setting-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { CollectorComponent } from './collector/collector.component'; import { DefineComponent } from './define/define.component'; import { MessageServerComponent } from './settings/message-server/message-server.component'; import { SettingsComponent } from './settings/settings.component'; @@ -9,6 +10,7 @@ import { SettingTagsComponent } from './tags/tags.component'; const routes: Routes = [ { path: 'tags', component: SettingTagsComponent }, + { path: 'collector', component: CollectorComponent }, { path: 'define', component: DefineComponent, data: { titleI18n: 'menu.extras.define' } }, { path: 'settings', diff --git a/web-app/src/app/routes/setting/setting.module.ts b/web-app/src/app/routes/setting/setting.module.ts index 8f090b383e5..712aea3e9fd 100644 --- a/web-app/src/app/routes/setting/setting.module.ts +++ b/web-app/src/app/routes/setting/setting.module.ts @@ -11,6 +11,7 @@ import { NzSwitchModule } from 'ng-zorro-antd/switch'; import { NzTagModule } from 'ng-zorro-antd/tag'; import { ColorPickerModule } from 'ngx-color-picker'; +import { CollectorComponent } from './collector/collector.component'; import { DefineComponent } from './define/define.component'; import { SettingRoutingModule } from './setting-routing.module'; import { MessageServerComponent } from './settings/message-server/message-server.component'; @@ -23,7 +24,8 @@ const COMPONENTS: Array> = [ DefineComponent, SettingsComponent, MessageServerComponent, - SystemConfigComponent + SystemConfigComponent, + CollectorComponent ]; @NgModule({ diff --git a/web-app/src/app/routes/setting/tags/tags.component.ts b/web-app/src/app/routes/setting/tags/tags.component.ts index 7e201689366..232e81dfb0a 100644 --- a/web-app/src/app/routes/setting/tags/tags.component.ts +++ b/web-app/src/app/routes/setting/tags/tags.component.ts @@ -66,11 +66,11 @@ export class SettingTagsComponent implements OnInit { onDeleteTags() { if (this.checkedTagIds == null || this.checkedTagIds.size === 0) { - this.notifySvc.warning(this.i18nSvc.fanyi('alert.center.notify.no-delete'), ''); + this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), ''); return; } this.modal.confirm({ - nzTitle: this.i18nSvc.fanyi('alert.center.confirm.delete-batch'), + nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'), nzOkText: this.i18nSvc.fanyi('common.button.ok'), nzCancelText: this.i18nSvc.fanyi('common.button.cancel'), nzOkDanger: true, diff --git a/web-app/src/app/service/collector.service.ts b/web-app/src/app/service/collector.service.ts index 3820e288926..a45a6e13f61 100644 --- a/web-app/src/app/service/collector.service.ts +++ b/web-app/src/app/service/collector.service.ts @@ -1,8 +1,7 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { Collector } from '../pojo/Collector'; import { CollectorSummary } from '../pojo/CollectorSummary'; import { Message } from '../pojo/Message'; import { Page } from '../pojo/Page'; @@ -18,4 +17,53 @@ export class CollectorService { public getCollectors(): Observable>> { return this.http.get>>(collector_uri); } + + public queryCollectors(search: string | undefined, pageIndex: number, pageSize: number): Observable>> { + pageIndex = pageIndex ? pageIndex : 0; + pageSize = pageSize ? pageSize : 8; + // 注意HttpParams是不可变对象 需要保存set后返回的对象为最新对象 + let httpParams = new HttpParams(); + httpParams = httpParams.appendAll({ + pageIndex: pageIndex, + pageSize: pageSize + }); + if (search != undefined && search != '' && search.trim() != '') { + httpParams = httpParams.append('name', search.trim()); + } + const options = { params: httpParams }; + return this.http.get>>(collector_uri, options); + } + + public goOnlineCollector(collectors: Set): Observable> { + let httpParams = new HttpParams(); + collectors.forEach(collector => { + // 注意HttpParams是不可变对象 需要保存append后返回的对象为最新对象 + // append方法可以叠加同一key, set方法会把key之前的值覆盖只留一个key-value + httpParams = httpParams.append('collectors', collector); + }); + const options = { params: httpParams }; + return this.http.put>(`${collector_uri}/online/`, null, options); + } + + public goOfflineCollector(collectors: Set): Observable> { + let httpParams = new HttpParams(); + collectors.forEach(collector => { + // 注意HttpParams是不可变对象 需要保存append后返回的对象为最新对象 + // append方法可以叠加同一key, set方法会把key之前的值覆盖只留一个key-value + httpParams = httpParams.append('collectors', collector); + }); + const options = { params: httpParams }; + return this.http.put>(`${collector_uri}/offline/`, null, options); + } + + public deleteCollector(collectors: Set): Observable> { + let httpParams = new HttpParams(); + collectors.forEach(collector => { + // 注意HttpParams是不可变对象 需要保存append后返回的对象为最新对象 + // append方法可以叠加同一key, set方法会把key之前的值覆盖只留一个key-value + httpParams = httpParams.append('collectors', collector); + }); + const options = { params: httpParams }; + return this.http.delete>(`${collector_uri}`, options); + } } diff --git a/web-app/src/assets/app-data.json b/web-app/src/assets/app-data.json index 6c5a0d5518b..b12491a1158 100644 --- a/web-app/src/assets/app-data.json +++ b/web-app/src/assets/app-data.json @@ -127,6 +127,12 @@ "group": true, "hideInBreadcrumb": true, "children": [ + { + "text": "collector", + "i18n": "menu.extras.collector", + "icon": "anticon-cluster", + "link": "/setting/collector" + }, { "text": "tags", "i18n": "menu.extras.tags", diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index e8c90dc2e91..d2daea083d6 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -37,6 +37,7 @@ }, "extras": { "": "More", + "collector": "Collector Cluster", "tags": "Tags Manage", "define": "Monitor Template", "help": "Help Center", @@ -204,9 +205,7 @@ "alert.center.time.tip": "Alerts were triggered {{times}} times during this alert period", "alert.center.first-time": "Start Time", "alert.center.last-time": "Latest Time", - "alert.center.notify.no-delete": "No items selected for deletion!", "alert.center.confirm.delete": "Please confirm whether to delete!", - "alert.center.confirm.delete-batch": "Please confirm whether to delete in batch!", "alert.center.confirm.clear-all": "Please confirm whether to clear all alerts!", "alert.center.notify.no-mark": "No items selected for mark!", "alert.center.confirm.mark-done-batch": "Please confirm whether to mark processed in batch!", @@ -403,6 +402,8 @@ "common.notify.new-fail": "Add Failed!", "common.notify.apply-success": "Apply Success!", "common.notify.apply-fail": "Apply Failed!", + "common.notify.operate-success": "Operate Success!", + "common.notify.operate-fail": "Operate Failed!", "common.notify.monitor-fail": "Query Monitor Failed!", "common.notify.edit-success": "Edit Success!", "common.notify.edit-fail": "Edit Failed!", @@ -498,13 +499,23 @@ "settings.system-config.timezone": "System Time Zone", "settings.system-config.ok": "Confirm Update", "collector": "Collector", + "collector.name": "Collector Name", "collector.status": "Online Status", - "collector.task": "Number of Tasks", + "collector.task": "Total Number of Tasks", "collector.start-time": "Start Time", "collector.ip": "IP Address", "collector.node": "Node Name", "collector.pinned": "Pinned", "collector.dispatched": "Dispatched", + "collector.delete": "Delete Collector", + "collector.online": "Online Collector", + "collector.offline": "Offline Collector", + "collector.confirm.online": "Please confirm whether to online this collector!", + "collector.confirm.offline": "Please confirm whether to offline this collector!", + "collector.confirm.online-batch": "Please confirm whether to online collector in batches!", + "collector.confirm.offline-batch": "Please confirm whether to offline the collector in batches!", + "collector.help": "The collection cluster is used to manage registered collector cluster nodes, displaying the current collector cluster status and scheduling task distribution.
In addition to manually logging off and online on the collector side, you can actively log off and online on the console side Manage collector nodes and support batch operations such as online, offline, and deletion.", + "collector.help.link": "https://hertzbeat.com/docs/help/guide", "about.title": "HertzBeat is an open source, real-time monitoring system with custom, performance and agentless.", "about.point.1": "Monitor, alarm, notify all in one, supports web, database, os, middleware, cloud-native, network etc.", "about.point.2": "Easy to use, full web-based operations with just a click of a mouse.", diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index 5a7750a457a..fecfc80a380 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -37,6 +37,7 @@ }, "extras": { "": "更多", + "collector": "采集集群", "tags": "标签管理", "define": "监控模版", "help": "帮助中心", @@ -204,9 +205,7 @@ "alert.center.time.tip": "此告警期间统计触发 {{times}} 次告警", "alert.center.first-time": "开始", "alert.center.last-time": "最新", - "alert.center.notify.no-delete": "未选中任何待删除项!", "alert.center.confirm.delete": "请确认是否删除!", - "alert.center.confirm.delete-batch": "请确认是否批量删除!", "alert.center.confirm.clear-all": "请确认是否清空所有告警记录!", "alert.center.notify.no-mark": "未选中任何待标记项!", "alert.center.confirm.mark-done-batch": "请确认是否批量标记已处理!", @@ -401,6 +400,8 @@ "common.notify.delete-fail": "删除失败!", "common.notify.new-success": "新增成功!", "common.notify.new-fail": "新增失败!", + "common.notify.operate-success": "操作成功!", + "common.notify.operate-fail": "操作失败!", "common.notify.apply-success": "应用成功!", "common.notify.apply-fail": "应用失败!", "common.notify.monitor-fail": "查询此监控定义详情失败!", @@ -494,13 +495,23 @@ "settings.system-config.timezone": "系统时区", "settings.system-config.ok": "确认更新", "collector": "采集器", + "collector.name": "采集器名称", "collector.status": "运行状态", - "collector.task": "任务数量", + "collector.task": "总任务数量", "collector.start-time": "启动时间", "collector.ip": "IP地址", "collector.node": "节点名称", "collector.pinned": "固定任务", "collector.dispatched": "调度任务", + "collector.delete": "删除采集器", + "collector.online": "上线采集器", + "collector.offline": "下线采集器", + "collector.confirm.online": "请确认是否上线此采集器!", + "collector.confirm.offline": "请确认是否下线此采集器!", + "collector.confirm.online-batch": "请确认是否批量上线采集器!", + "collector.confirm.offline-batch": "请确认是否批量下线采集器!", + "collector.help": "采集集群用于已注册的采集器集群节点管理,展示当前采集器集群状态和调度任务分布。
除了采集器端手动下线上线外,您可在控制台端主动管理采集器节点,支持上线,下线,删除等批量操作。", + "collector.help.link": "https://hertzbeat.com/docs/help/guide", "about.title": "HertzBeat(赫兹跳动) 是一个拥有强大自定义监控能力,高性能集群,无需 Agent 的开源实时监控告警系统。", "about.point.1": "一站式监控告警通知,支持应用服务,数据库,操作系统,中间件,云原生,网络等。", "about.point.2": "易用友好,无需 Agent,全页面操作,鼠标点一点就能监控告警。", diff --git a/web-app/src/assets/i18n/zh-TW.json b/web-app/src/assets/i18n/zh-TW.json index 4e05a50c9f3..51df9c260a1 100644 --- a/web-app/src/assets/i18n/zh-TW.json +++ b/web-app/src/assets/i18n/zh-TW.json @@ -37,6 +37,7 @@ }, "extras": { "": "更多", + "collector": "採集集群", "tags": "標簽管理", "define": "監控模版", "help": "幫助中心", @@ -203,9 +204,7 @@ "alert.center.time.tip": "此告警期間統計觸發 {{times}} 次告警", "alert.center.first-time": "開始", "alert.center.last-time": "最新", - "alert.center.notify.no-delete": "未選中任何待刪除項!", "alert.center.confirm.delete": "請確認是否刪除!", - "alert.center.confirm.delete-batch": "請確認是否批量刪除!", "alert.center.confirm.clear-all": "請確認是否清空所有告警記錄!", "alert.center.notify.no-mark": "未選中任何待標記項!", "alert.center.confirm.mark-done-batch": "請確認是否批量標記已處理!", @@ -402,6 +401,8 @@ "common.notify.new-fail": "新增失敗!", "common.notify.apply-success": "應用成功!", "common.notify.apply-fail": "應用失敗!", + "common.notify.operate-success": "操作成功!", + "common.notify.operate-fail": "操作失敗!", "common.notify.monitor-fail": "查詢此監控定義詳情失敗!", "common.notify.edit-success": "修改成功!", "common.notify.edit-fail": "修改失敗!", @@ -493,13 +494,23 @@ "settings.system-config.timezone": "系統時區", "settings.system-config.ok": "確認更新", "collector": "採集器", + "collector.name": "採集器名称", "collector.status": "運行狀態", - "collector.task": "任務數量", + "collector.task": "总任務數量", "collector.start-time": "啟動時間", "collector.ip": "IP地址", "collector.node": "節點名稱", "collector.pinned": "固定任務", "collector.dispatched": "調度任務", + "collector.delete": "刪除採集器", + "collector.online": "上線採集器", + "collector.offline": "下線採集器", + "collector.confirm.online": "請確認是否上線此採集器!", + "collector.confirm.offline": "請確認是否下線此採集器!", + "collector.confirm.online-batch": "請確認是否批量上線採集器!", + "collector.confirm.offline-batch": "請確認是否批量下線採集器!", + "collector.help": "採集集群用於已註冊的採集器集群節點管理,展示當前採集器集群狀態和調度任務分佈。
除了採集器端手動下線上線外,您可在控制台端主動管理採集器節點,支持上線,下線,刪除等批量操作。", + "collector.help.link": "https://hertzbeat.com/docs/help/guide", "about.title": "HertzBeat(赫茲跳動) 是一個擁有強大自定義監控能力,高性能集群,無需 Agent 的開源實時監控告警系統。", "about.point.1": "一站式監控告警通知,支持應用服務,數據庫,操作系統,中間件,雲原生,網絡等。", "about.point.2": "易用友好,無需 Agent,全頁面操作,鼠標點一點就能監控告警。",