feat: 新增嵌入式分布式调度底座
- 提供通用调度 API、Quartz JDBC Provider 与独立 Starter - 补充 MySQL、PostgreSQL、H2 建表脚本与接入校验 - 同步完善 Federation 与 Scheduler 模块说明
This commit is contained in:
23
easy-agents-scheduler/easy-agents-scheduler-core/pom.xml
Normal file
23
easy-agents-scheduler/easy-agents-scheduler-core/pom.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-scheduler</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>easy-agents-scheduler-core</artifactId>
|
||||
<name>easy-agents-scheduler-core</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 同一调度任务的并发执行策略。
|
||||
*/
|
||||
public enum ConcurrencyPolicy {
|
||||
|
||||
/** 允许同一任务的不同触发同时执行。 */
|
||||
ALLOW,
|
||||
|
||||
/** 同一任务任意时刻最多执行一个触发。 */
|
||||
DISALLOW
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 带显式时区的 Cron 调度计划。
|
||||
*
|
||||
* @param expression Quartz Cron 表达式
|
||||
* @param zoneId 解释 Cron 表达式的时区
|
||||
*/
|
||||
public record CronSchedulePlan(String expression, ZoneId zoneId) implements SchedulePlan {
|
||||
|
||||
/** Quartz 官方 JDBC 表中 Cron 表达式的最大长度。 */
|
||||
public static final int MAX_EXPRESSION_LENGTH = 120;
|
||||
|
||||
/** Quartz 官方 JDBC 表中时区标识的最大长度。 */
|
||||
public static final int MAX_ZONE_ID_LENGTH = 80;
|
||||
|
||||
/**
|
||||
* 校验并创建 Cron 调度计划。
|
||||
*
|
||||
* @throws IllegalArgumentException 表达式为空时抛出
|
||||
* @throws NullPointerException 时区为空时抛出
|
||||
*/
|
||||
public CronSchedulePlan {
|
||||
if (expression == null || expression.isBlank()) {
|
||||
throw new IllegalArgumentException("cron expression must not be blank");
|
||||
}
|
||||
expression = expression.trim().replaceAll("\\s+", " ");
|
||||
if (expression.length() > MAX_EXPRESSION_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"cron expression length must not exceed " + MAX_EXPRESSION_LENGTH
|
||||
);
|
||||
}
|
||||
zoneId = Objects.requireNonNull(zoneId, "zoneId must not be null");
|
||||
if (zoneId.getId().length() > MAX_ZONE_ID_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"zoneId length must not exceed " + MAX_ZONE_ID_LENGTH
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度器错过计划时间后的处理策略。
|
||||
*/
|
||||
public enum MisfirePolicy {
|
||||
|
||||
/** 跳过已经错过的触发。 */
|
||||
SKIP,
|
||||
|
||||
/** 恢复后立即补触发一次。 */
|
||||
FIRE_ONCE_NOW
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 只执行一次的调度计划。
|
||||
*
|
||||
* @param fireAt 计划触发时间
|
||||
*/
|
||||
public record OnceSchedulePlan(Instant fireAt) implements SchedulePlan {
|
||||
|
||||
/**
|
||||
* 校验并创建一次性调度计划。
|
||||
*
|
||||
* @throws NullPointerException 触发时间为空时抛出
|
||||
*/
|
||||
public OnceSchedulePlan {
|
||||
fireAt = Objects.requireNonNull(fireAt, "fireAt must not be null");
|
||||
// Quartz 和关系数据库均以毫秒保存时间,提前归一化可保持持久化往返相等。
|
||||
fireAt = Instant.ofEpochMilli(fireAt.toEpochMilli());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 与具体调度实现无关的任务定义。
|
||||
*
|
||||
* @param id 稳定任务标识
|
||||
* @param handlerCode 任务处理器代码
|
||||
* @param plan 调度计划
|
||||
* @param misfirePolicy 错过触发后的处理策略
|
||||
* @param concurrencyPolicy 同一任务的并发策略
|
||||
* @param recoverOnNodeFailure 执行节点故障后是否请求恢复执行
|
||||
* @param parameters 只包含字符串的不可变任务参数
|
||||
* @param description 可选任务说明
|
||||
*/
|
||||
public record ScheduleDefinition(
|
||||
ScheduleId id,
|
||||
String handlerCode,
|
||||
SchedulePlan plan,
|
||||
MisfirePolicy misfirePolicy,
|
||||
ConcurrencyPolicy concurrencyPolicy,
|
||||
boolean recoverOnNodeFailure,
|
||||
Map<String, String> parameters,
|
||||
String description
|
||||
) {
|
||||
|
||||
/** 参数键和值合计允许的默认最大 UTF-8 字节数。 */
|
||||
public static final int MAX_PARAMETER_BYTES = 16 * 1024;
|
||||
|
||||
/** Handler Code 最大长度。 */
|
||||
public static final int MAX_HANDLER_CODE_LENGTH = 190;
|
||||
|
||||
/** 任务说明最大长度,与 Quartz 官方表结构保持一致。 */
|
||||
public static final int MAX_DESCRIPTION_LENGTH = 250;
|
||||
|
||||
/**
|
||||
* 校验并创建任务定义。
|
||||
*
|
||||
* @throws ScheduleException 参数不合法时抛出
|
||||
* @throws IllegalArgumentException Handler 或说明不合法时抛出
|
||||
* @throws NullPointerException 必填模型为空时抛出
|
||||
*/
|
||||
public ScheduleDefinition {
|
||||
id = Objects.requireNonNull(id, "id must not be null");
|
||||
plan = Objects.requireNonNull(plan, "plan must not be null");
|
||||
misfirePolicy = Objects.requireNonNull(misfirePolicy, "misfirePolicy must not be null");
|
||||
concurrencyPolicy = Objects.requireNonNull(concurrencyPolicy, "concurrencyPolicy must not be null");
|
||||
handlerCode = normalizeRequired(handlerCode, "handlerCode", MAX_HANDLER_CODE_LENGTH);
|
||||
description = description == null ? "" : description.trim();
|
||||
if (description.length() > MAX_DESCRIPTION_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
"description length must not exceed " + MAX_DESCRIPTION_LENGTH
|
||||
);
|
||||
}
|
||||
parameters = copyAndValidateParameters(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 防御复制并校验字符串参数。
|
||||
*
|
||||
* @param parameters 待校验参数,可为空
|
||||
* @return 不可变参数副本
|
||||
* @throws ScheduleException 参数键为空、值为空或总大小超限时抛出
|
||||
*/
|
||||
public static Map<String, String> copyAndValidateParameters(Map<String, String> parameters) {
|
||||
if (parameters == null || parameters.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
int totalBytes = 0;
|
||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (key == null || key.isBlank()) {
|
||||
throw invalidParameters("parameter key must not be blank");
|
||||
}
|
||||
if (value == null) {
|
||||
throw invalidParameters("parameter value must not be null: " + key);
|
||||
}
|
||||
totalBytes += key.getBytes(StandardCharsets.UTF_8).length;
|
||||
totalBytes += value.getBytes(StandardCharsets.UTF_8).length;
|
||||
if (totalBytes > MAX_PARAMETER_BYTES) {
|
||||
throw invalidParameters(
|
||||
"parameter size must not exceed " + MAX_PARAMETER_BYTES + " UTF-8 bytes"
|
||||
);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造参数非法异常。
|
||||
*
|
||||
* @param message 非法原因
|
||||
* @return 携带稳定错误码的异常
|
||||
*/
|
||||
private static ScheduleException invalidParameters(String message) {
|
||||
return new ScheduleException(ScheduleErrorCode.INVALID_DEFINITION, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化必填字符串。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @param field 字段名
|
||||
* @param maxLength 最大长度
|
||||
* @return 规范化值
|
||||
*/
|
||||
private static String normalizeRequired(String value, String field, int maxLength) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() > maxLength) {
|
||||
throw new IllegalArgumentException(field + " length must not exceed " + maxLength);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度底座向调用方暴露的稳定错误码。
|
||||
*/
|
||||
public enum ScheduleErrorCode {
|
||||
|
||||
/** 调度定义不合法。 */
|
||||
INVALID_DEFINITION,
|
||||
|
||||
/** 同标识任务已存在且定义冲突。 */
|
||||
SCHEDULE_CONFLICT,
|
||||
|
||||
/** 目标任务不存在。 */
|
||||
SCHEDULE_NOT_FOUND,
|
||||
|
||||
/** 当前节点没有注册任务所需 Handler。 */
|
||||
HANDLER_NOT_FOUND,
|
||||
|
||||
/** Handler Code 重复注册。 */
|
||||
DUPLICATE_HANDLER,
|
||||
|
||||
/** Quartz 数据库结构缺失或不兼容。 */
|
||||
SCHEMA_INVALID,
|
||||
|
||||
/** 调度 Provider 启动或运行失败。 */
|
||||
PROVIDER_FAILURE,
|
||||
|
||||
/** 调度器已经关闭。 */
|
||||
SCHEDULER_CLOSED
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 调度底座异常,携带稳定错误码供调用方分类处理。
|
||||
*/
|
||||
public class ScheduleException extends RuntimeException {
|
||||
|
||||
/** 稳定错误码。 */
|
||||
private final ScheduleErrorCode errorCode;
|
||||
|
||||
/**
|
||||
* 创建调度异常。
|
||||
*
|
||||
* @param errorCode 稳定错误码
|
||||
* @param message 非空错误说明
|
||||
*/
|
||||
public ScheduleException(ScheduleErrorCode errorCode, String message) {
|
||||
super(requireMessage(message));
|
||||
this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带原始原因的调度异常。
|
||||
*
|
||||
* @param errorCode 稳定错误码
|
||||
* @param message 非空错误说明
|
||||
* @param cause 原始异常
|
||||
*/
|
||||
public ScheduleException(ScheduleErrorCode errorCode, String message, Throwable cause) {
|
||||
super(requireMessage(message), cause);
|
||||
this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回稳定错误码。
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public ScheduleErrorCode errorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验异常消息。
|
||||
*
|
||||
* @param message 原始消息
|
||||
* @return 非空消息
|
||||
*/
|
||||
private static String requireMessage(String message) {
|
||||
if (message == null || message.isBlank()) {
|
||||
throw new IllegalArgumentException("message must not be blank");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 调度执行事件监听器,适合接入日志、指标和诊断事件。
|
||||
*/
|
||||
public interface ScheduleExecutionListener {
|
||||
|
||||
/**
|
||||
* Handler 开始执行时回调。
|
||||
*
|
||||
* @param context 触发上下文
|
||||
*/
|
||||
default void onStarted(ScheduleFireContext context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler 成功完成时回调。
|
||||
*
|
||||
* @param context 触发上下文
|
||||
* @param duration 执行耗时
|
||||
*/
|
||||
default void onSucceeded(ScheduleFireContext context, Duration duration) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler 执行失败时回调。
|
||||
*
|
||||
* @param context 触发上下文
|
||||
* @param duration 失败前执行耗时
|
||||
* @param failure 原始失败原因
|
||||
*/
|
||||
default void onFailed(ScheduleFireContext context, Duration duration, Throwable failure) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度计划发生 Misfire 时回调。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @param expectedFireTime 原计划触发时间,底层无法提供时为空
|
||||
*/
|
||||
default void onMisfired(ScheduleId scheduleId, Instant expectedFireTime) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Handler 执行时可读取的稳定触发上下文。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @param handlerCode Handler Code
|
||||
* @param scheduledFireTime 计划触发时间
|
||||
* @param actualFireTime 实际触发时间
|
||||
* @param fireInstanceId 当前物理触发实例标识
|
||||
* @param invocationId 立即触发调用标识,周期触发时可为空
|
||||
* @param recovering 当前执行是否由节点故障恢复产生
|
||||
* @param parameters 合并后的不可变字符串参数
|
||||
*/
|
||||
public record ScheduleFireContext(
|
||||
ScheduleId scheduleId,
|
||||
String handlerCode,
|
||||
Instant scheduledFireTime,
|
||||
Instant actualFireTime,
|
||||
String fireInstanceId,
|
||||
String invocationId,
|
||||
boolean recovering,
|
||||
Map<String, String> parameters
|
||||
) {
|
||||
|
||||
/**
|
||||
* 校验并创建触发上下文。
|
||||
*
|
||||
* @throws NullPointerException 必填字段为空时抛出
|
||||
*/
|
||||
public ScheduleFireContext {
|
||||
scheduleId = Objects.requireNonNull(scheduleId, "scheduleId must not be null");
|
||||
handlerCode = Objects.requireNonNull(handlerCode, "handlerCode must not be null");
|
||||
scheduledFireTime = Objects.requireNonNull(
|
||||
scheduledFireTime,
|
||||
"scheduledFireTime must not be null"
|
||||
);
|
||||
actualFireTime = Objects.requireNonNull(actualFireTime, "actualFireTime must not be null");
|
||||
fireInstanceId = Objects.requireNonNull(fireInstanceId, "fireInstanceId must not be null");
|
||||
parameters = ScheduleDefinition.copyAndValidateParameters(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 立即触发请求已被调度器接受的回执。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @param invocationId 调用方提供的链路标识
|
||||
* @param acceptedAt 调度器接受请求的时间
|
||||
*/
|
||||
public record ScheduleFireReceipt(
|
||||
ScheduleId scheduleId,
|
||||
String invocationId,
|
||||
Instant acceptedAt
|
||||
) {
|
||||
|
||||
/**
|
||||
* 校验并创建立即触发回执。
|
||||
*
|
||||
* @throws IllegalArgumentException 调用标识为空时抛出
|
||||
* @throws NullPointerException 任务标识或接受时间为空时抛出
|
||||
*/
|
||||
public ScheduleFireReceipt {
|
||||
scheduleId = Objects.requireNonNull(scheduleId, "scheduleId must not be null");
|
||||
acceptedAt = Objects.requireNonNull(acceptedAt, "acceptedAt must not be null");
|
||||
if (invocationId == null || invocationId.isBlank()) {
|
||||
throw new IllegalArgumentException("invocationId must not be blank");
|
||||
}
|
||||
invocationId = invocationId.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度任务处理器,由调用方注册到运行节点。
|
||||
*/
|
||||
public interface ScheduleHandler {
|
||||
|
||||
/**
|
||||
* 返回跨节点一致的稳定 Handler Code。
|
||||
*
|
||||
* @return 非空 Handler Code
|
||||
*/
|
||||
String code();
|
||||
|
||||
/**
|
||||
* 执行一次调度触发。
|
||||
*
|
||||
* @param context 触发上下文
|
||||
* @throws Exception 业务执行失败时抛出,调度器保留原始异常链
|
||||
*/
|
||||
void execute(ScheduleFireContext context) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度任务的稳定标识,由命名空间和名称组成。
|
||||
*
|
||||
* @param namespace 业务无关的命名空间
|
||||
* @param name 命名空间内唯一的任务名称
|
||||
*/
|
||||
public record ScheduleId(String namespace, String name) {
|
||||
|
||||
/** 命名空间最大长度。 */
|
||||
public static final int MAX_NAMESPACE_LENGTH = 160;
|
||||
|
||||
/** 任务名称最大长度。 */
|
||||
public static final int MAX_NAME_LENGTH = 190;
|
||||
|
||||
/**
|
||||
* 校验并创建任务标识。
|
||||
*
|
||||
* @throws IllegalArgumentException 标识为空、过长或含控制字符时抛出
|
||||
*/
|
||||
public ScheduleId {
|
||||
namespace = requireIdentifier(namespace, "namespace", MAX_NAMESPACE_LENGTH);
|
||||
name = requireIdentifier(name, "name", MAX_NAME_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化并校验标识片段。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @param field 字段名
|
||||
* @param maxLength 最大长度
|
||||
* @return 规范化值
|
||||
*/
|
||||
private static String requireIdentifier(String value, String field, int maxLength) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() > maxLength) {
|
||||
throw new IllegalArgumentException(field + " length must not exceed " + maxLength);
|
||||
}
|
||||
for (int index = 0; index < normalized.length(); index++) {
|
||||
if (Character.isISOControl(normalized.charAt(index))) {
|
||||
throw new IllegalArgumentException(field + " must not contain control characters");
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回便于日志和诊断使用的稳定文本。
|
||||
*
|
||||
* @return namespace/name 格式的任务标识
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return namespace + "/" + name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度计划的封闭类型,首期支持 Cron 和一次性计划。
|
||||
*/
|
||||
public sealed interface SchedulePlan permits CronSchedulePlan, OnceSchedulePlan {
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 与具体调度实现无关的任务管理服务。
|
||||
*/
|
||||
public interface ScheduleService {
|
||||
|
||||
/**
|
||||
* 创建任务;同标识同定义重复创建视为幂等成功。
|
||||
*
|
||||
* @param definition 任务定义
|
||||
* @return 创建后的任务视图
|
||||
* @throws ScheduleException 同标识存在不同定义或 Provider 失败时抛出
|
||||
*/
|
||||
ScheduleView create(ScheduleDefinition definition);
|
||||
|
||||
/**
|
||||
* 将任务完整替换为目标定义,目标不存在时按目标定义创建。
|
||||
*
|
||||
* @param definition 目标任务定义
|
||||
* @return 替换后的任务视图
|
||||
* @throws ScheduleException Provider 失败时抛出
|
||||
*/
|
||||
ScheduleView replace(ScheduleDefinition definition);
|
||||
|
||||
/**
|
||||
* 暂停任务的计划触发。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @return 暂停后的任务视图
|
||||
* @throws ScheduleException 任务不存在或 Provider 失败时抛出
|
||||
*/
|
||||
ScheduleView pause(ScheduleId scheduleId);
|
||||
|
||||
/**
|
||||
* 恢复任务的计划触发。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @return 恢复后的任务视图
|
||||
* @throws ScheduleException 任务不存在或 Provider 失败时抛出
|
||||
*/
|
||||
ScheduleView resume(ScheduleId scheduleId);
|
||||
|
||||
/**
|
||||
* 删除任务及其计划触发器。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @return 存在并删除成功时为 true,原本不存在时为 false
|
||||
* @throws ScheduleException Provider 失败时抛出
|
||||
*/
|
||||
boolean delete(ScheduleId scheduleId);
|
||||
|
||||
/**
|
||||
* 立即触发一次已存在任务。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @param invocationId 调用方生成的链路和幂等标识
|
||||
* @param parameters 仅对本次触发生效并覆盖同名基础参数的字符串参数
|
||||
* @return 请求接受回执
|
||||
* @throws ScheduleException 参数非法、任务不存在或 Provider 失败时抛出
|
||||
*/
|
||||
ScheduleFireReceipt triggerNow(
|
||||
ScheduleId scheduleId,
|
||||
String invocationId,
|
||||
Map<String, String> parameters
|
||||
);
|
||||
|
||||
/**
|
||||
* 查询任务当前定义和状态。
|
||||
*
|
||||
* @param scheduleId 任务标识
|
||||
* @return 任务不存在时为空
|
||||
* @throws ScheduleException Provider 失败时抛出
|
||||
*/
|
||||
Optional<ScheduleView> get(ScheduleId scheduleId);
|
||||
|
||||
/**
|
||||
* 从当前时间开始预览后续触发时间,不创建持久任务。
|
||||
*
|
||||
* @param plan 调度计划
|
||||
* @param limit 最大返回数量
|
||||
* @return 按时间升序排列的触发时间
|
||||
* @throws ScheduleException 计划不合法时抛出
|
||||
*/
|
||||
List<Instant> nextFireTimes(SchedulePlan plan, int limit);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
/**
|
||||
* 调度任务的实现无关状态。
|
||||
*/
|
||||
public enum ScheduleStatus {
|
||||
|
||||
/** 等待正常触发。 */
|
||||
SCHEDULED,
|
||||
|
||||
/** 已暂停。 */
|
||||
PAUSED,
|
||||
|
||||
/** 当前触发被并发策略阻塞。 */
|
||||
BLOCKED,
|
||||
|
||||
/** 已完成且不会再次触发。 */
|
||||
COMPLETE,
|
||||
|
||||
/** 调度存储将任务标记为错误。 */
|
||||
ERROR
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 调度任务定义及当前运行状态的只读视图。
|
||||
*
|
||||
* @param definition 任务定义
|
||||
* @param status 当前状态
|
||||
* @param previousFireTime 上一次触发时间,可为空
|
||||
* @param nextFireTime 下一次触发时间,可为空
|
||||
*/
|
||||
public record ScheduleView(
|
||||
ScheduleDefinition definition,
|
||||
ScheduleStatus status,
|
||||
Instant previousFireTime,
|
||||
Instant nextFireTime
|
||||
) {
|
||||
|
||||
/**
|
||||
* 校验并创建任务视图。
|
||||
*
|
||||
* @throws NullPointerException 定义或状态为空时抛出
|
||||
*/
|
||||
public ScheduleView {
|
||||
definition = Objects.requireNonNull(definition, "definition must not be null");
|
||||
status = Objects.requireNonNull(status, "status must not be null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.easyagents.scheduler;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
/**
|
||||
* {@link ScheduleDefinition} 不可变性和输入边界测试。
|
||||
*/
|
||||
public class ScheduleDefinitionTest {
|
||||
|
||||
/**
|
||||
* 验证任务参数会被防御复制。
|
||||
*/
|
||||
@Test
|
||||
public void shouldDefensivelyCopyParameters() {
|
||||
Map<String, String> parameters = new HashMap<>();
|
||||
parameters.put("reportId", "r-1");
|
||||
|
||||
ScheduleDefinition definition = definition(parameters);
|
||||
parameters.put("reportId", "r-2");
|
||||
|
||||
assertEquals("r-1", definition.parameters().get("reportId"));
|
||||
assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> definition.parameters().put("new", "value")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证超大参数会被拒绝。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectOversizedParameters() {
|
||||
Map<String, String> parameters = Map.of(
|
||||
"payload",
|
||||
"x".repeat(ScheduleDefinition.MAX_PARAMETER_BYTES)
|
||||
);
|
||||
|
||||
ScheduleException exception = assertThrows(
|
||||
ScheduleException.class,
|
||||
() -> definition(parameters)
|
||||
);
|
||||
assertEquals(ScheduleErrorCode.INVALID_DEFINITION, exception.errorCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Cron 表达式会规范化空白并拒绝超过 JDBC 表上限的内容。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNormalizeAndBoundCronExpression() {
|
||||
CronSchedulePlan normalized = new CronSchedulePlan(
|
||||
"0 0/5 * * * ?",
|
||||
ZoneId.of("UTC")
|
||||
);
|
||||
assertEquals("0 0/5 * * * ?", normalized.expression());
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new CronSchedulePlan("0".repeat(121), ZoneId.of("UTC"))
|
||||
);
|
||||
}
|
||||
|
||||
private ScheduleDefinition definition(Map<String, String> parameters) {
|
||||
return new ScheduleDefinition(
|
||||
new ScheduleId("reports", "daily"),
|
||||
"generate-report",
|
||||
new CronSchedulePlan("0 0 2 * * ?", ZoneId.of("Asia/Shanghai")),
|
||||
MisfirePolicy.FIRE_ONCE_NOW,
|
||||
ConcurrencyPolicy.DISALLOW,
|
||||
true,
|
||||
parameters,
|
||||
"daily report"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user