diff --git a/spring-cloud-alibaba-dependencies/pom.xml b/spring-cloud-alibaba-dependencies/pom.xml index cd1dd63b6..86a9268c8 100644 --- a/spring-cloud-alibaba-dependencies/pom.xml +++ b/spring-cloud-alibaba-dependencies/pom.xml @@ -23,6 +23,8 @@ 1.6.1 1.8.6 1.0.11 + 4.23.0 + 1.11.4 2.2.1 @@ -160,6 +162,23 @@ ${seata.version} + + + net.javacrumbs.shedlock + shedlock-spring + ${shedlock.version} + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + ${shedlock.version} + + + com.aliyun.schedulerx + schedulerx2-worker + ${schedulerx.worker.version} + + com.alibaba.cloud @@ -251,6 +270,12 @@ ${revision} + + com.alibaba.cloud + spring-cloud-starter-alibaba-schedulerx + ${revision} + + com.alibaba.spring spring-context-support diff --git a/spring-cloud-alibaba-examples/pom.xml b/spring-cloud-alibaba-examples/pom.xml index a7b5aecb6..06b6e9531 100644 --- a/spring-cloud-alibaba-examples/pom.xml +++ b/spring-cloud-alibaba-examples/pom.xml @@ -46,7 +46,7 @@ rocketmq-example/rocketmq-tx-example rocketmq-example/rocketmq-pollable-consume-example rocketmq-example/rocketmq-retrieable-consume-example - + spring-cloud-scheduling-example spring-cloud-bus-rocketmq-example spring-cloud-alibaba-sidecar-examples/spring-cloud-alibaba-sidecar-nacos-example spring-cloud-alibaba-sidecar-examples/spring-cloud-alibaba-sidecar-consul-example @@ -57,7 +57,7 @@ integrated-example/integrated-praise-provider integrated-example/integrated-praise-consumer integrated-example/integrated-common - integrated-example/integrated-frontend + integrated-example/integrated-frontend governance-example/label-routing-example/istio-consumer-example governance-example/label-routing-example/opensergo-consumer-example governance-example/label-routing-example/consumer-example @@ -65,7 +65,6 @@ governance-example/label-routing-example/provider-version-example governance-example/authentication-example/istio-authentication-provider-mvc-example governance-example/authentication-example/istio-authentication-provider-webflux-example - diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README-en.md b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README-en.md new file mode 100644 index 000000000..2b8183a38 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README-en.md @@ -0,0 +1,101 @@ +# Spring Cloud Alibaba Scheduling Example + +## Project description + +Spring Cloud Alibaba Scheduling provides a timing task scheduling capability based on Spring Scheduling, supporting distributed scenarios for timing task scheduling. It offers a quick integration solution for timing task scheduling services in distributed scenarios. + +The current offering is based on the open-source ShedLock for distributed lock acquisition, along with Alibaba Cloud's SchedulerX service [quick start](https://sca.aliyun.com/en/docs/2023/user-guide/schedulerx/quick-start/), Subsequent releases will provide access to more open-source solutions implementations. + +## Project dependencies + +### Access `spring-cloud-starter-alibaba-schedulerx` + +Add the following dependencies to the project `pom.xml`: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-schedulerx + + ``` + +## Project config description + +In the Example project, two types of integration configuration modes are provided for `shedlock` and `schedulerx`, Select the required access mode configuration file in `application.yaml`, the example defaults to the shedlock solution. + +### Solution 1. Distributed shedlock integration configuration + +Edit the following configuration to the `application-schedulerx.yml`: + ```yaml + spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + # Set config value: shedlock + distributed-mode: shedlock + datasource: + driver-class: com.mysql.cj.jdbc.Driver + url: {jdbc_url} + username: {jdbc.username} + password: {jdbc.password} + ``` +You should replace `{jdbc_url}`,`{jdbc.username}`, and `{jdbc.password}` with your actual database connection information. + +>️ Precautions:If there's no database instance can be used, please create a database instance first. + +### Solution 2. Alibaba Cloud's SchedulerX integration configuration +Edit the following configuration to the `application-schedulerx.yml`: + ```yaml + spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + # Set config value: schedulerx + distributed-mode: schedulerx + schedulerx: + # This configuration is required, Please get it from aliyun schedulerx console + endpoint: acm.aliyun.com + namespace: aad167f6-xxxx-xxxx-xxxx-xxxxxxxxx + groupId: xxxxx + appKey: PZm1XXXXXXXXXXXX + # Optional config, if you need to sync task to schedulerx + # task-sync: true + # region-id: public + # aliyun-access-key: XXXXXXXXXXXX + # aliyun-secret-key: XXXXXXXXXXXX + # task-model-default: standalone + ``` +On Alibaba Cloud service, each account has be granted a free quota for schedulerx. For detailed instructions on how to configure and use cloud product integrations, please refer to the respective product documentation. Refer to: [SchedulerX Spring Task](https://www.alibabacloud.com/help/en/schedulerx/user-guide/spring-jobs) + +## Start application + +After completing the above selection and configuration, simply run the `ScheduleApplication` class in Example to start the application. The `SimpleJob` class in this example project includes two Spring scheduled tasks that run every minute. Upon starting, you can expect to see the following logs: + +```text +2024-05-17T11:20:59.981+08:00 INFO 66613 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-17 11:20:59 do job1... +2024-05-17T11:20:59.985+08:00 INFO 66613 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-1] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-17 11:20:59 do job2... +``` +### Distributed running verification + +Create two application launch configurations in IDEA, each with the startup parameter `--server.port={port}` to start the corresponding application processes. The example project defaults to using `shedlock`, +We can observe that `job1` will be triggered by both applications, whereas `job2` which has been annotated with `@SchedulerLock` will only be triggered in one application at the same time. +![idea-server-port](images/idea-server-port.png) + +- ScheduleApplication-1, startup parameter: `--server.port=18080`, application logs: +```text +2024-05-20T14:02:00.003+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job1... +2024-05-20T14:03:00.008+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job1... +2024-05-20T14:03:00.008+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-1] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job2... +2024-05-20T14:04:00.006+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job1... +2024-05-20T14:04:00.010+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-2] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job2... +2024-05-20T14:05:00.003+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-5] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job1... +``` +- ScheduleApplication-2, startup parameter: `--server.port=18081`, application logs: +```text +2024-05-20T14:02:00.003+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job1... +2024-05-20T14:02:00.008+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job2... +2024-05-20T14:03:00.004+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-5] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job1... +2024-05-20T14:04:00.006+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job1... +2024-05-20T14:05:00.004+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-2] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job1... +2024-05-20T14:05:00.007+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job2... +``` diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README.md b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README.md new file mode 100644 index 000000000..76e7835dc --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/README.md @@ -0,0 +1,100 @@ +# Spring Cloud Alibaba Scheduling Example + +## 项目说明 + +Spring Cloud Alibaba Scheduling 提供了基于 Spring Scheduling 的定时任务调度能力,支持分布式场景下的定时任务调度,为分布式场景下的定时任务调度服务提供快速接入方案。 + +目前提供基于开源shedlock分布式抢锁模式,以及阿里云上SchedulerX服务的 [快速接入](https://sca.aliyun.com/docs/2023/user-guide/schedulerx/quick-start/) ,后续将提供更多实现的开源方案接入。 + +## 应用依赖 + +### 接入 `spring-cloud-starter-alibaba-schedulerx` + +在项目 pom.xml 中加入以下依赖: + + ```xml + + com.alibaba.cloud + spring-cloud-starter-alibaba-schedulerx + + ``` + +## 配置说明 + +在Example工程中提供shedlock和schedulerx的两种接入配置模式(***二选一***),在`application.yaml`中选择需要的接入模式配置文件,案例中默认采用开源的shedlock方案。 + +### 方案一、分布式shedlock接入配置说明 + +在 application-schedulerx.yml 配置文件中修改以下配置: + ```yaml + spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + # Set config value: shedlock + distributed-mode: shedlock + datasource: + driver-class: com.mysql.cj.jdbc.Driver + url: {jdbc_url} + username: {jdbc.username} + password: {jdbc.password} + ``` +使用时请需要替换`{jdbc_url}`、`{jdbc.username}`、`{jdbc.password}`为实际自有的数据库连接信息。 + +>️ 注意:如未创建数据库,请先手动创建数据实例。 + +### 方案二、云产品SchedulerX接入配置说明 +在 application-schedulerx.yml 配置文件中修改以下配置: + ```yaml + spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + # Set config value: schedulerx + distributed-mode: schedulerx + schedulerx: + # This configuration is required, Please get it from aliyun schedulerx console + endpoint: acm.aliyun.com + namespace: aad167f6-xxxx-xxxx-xxxx-xxxxxxxxx + groupId: xxxxx + appKey: PZm1XXXXXXXXXXXX + # Optional config, if you need to sync task to schedulerx + # task-sync: true + # region-id: public + # aliyun-access-key: XXXXXXXXXXXX + # aliyun-secret-key: XXXXXXXXXXXX + # task-model-default: standalone + ``` +阿里云上产品每个用户开通后都会有免费额度,详细云上产品接入配置使用说明,请参考:[阿里云SchedulerX Spring定时任务](https://help.aliyun.com/zh/schedulerx/user-guide/spring-jobs) + +## 启动应用 + +在完成上述接入选择和配置后,直接运行Example中的 `ScheduleApplication`类即可启动运行。本案例工程中的`SimpleJob`类包含了两个每分钟执行一次的Spring定时任务,启动后可得到如下日志: +```text +2024-05-17T11:20:59.981+08:00 INFO 66613 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-17 11:20:59 do job1... +2024-05-17T11:20:59.985+08:00 INFO 66613 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-1] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-17 11:20:59 do job2... +``` +### 分布式运行验证 + +在IDEA环境中创建两个应用启动项,各自分别配置启动参数`--server.port={应用端口}`,启动相应的应用进程。案例工程默认采用`shedlock`, +我们可以直接看到`job1`两个应用都会同时触发,而`job2`添加了`@SchedulerLock`注解则会同一时间点只会在一个应用进程中执行。 +![idea-server-port](images/idea-server-port.png) + +- ScheduleApplication-1,启动参数:`--server.port=18080`,定时任务运行日志如下: +```text +2024-05-20T14:02:00.003+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job1... +2024-05-20T14:03:00.008+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job1... +2024-05-20T14:03:00.008+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-1] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job2... +2024-05-20T14:04:00.006+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job1... +2024-05-20T14:04:00.010+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-2] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job2... +2024-05-20T14:05:00.003+08:00 INFO 80520 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-5] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job1... +``` +- ScheduleApplication-2,启动参数:`--server.port=18081`,定时任务运行日志如下: +```text +2024-05-20T14:02:00.003+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job1... +2024-05-20T14:02:00.008+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:02:00 do job2... +2024-05-20T14:03:00.004+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-5] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:03:00 do job1... +2024-05-20T14:04:00.006+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-3] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:04:00 do job1... +2024-05-20T14:05:00.004+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-2] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job1... +2024-05-20T14:05:00.007+08:00 INFO 80596 --- [spring-cloud-alibaba-schedule-example] [ sca-schedule-4] c.a.c.examples.schedule.job.SimpleJob : time=2024-05-20 14:05:00 do job2... +``` diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/images/idea-server-port.png b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/images/idea-server-port.png new file mode 100644 index 000000000..fee23d024 Binary files /dev/null and b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/images/idea-server-port.png differ diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/pom.xml b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/pom.xml new file mode 100644 index 000000000..37bbe6cd2 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/pom.xml @@ -0,0 +1,65 @@ + + + + spring-cloud-alibaba-examples + com.alibaba.cloud + ${revision} + ../pom.xml + + 4.0.0 + + spring-cloud-scheduling-example + Spring Cloud Starter Alibaba Scheduling Example + Example demonstrating how to use Spring Cloud Schedule + jar + + + 8.0.28 + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-schedulerx + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + true + + + + + diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/ScheduleApplication.java b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/ScheduleApplication.java new file mode 100644 index 000000000..a2aab18eb --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/ScheduleApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.examples.schedule; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * ScheduleApplication. + * + * @author yaohui + */ +@SpringBootApplication +@EnableScheduling +public class ScheduleApplication { + + public static void main(String[] args) { + SpringApplication.run(ScheduleApplication.class, args); + } + +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/job/SimpleJob.java b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/job/SimpleJob.java new file mode 100644 index 000000000..654e2cb81 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/java/com/alibaba/cloud/examples/schedule/job/SimpleJob.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.examples.schedule.job; + +import java.util.concurrent.TimeUnit; + +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * @author yaohui + **/ +@Component +public class SimpleJob { + + private static final Logger logger = LoggerFactory.getLogger(SimpleJob.class); + + /** + * run without lock, all instance running at the same time. + */ + @Scheduled(cron = "0 */1 * * * ?") + public void job1() { + logger.info("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job1..."); + } + + + /** + * run with lock, only one instance running at the same time. + * + * @throws InterruptedException interrupted exception + */ + @Scheduled(cron = "0 */1 * * * ?") + @SchedulerLock(name = "lock-job2", lockAtMostFor = "10s") + public void job2() throws InterruptedException { + logger.info("time=" + DateTime.now().toString("YYYY-MM-dd HH:mm:ss") + " do job2..."); + TimeUnit.SECONDS.sleep(1L); + } +} diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-schedulerx.yaml b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-schedulerx.yaml new file mode 100644 index 000000000..d01f8bf75 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-schedulerx.yaml @@ -0,0 +1,17 @@ +spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + distributed-mode: schedulerx + schedulerx: + # This configuration is required, Please get it from aliyun schedulerx console + endpoint: acm.aliyun.com + namespace: aad167f6-xxxx-xxxx-xxxx-xxxxxxxxx + groupId: xxxxx + appKey: PZm1XXXXXXXXXXXX + # Optional config, if you need to sync task to schedulerx +# task-sync: true +# region-id: public +# aliyun-access-key: XXXXXXXXXXXX +# aliyun-secret-key: XXXXXXXXXXXX +# task-model-default: standalone diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-shedlock.yaml b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-shedlock.yaml new file mode 100644 index 000000000..e0c663a32 --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application-shedlock.yaml @@ -0,0 +1,13 @@ +spring: + cloud: + scheduling: + # Distributed mode: shedlock, schedulerx + distributed-mode: shedlock + datasource: + driver-class: com.mysql.cj.jdbc.Driver + # Change to your database jdbc url value + url: jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true + # Change to your database username value + username: root + # Change to your database password value + password: 123456 diff --git a/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application.yaml b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application.yaml new file mode 100644 index 000000000..4edaaa7da --- /dev/null +++ b/spring-cloud-alibaba-examples/spring-cloud-scheduling-example/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +server: + port: 18080 +spring: + profiles: + # Select the config file to use, shedlock or schedulerx + active: shedlock + application: + name: spring-cloud-alibaba-schedule-example + # Spring task scheduling config + task: + scheduling: + thread-name-prefix: sca-schedule- + pool: + size: 5 + shutdown: + await-termination: true + await-termination-period: 60s diff --git a/spring-cloud-alibaba-starters/pom.xml b/spring-cloud-alibaba-starters/pom.xml index 4700172ef..5e2be2ede 100644 --- a/spring-cloud-alibaba-starters/pom.xml +++ b/spring-cloud-alibaba-starters/pom.xml @@ -31,6 +31,7 @@ spring-cloud-starter-alibaba-governance-auth spring-cloud-starter-alibaba-governance-routing spring-cloud-alibaba-commons + spring-cloud-starter-alibaba-schedulerx diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/pom.xml new file mode 100644 index 000000000..883802617 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + + + com.alibaba.cloud + spring-cloud-alibaba-starters + ${revision} + ../pom.xml + + spring-cloud-starter-alibaba-schedulerx + Spring Cloud Starter Alibaba Scheduling + + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.slf4j + slf4j-api + + + + junit + junit + test + + + + + com.aliyun.schedulerx + schedulerx2-worker + + + + com.aliyun + aliyun-java-sdk-schedulerx2 + 1.1.0 + + + + com.aliyun + aliyun-java-sdk-core + 4.4.6 + + + + + + net.javacrumbs.shedlock + shedlock-spring + + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + + + + diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/SchedulingConstants.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/SchedulingConstants.java new file mode 100644 index 000000000..6445f7dfa --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/SchedulingConstants.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling; + +/** + * @author yaohui + **/ +public final class SchedulingConstants { + + /** + * Scheduling config prefix. + */ + public static final String SCHEDULING_CONFIG_PREFIX = "spring.cloud.scheduling"; + + /** + * Scheduling distributed mode. + */ + public static final String SCHEDULING_CONFIG_DISTRIBUTED_MODE_KEY = SCHEDULING_CONFIG_PREFIX + ".distributed-mode"; + + private SchedulingConstants() { + throw new AssertionError("Must not instantiate constant utility class"); + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/JobProperty.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/JobProperty.java new file mode 100644 index 000000000..314e12de1 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/JobProperty.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx; + +import com.alibaba.schedulerx.common.domain.ExecuteMode; +import com.alibaba.schedulerx.common.domain.JobType; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Job property. + * + * @author xiaomeng.hxm + */ +@ConfigurationProperties(prefix = SchedulerxProperties.CONFIG_PREFIX) +public final class JobProperty { + + private String jobName; + + private String jobType = JobType.JAVA.getKey(); + + private String jobModel = ExecuteMode.STANDALONE.getKey(); + + private String className; + + private String content; + + private Integer timeType; + + private String timeExpression; + + private String cron; + + private String oneTime; + + private String jobParameter; + + private String description; + + private boolean overwrite = false; + + public String getJobType() { + return jobType; + } + + public void setJobType(String jobType) { + this.jobType = jobType; + } + + public String getJobModel() { + return jobModel; + } + + public void setJobModel(String jobModel) { + this.jobModel = jobModel; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getCron() { + return cron; + } + + public void setCron(String cron) { + this.cron = cron; + } + + public String getOneTime() { + return oneTime; + } + + public void setOneTime(String oneTime) { + this.oneTime = oneTime; + } + + public String getJobParameter() { + return jobParameter; + } + + public void setJobParameter(String jobParameter) { + this.jobParameter = jobParameter; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isOverwrite() { + return overwrite; + } + + public void setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getJobName() { + return jobName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public Integer getTimeType() { + return timeType; + } + + public void setTimeType(Integer timeType) { + this.timeType = timeType; + } + + public String getTimeExpression() { + return timeExpression; + } + + public void setTimeExpression(String timeExpression) { + this.timeExpression = timeExpression; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxAutoConfigure.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxAutoConfigure.java new file mode 100644 index 000000000..261e66577 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxAutoConfigure.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx; + +import com.alibaba.cloud.scheduling.SchedulingConstants; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; + +/** + * schedulerx2 starter. + * + * @author yaohui + **/ +@EnableConfigurationProperties(SchedulerxProperties.class) +@ConditionalOnProperty(name = SchedulingConstants.SCHEDULING_CONFIG_DISTRIBUTED_MODE_KEY, havingValue = "schedulerx") +@Import({SchedulerxConfigurations.SchedulerxWorkerConfiguration.class, + SchedulerxConfigurations.SpringScheduleAdaptConfiguration.class}) +public class SchedulerxAutoConfigure { + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxConfigurations.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxConfigurations.java new file mode 100644 index 000000000..b2c92bcee --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxConfigurations.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx; + +import javax.annotation.PostConstruct; + +import com.alibaba.cloud.scheduling.schedulerx.service.JobSyncService; +import com.alibaba.cloud.scheduling.schedulerx.service.ScheduledJobSyncConfigurer; +import com.alibaba.schedulerx.common.util.ConfigUtil; +import com.alibaba.schedulerx.common.util.StringUtils; +import com.alibaba.schedulerx.worker.SchedulerxWorker; +import com.alibaba.schedulerx.worker.domain.WorkerConstants; +import com.alibaba.schedulerx.worker.log.LogFactory; +import com.alibaba.schedulerx.worker.log.Logger; +import com.alibaba.schedulerx.worker.processor.springscheduling.NoOpScheduler; +import com.alibaba.schedulerx.worker.processor.springscheduling.SchedulerxAnnotationBeanPostProcessor; +import com.alibaba.schedulerx.worker.processor.springscheduling.SchedulerxSchedulingConfigurer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; + +/** + * @author yaohui + **/ +public class SchedulerxConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SchedulerxWorker.class) + @ConditionalOnProperty(prefix = SchedulerxProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + static class SchedulerxWorkerConfiguration { + + private static final Logger LOGGER = LogFactory.getLogger(SchedulerxAutoConfigure.class); + + private static final String WORKER_STARTER_SPRING_CLOUD = "springcloud"; + + @Autowired + private SchedulerxProperties properties; + + @Bean + public JobSyncService jobSyncService() { + return new JobSyncService(); + } + + @PostConstruct + public void syncJobs() throws Exception { + if (!properties.getJobs().isEmpty()) { + LOGGER.info("{}.jobs is not empty, start to sync jobs...", SchedulerxProperties.CONFIG_PREFIX); + jobSyncService().syncJobs(); + LOGGER.info("sync jobs finished."); + } + } + + @Bean + public SchedulerxWorker schedulerxWorker() { + SchedulerxWorker schedulerxWorker = new SchedulerxWorker(); + schedulerxWorker.setDomainName(properties.getDomainName()); + schedulerxWorker.setGroupId(properties.getGroupId()); + schedulerxWorker.setEnableBatchWork(properties.isEnableBatchWork()); + schedulerxWorker.setDisableSites(properties.getDisableSites()); + schedulerxWorker.setEnableSites(properties.getEnableSites()); + schedulerxWorker.setDisableUnits(properties.getDisableUnits()); + schedulerxWorker.setEnableUnits(properties.getEnableUnits()); + schedulerxWorker.setAppKey(properties.getAppKey()); + schedulerxWorker.setAliyunAccessKey(properties.getAliyunAccessKey()); + schedulerxWorker.setAliyunSecretKey(properties.getAliyunSecretKey()); + schedulerxWorker.setNamespace(properties.getNamespace()); + schedulerxWorker.setHost(properties.getHost()); + schedulerxWorker.setPort(properties.getPort()); + schedulerxWorker.setEndpoint(properties.getEndpoint()); + schedulerxWorker.setNamespaceSource(properties.getNamespaceSource()); + schedulerxWorker.setMaxTaskBodySize(properties.getMaxTaskBodySize()); + schedulerxWorker.setBlockAppStart(properties.isBlockAppStart()); + schedulerxWorker.setSTSAccessKey(properties.getStsAccessKey()); + schedulerxWorker.setSTSSecretKey(properties.getStsSecretKey()); + schedulerxWorker.setSTSSecretToken(properties.getStsToken()); + schedulerxWorker.setSlsCollectorEnable(properties.isSlsCollectorEnable()); + schedulerxWorker.setShareContainerPool(properties.isShareContainerPool()); + schedulerxWorker.setThreadPoolMode(properties.getThreadPoolMode()); + schedulerxWorker.setLabel(properties.getLabel()); + schedulerxWorker.setLabelPath(properties.getLabelPath()); + if (properties.isShareContainerPool() || WorkerConstants.THREAD_POOL_MODE_ALL.equals(properties.getThreadPoolMode())) { + schedulerxWorker.setSharePoolSize(properties.getSharePoolSize()); + schedulerxWorker.setSharePoolQueueSize(properties.getSharePoolQueueSize()); + } + if (StringUtils.isNotEmpty(properties.getEndpointPort())) { + schedulerxWorker.setEndpointPort(Integer.parseInt(properties.getEndpointPort())); + } + schedulerxWorker.setEnableCgroupMetrics(properties.isEnableCgroupMetrics()); + if (properties.isEnableCgroupMetrics()) { + schedulerxWorker.setCgroupPathPrefix(properties.getCgroupPathPrefix()); + } + if (StringUtils.isNotEmpty(properties.getNamespaceSource())) { + schedulerxWorker.setNamespaceSource(properties.getNamespaceSource()); + } + schedulerxWorker.setAkkaRemotingAutoRecover(properties.isAkkaRemotingAutoRecover()); + schedulerxWorker.setEnableHeartbeatLog(properties.isEnableHeartbeatLog()); + schedulerxWorker.setMapMasterStatusCheckInterval(properties.getMapMasterStatusCheckInterval()); + schedulerxWorker.setEnableSecondDelayCycleIntervalMs(properties.isEnableSecondDealyCycleIntervalMs()); + schedulerxWorker.setEnableMapMasterFailover(properties.isEnableMapMasterFailover()); + schedulerxWorker.setEnableSecondDelayStandaloneDispatch(properties.isEnableSecondDelayStandaloneDispatch()); + schedulerxWorker.setPageSize(properties.getPageSize()); + schedulerxWorker.setGraceShutdownMode(properties.getGraceShutdownMode()); + if (properties.getGraceShutdownTimeout() > 0) { + schedulerxWorker.setGraceShutdownTimeout(properties.getGraceShutdownTimeout()); + } + schedulerxWorker.setBroadcastDispatchThreadNum(properties.getBroadcastDispatchThreadNum()); + schedulerxWorker.setBroadcastDispatchThreadEnable(properties.isBroadcastDispatchThreadEnable()); + schedulerxWorker.setBroadcastMasterExecEnable(properties.isBroadcastMasterExecEnable()); + schedulerxWorker.setBroadcastDispatchRetryTimes(properties.getBroadcastDispatchRetryTimes()); + schedulerxWorker.setProcessorPoolSize(properties.getProcessorPoolSize()); + schedulerxWorker.setMapMasterDispatchRandom(properties.isMapMasterDispatchRandom()); + schedulerxWorker.setMapMasterRouterStrategy(properties.getMapMasterRouterStrategy()); + if (StringUtils.isNotEmpty(properties.getH2DatabaseUser())) { + schedulerxWorker.setH2DatabaseUser(properties.getH2DatabaseUser()); + } + if (StringUtils.isNotEmpty(properties.getH2DatabasePassword())) { + schedulerxWorker.setH2DatabasePassword(properties.getH2DatabasePassword()); + } + schedulerxWorker.setHttpServerEnable(properties.getHttpServerEnable()); + schedulerxWorker.setHttpServerPort(properties.getHttpServerPort()); + if (properties.getMaxMapDiskPercent() != null) { + schedulerxWorker.setMaxMapDiskPercent(properties.getMaxMapDiskPercent()); + } + + ConfigUtil.getWorkerConfig().setProperty(WorkerConstants.WORKER_STARTER_MODE, + WORKER_STARTER_SPRING_CLOUD); + return schedulerxWorker; + } + } + + + @Configuration(proxyBeanMethods = false) + @AutoConfigureAfter(SchedulerxWorkerConfiguration.class) + @ConditionalOnBean(SchedulerxWorker.class) + static class SpringScheduleAdaptConfiguration { + + @Bean(ScheduledAnnotationBeanPostProcessor.DEFAULT_TASK_SCHEDULER_BEAN_NAME) + public NoOpScheduler noOpScheduler() { + return new NoOpScheduler(); + } + + @Bean + public SchedulerxSchedulingConfigurer schedulerxSchedulingConfigurer() { + return new SchedulerxSchedulingConfigurer(); + } + + @Bean + public SchedulerxAnnotationBeanPostProcessor schedulerxAnnotationBeanPostProcessor() { + return new SchedulerxAnnotationBeanPostProcessor(); + } + + @Bean + @ConditionalOnProperty(prefix = SchedulerxProperties.CONFIG_PREFIX, name = "task-sync", havingValue = "true") + public ScheduledJobSyncConfigurer scheduledJobSyncConfigurer() { + return new ScheduledJobSyncConfigurer(); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxProperties.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxProperties.java new file mode 100644 index 000000000..d5fd9afeb --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/SchedulerxProperties.java @@ -0,0 +1,786 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.alibaba.cloud.scheduling.SchedulingConstants; +import com.alibaba.schedulerx.common.domain.ContactInfo; +import com.alibaba.schedulerx.common.util.JsonUtil; +import com.alibaba.schedulerx.worker.domain.WorkerConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * schedulerx worker properties. + * + * @author yaohui + **/ +@ConfigurationProperties(prefix = SchedulerxProperties.CONFIG_PREFIX) +public class SchedulerxProperties implements InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(SchedulerxProperties.class); + + /** + * schedulerx config prefix. + */ + public static final String CONFIG_PREFIX = SchedulingConstants.SCHEDULING_CONFIG_PREFIX + ".schedulerx"; + + /** + * domainName. + */ + private String domainName; + + /** + * groupId. + */ + private String groupId; + + /** + * host. + */ + private String host; + + /** + * client port. + */ + private int port = 0; + + private String enableUnits; + + private String disableUnits; + + private String enableSites; + + private String disableSites; + + private boolean enableBatchWork; + + /** + * enabled: true; false. + */ + private boolean enabled = true; + + /** + * appName. + */ + private String appName; + + /** + * appKey. + */ + private String appKey; + + /** + * aliyunRamRole. + */ + private String aliyunRamRole; + + /** + * aliyunAccessKey. + */ + private String aliyunAccessKey; + /** + * aliyunSecretKey. + */ + private String aliyunSecretKey; + + /** + * STS ak. + */ + private String stsAccessKey; + + /** + * STS sk. + */ + private String stsSecretKey; + + /** + * STS secret token. + */ + private String stsToken; + + /** + * Namespace UID. + */ + private String namespace; + + /** + * endpoint. + */ + private String endpoint; + + /** + * endpointPort. + */ + private String endpointPort; + + /** + * namespaceName. + */ + private String namespaceName; + + /** + * namespaceSource. + */ + private String namespaceSource; + + /** + * maxTaskBodySize (byte). + */ + private int maxTaskBodySize = WorkerConstants.TASK_BODY_SIZE_MAX_DEFAULT; + + private boolean blockAppStart = true; + + /** + * slsCollectorEnable. + */ + private boolean slsCollectorEnable = true; + + /** + * shareContainerPool. + */ + private boolean shareContainerPool = false; + + /** + * threadPoolMode. + */ + private String threadPoolMode; + + /** + * sharePoolSize. + */ + private int sharePoolSize = WorkerConstants.SHARE_POOL_SIZE_DEFAULT; + + /** + * sharePoolQueueSize. + */ + private int sharePoolQueueSize = Integer.MAX_VALUE; + + /** + * Wlabel. + */ + private String label; + + private String labelPath = "/etc/podinfo/annotations"; + + /** + * enableCgroupMetrics. + */ + private boolean enableCgroupMetrics = false; + + /** + * cgroupPathPrefix. + */ + private String cgroupPathPrefix = "/sys/fs/cgroup/cpu/"; + + /** + * akkaRemotingAutoRecover. + */ + private boolean akkaRemotingAutoRecover = true; + + /** + * enableHeartbeatLog. + */ + private boolean enableHeartbeatLog = true; + + /** + * mapMasterStatusCheckInterval(ms). + */ + private int mapMasterStatusCheckInterval = WorkerConstants.Map_MASTER_STATUS_CHECK_INTERVAL_DEFAULT; + + /** + * enableSecondDealyCycleIntervalMs. + */ + private boolean enableSecondDealyCycleIntervalMs = false; + + /** + * enableMapMasterFailover. + */ + private boolean enableMapMasterFailover = true; + + /** + * enableSecondDelayStandaloneDispatch. + */ + private boolean enableSecondDelayStandaloneDispatch = false; + + /** + * pageSize. + */ + private int pageSize = 1000; + + /** + * GraceShutdownMode(WAIT_ALL; WAIT_RUNNING;). + */ + private String graceShutdownMode; + + /** + * graceShutdownTimeout. + */ + private long graceShutdownTimeout = WorkerConstants.GRACE_SHUTDOWN_TIMEOUT_DEFAULT; + + /** + * broadcastDispatchThreadNum. + */ + private int broadcastDispatchThreadNum = 4; + + /** + * broadcastDispatchRetryTimes. + */ + private int broadcastDispatchRetryTimes = 1; + + /** + * broadcastDispatchThreadEnable. + */ + private boolean broadcastDispatchThreadEnable = false; + + /** + * broadcastMasterExecEnable. + */ + private boolean broadcastMasterExecEnable = true; + + /** + * mapMasterDispatchRandom. + */ + private boolean mapMasterDispatchRandom = false; + + private Integer mapMasterRouterStrategy; + + private String regionId; + + /** + * h2DatabaseUser. + */ + private String h2DatabaseUser; + + /** + * h2DatabasePassword. + */ + private String h2DatabasePassword; + + /** + * httpServerEnable. + */ + private Boolean httpServerEnable; + + /** + * httpServerPort. + */ + private Integer httpServerPort; + + /** + * maxMapDiskPercent. + */ + private Float maxMapDiskPercent; + + private Map jobs = new LinkedHashMap<>(); + + private String alarmChannel; + + private Map alarmUsers = new LinkedHashMap<>(); + + private Map processorPoolSize = new HashMap<>(); + + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getEnableUnits() { + return enableUnits; + } + + public void setEnableUnits(String enableUnits) { + this.enableUnits = enableUnits; + } + + public String getDisableUnits() { + return disableUnits; + } + + public void setDisableUnits(String disableUnits) { + this.disableUnits = disableUnits; + } + + public String getEnableSites() { + return enableSites; + } + + public void setEnableSites(String enableSites) { + this.enableSites = enableSites; + } + + public String getDisableSites() { + return disableSites; + } + + public void setDisableSites(String disableSites) { + this.disableSites = disableSites; + } + + public boolean isEnableBatchWork() { + return enableBatchWork; + } + + public void setEnableBatchWork(boolean enableBatchWork) { + this.enableBatchWork = enableBatchWork; + } + + public String getAliyunAccessKey() { + return aliyunAccessKey; + } + + public void setAliyunAccessKey(String aliyunAccessKey) { + this.aliyunAccessKey = aliyunAccessKey; + } + + public String getAliyunSecretKey() { + return aliyunSecretKey; + } + + public void setAliyunSecretKey(String aliyunSecretKey) { + this.aliyunSecretKey = aliyunSecretKey; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getEndpointPort() { + return endpointPort; + } + + public void setEndpointPort(String endpointPort) { + this.endpointPort = endpointPort; + } + + public String getNamespaceName() { + return namespaceName; + } + + public void setNamespaceName(String namespaceName) { + this.namespaceName = namespaceName; + } + + public String getNamespaceSource() { + return namespaceSource; + } + + public void setNamespaceSource(String namespaceSource) { + this.namespaceSource = namespaceSource; + } + + public int getMaxTaskBodySize() { + return maxTaskBodySize; + } + + public void setMaxTaskBodySize(int maxTaskBodySize) { + this.maxTaskBodySize = maxTaskBodySize; + } + + public boolean isBlockAppStart() { + return blockAppStart; + } + + public void setBlockAppStart(boolean blockAppStart) { + this.blockAppStart = blockAppStart; + } + + public String getAppName() { + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getAppKey() { + return appKey; + } + + public void setAppKey(String appKey) { + this.appKey = appKey; + } + + public String getStsAccessKey() { + return stsAccessKey; + } + + public void setStsAccessKey(String stsAccessKey) { + this.stsAccessKey = stsAccessKey; + } + + public String getStsSecretKey() { + return stsSecretKey; + } + + public void setStsSecretKey(String stsSecretKey) { + this.stsSecretKey = stsSecretKey; + } + + public String getStsToken() { + return stsToken; + } + + public String getAliyunRamRole() { + return aliyunRamRole; + } + + public void setAliyunRamRole(String aliyunRamRole) { + this.aliyunRamRole = aliyunRamRole; + } + + public void setStsToken(String stsToken) { + this.stsToken = stsToken; + } + + public boolean isSlsCollectorEnable() { + return slsCollectorEnable; + } + + public void setSlsCollectorEnable(boolean slsCollectorEnable) { + this.slsCollectorEnable = slsCollectorEnable; + } + + public boolean isShareContainerPool() { + return shareContainerPool; + } + + public void setShareContainerPool(boolean shareContainerPool) { + this.shareContainerPool = shareContainerPool; + } + + public int getSharePoolSize() { + return sharePoolSize; + } + + public void setSharePoolSize(int sharePoolSize) { + this.sharePoolSize = sharePoolSize; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + if (label != null) { + if (label.startsWith("#") && label.endsWith("#")) { + String labelKey = label.substring(1, label.length() - 1); + this.label = System.getenv(labelKey); + return; + } + } + this.label = label; + } + + public String getLabelPath() { + return labelPath; + } + + public void setLabelPath(String labelPath) { + this.labelPath = labelPath; + } + + public boolean isEnableCgroupMetrics() { + return enableCgroupMetrics; + } + + public void setEnableCgroupMetrics(boolean enableCgroupMetrics) { + this.enableCgroupMetrics = enableCgroupMetrics; + } + + public String getCgroupPathPrefix() { + return cgroupPathPrefix; + } + + public void setCgroupPathPrefix(String cgroupPathPrefix) { + this.cgroupPathPrefix = cgroupPathPrefix; + } + + public boolean isAkkaRemotingAutoRecover() { + return akkaRemotingAutoRecover; + } + + public void setAkkaRemotingAutoRecover(boolean akkaRemotingAutoRecover) { + this.akkaRemotingAutoRecover = akkaRemotingAutoRecover; + } + + public boolean isEnableHeartbeatLog() { + return enableHeartbeatLog; + } + + public void setEnableHeartbeatLog(boolean enableHeartbeatLog) { + this.enableHeartbeatLog = enableHeartbeatLog; + } + + public int getMapMasterStatusCheckInterval() { + return mapMasterStatusCheckInterval; + } + + public void setMapMasterStatusCheckInterval(int mapMasterStatusCheckInterval) { + this.mapMasterStatusCheckInterval = mapMasterStatusCheckInterval; + } + + public boolean isEnableSecondDealyCycleIntervalMs() { + return enableSecondDealyCycleIntervalMs; + } + + public void setEnableSecondDealyCycleIntervalMs(boolean enableSecondDealyCycleIntervalMs) { + this.enableSecondDealyCycleIntervalMs = enableSecondDealyCycleIntervalMs; + } + + public boolean isEnableMapMasterFailover() { + return enableMapMasterFailover; + } + + public void setEnableMapMasterFailover(boolean enableMapMasterFailover) { + this.enableMapMasterFailover = enableMapMasterFailover; + } + + public boolean isEnableSecondDelayStandaloneDispatch() { + return enableSecondDelayStandaloneDispatch; + } + + public void setEnableSecondDelayStandaloneDispatch(boolean enableSecondDelayStandaloneDispatch) { + this.enableSecondDelayStandaloneDispatch = enableSecondDelayStandaloneDispatch; + } + + public int getPageSize() { + return pageSize; + } + + public String getGraceShutdownMode() { + return graceShutdownMode; + } + + public void setGraceShutdownMode(String graceShutdownMode) { + this.graceShutdownMode = graceShutdownMode; + } + + public long getGraceShutdownTimeout() { + return graceShutdownTimeout; + } + + public void setGraceShutdownTimeout(long graceShutdownTimeout) { + this.graceShutdownTimeout = graceShutdownTimeout; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(String regionId) { + this.regionId = regionId; + } + + public Map getJobs() { + return jobs; + } + + public void setJobs(Map jobs) { + this.jobs = jobs; + } + + public String getAlarmChannel() { + return alarmChannel; + } + + public void setAlarmChannel(String alarmChannel) { + this.alarmChannel = alarmChannel; + } + + public Map getAlarmUsers() { + return alarmUsers; + } + + public void setAlarmUsers(Map alarmUsers) { + this.alarmUsers = alarmUsers; + } + + public int getBroadcastDispatchThreadNum() { + return broadcastDispatchThreadNum; + } + + public void setBroadcastDispatchThreadNum(int broadcastDispatchThreadNum) { + this.broadcastDispatchThreadNum = broadcastDispatchThreadNum; + } + + public boolean isBroadcastDispatchThreadEnable() { + return broadcastDispatchThreadEnable; + } + + public void setBroadcastDispatchThreadEnable(boolean broadcastDispatchThreadEnable) { + this.broadcastDispatchThreadEnable = broadcastDispatchThreadEnable; + } + + public String getThreadPoolMode() { + return threadPoolMode; + } + + public void setThreadPoolMode(String threadPoolMode) { + this.threadPoolMode = threadPoolMode; + } + + public Map getProcessorPoolSize() { + return processorPoolSize; + } + + public void setProcessorPoolSize(Map processorPoolSize) { + this.processorPoolSize = processorPoolSize; + } + + public int getSharePoolQueueSize() { + return sharePoolQueueSize; + } + + public void setSharePoolQueueSize(int sharePoolQueueSize) { + this.sharePoolQueueSize = sharePoolQueueSize; + } + + public boolean isMapMasterDispatchRandom() { + return mapMasterDispatchRandom; + } + + public void setMapMasterDispatchRandom(boolean mapMasterDispatchRandom) { + this.mapMasterDispatchRandom = mapMasterDispatchRandom; + } + + public boolean isBroadcastMasterExecEnable() { + return broadcastMasterExecEnable; + } + + public void setBroadcastMasterExecEnable(boolean broadcastMasterExecEnable) { + this.broadcastMasterExecEnable = broadcastMasterExecEnable; + } + + public int getBroadcastDispatchRetryTimes() { + return broadcastDispatchRetryTimes; + } + + public void setBroadcastDispatchRetryTimes(int broadcastDispatchRetryTimes) { + this.broadcastDispatchRetryTimes = broadcastDispatchRetryTimes; + } + + public Integer getMapMasterRouterStrategy() { + return mapMasterRouterStrategy; + } + + public void setMapMasterRouterStrategy(Integer mapMasterRouterStrategy) { + this.mapMasterRouterStrategy = mapMasterRouterStrategy; + } + + public String getH2DatabaseUser() { + return h2DatabaseUser; + } + + public void setH2DatabaseUser(String h2DatabaseUser) { + this.h2DatabaseUser = h2DatabaseUser; + } + + public String getH2DatabasePassword() { + return h2DatabasePassword; + } + + public void setH2DatabasePassword(String h2DatabasePassword) { + this.h2DatabasePassword = h2DatabasePassword; + } + + public Boolean getHttpServerEnable() { + return httpServerEnable; + } + + public void setHttpServerEnable(Boolean httpServerEnable) { + this.httpServerEnable = httpServerEnable; + } + + public Integer getHttpServerPort() { + return httpServerPort; + } + + public void setHttpServerPort(Integer httpServerPort) { + this.httpServerPort = httpServerPort; + } + + public Float getMaxMapDiskPercent() { + return maxMapDiskPercent; + } + + public void setMaxMapDiskPercent(float maxMapDiskPercent) { + this.maxMapDiskPercent = maxMapDiskPercent; + } + + @Override + public void afterPropertiesSet() { + logger.info("SchedulerxProperties->" + JsonUtil.toJson(this)); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/constants/SchedulerxConstants.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/constants/SchedulerxConstants.java new file mode 100644 index 000000000..0710b1ba8 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/constants/SchedulerxConstants.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx.constants; + +/** + * Schedulerx constants. + * + * @author yaohui + */ +public final class SchedulerxConstants { + + /** + * Schedulerx default namespace source. + */ + public static final String NAMESPACE_SOURCE_SPRINGBOOT = "springboot"; + + /** + * Aliyun pop product. + */ + public static final String ALIYUN_POP_PRODUCT = "schedulerx2"; + + /** + * Aliyun pop endpoint. + */ + public static final String ALIYUN_POP_SCHEDULERX_ENDPOINT = "schedulerx.aliyuncs.com"; + + /** + * Second delay max value. + */ + public static final int SECOND_DELAY_MAX_VALUE = 60; + + /** + * Second delay min value. + */ + public static final int SECOND_DELAY_MIN_VALUE = 1; + + /** + * Job timeout default value. + */ + public static final long JOB_TIMEOUT_DEFAULT = 3600L; + + /** + * Job retry count default value. + */ + public static final int JOB_RETRY_COUNT_DEFAULT = 3; + + /** + * Job retry interval default value. + */ + public static final int JOB_RETRY_INTERVAL_DEFAULT = 30; + + /** + * Job alarm channel default value. + */ + public static final String JOB_ALARM_CHANNEL_DEFAULT = "default"; + + /** + * Job model mapreduce alias. + */ + public static final String JOB_MODEL_MAPREDUCE_ALIAS = "mapreduce"; + + @Override + public String toString() { + return super.toString(); + } + + private SchedulerxConstants() { + throw new AssertionError("Must not instantiate constant utility class"); + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/JobSyncService.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/JobSyncService.java new file mode 100644 index 000000000..67e6449fc --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/JobSyncService.java @@ -0,0 +1,435 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import com.alibaba.cloud.scheduling.schedulerx.JobProperty; +import com.alibaba.cloud.scheduling.schedulerx.SchedulerxProperties; +import com.alibaba.cloud.scheduling.schedulerx.constants.SchedulerxConstants; +import com.alibaba.cloud.scheduling.schedulerx.util.CronExpression; +import com.alibaba.schedulerx.common.domain.ContactInfo; +import com.alibaba.schedulerx.common.domain.ExecuteMode; +import com.alibaba.schedulerx.common.domain.JobType; +import com.alibaba.schedulerx.common.domain.TimeType; +import com.alibaba.schedulerx.common.sdk.common.MonitorConfig; +import com.alibaba.schedulerx.common.util.JsonUtil; +import com.alibaba.schedulerx.common.util.StringUtils; +import com.alibaba.schedulerx.worker.log.LogFactory; +import com.alibaba.schedulerx.worker.log.Logger; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.auth.InstanceProfileCredentialsProvider; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.schedulerx2.model.v20190430.CreateAppGroupRequest; +import com.aliyuncs.schedulerx2.model.v20190430.CreateAppGroupResponse; +import com.aliyuncs.schedulerx2.model.v20190430.CreateJobRequest; +import com.aliyuncs.schedulerx2.model.v20190430.CreateJobResponse; +import com.aliyuncs.schedulerx2.model.v20190430.CreateNamespaceRequest; +import com.aliyuncs.schedulerx2.model.v20190430.CreateNamespaceResponse; +import com.aliyuncs.schedulerx2.model.v20190430.GetJobInfoRequest; +import com.aliyuncs.schedulerx2.model.v20190430.GetJobInfoResponse; +import com.aliyuncs.schedulerx2.model.v20190430.GetJobInfoResponse.Data.JobConfigInfo; +import com.aliyuncs.schedulerx2.model.v20190430.UpdateJobRequest; +import com.aliyuncs.schedulerx2.model.v20190430.UpdateJobResponse; + +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * JobSyncService. + * + * @author xiaomeng.hxm + */ +public class JobSyncService { + + private static final Logger logger = LogFactory.getLogger(JobSyncService.class); + + @Autowired + private SchedulerxProperties properties; + + private DefaultAcsClient client; + + private synchronized DefaultAcsClient getClient() { + // build aliyun pop client + if (client == null) { + DefaultProfile.addEndpoint(properties.getRegionId(), SchedulerxConstants.ALIYUN_POP_PRODUCT, SchedulerxConstants.ALIYUN_POP_SCHEDULERX_ENDPOINT); + if (StringUtils.isNotEmpty(properties.getAliyunRamRole())) { + DefaultProfile profile = DefaultProfile.getProfile(properties.getRegionId()); + InstanceProfileCredentialsProvider provider = new InstanceProfileCredentialsProvider( + properties.getAliyunRamRole()); + client = new DefaultAcsClient(profile, provider); + } + else { + DefaultProfile defaultProfile = DefaultProfile.getProfile(properties.getRegionId(), + properties.getAliyunAccessKey(), + properties.getAliyunSecretKey()); + client = new DefaultAcsClient(defaultProfile); + } + } + return client; + } + + /** + * Sync job config. + * + * @param jobs job configs + * @param namespaceSource namespace source + * @throws Exception sync job config exception + */ + public synchronized void syncJobs(Map jobs, String namespaceSource) throws Exception { + DefaultAcsClient client = getClient(); + for (Entry entry : jobs.entrySet()) { + String jobName = entry.getKey(); + JobProperty jobProperty = entry.getValue(); + JobConfigInfo jobConfigInfo = getJob(client, jobName, namespaceSource); + if (jobConfigInfo == null) { + createJob(client, jobName, jobProperty, namespaceSource); + } + else if (jobProperty.isOverwrite()) { + updateJob(client, jobConfigInfo, jobProperty, namespaceSource); + } + } + } + + /** + * sync jobs. + * + * @throws Exception sync jobs exception + */ + public void syncJobs() throws Exception { + // 1. create namespace + if (syncNamespace(getClient())) { + // 2. create app group + if (syncAppGroup(getClient())) { + syncJobs(properties.getJobs(), getNamespaceSource()); + properties.setNamespaceSource(getNamespaceSource()); + } + } + } + + /** + * sync namespace. + * + * @param client pop client + * @return true if success + * @throws Exception sync namespace exception + */ + public boolean syncNamespace(DefaultAcsClient client) throws Exception { + if (StringUtils.isEmpty(properties.getNamespace())) { + logger.error("please set {}.namespace", SchedulerxProperties.CONFIG_PREFIX); + throw new IOException(String.format("please set %s.namespace", SchedulerxProperties.CONFIG_PREFIX)); + } + + if (StringUtils.isEmpty(properties.getNamespaceName())) { + logger.error("please set {}.namespaceName", SchedulerxProperties.CONFIG_PREFIX); + throw new IOException(String.format("please set %s.namespaceName", SchedulerxProperties.CONFIG_PREFIX)); + } + + CreateNamespaceRequest request = new CreateNamespaceRequest(); + request.setUid(properties.getNamespace()); + request.setName(properties.getNamespaceName()); + request.setSource(getNamespaceSource()); + CreateNamespaceResponse response = client.getAcsResponse(request); + if (response.getSuccess()) { + logger.info(JsonUtil.toJson(response)); + return true; + } + else { + throw new IOException(response.getMessage()); + } + + } + + /** + * sync app group. + * + * @param client pop client + * @return sync app group result + * @throws IOException sync app group exception. + * @throws ClientException sync app group pop client exception. + */ + public boolean syncAppGroup(DefaultAcsClient client) throws IOException, ClientException { + if (StringUtils.isEmpty(properties.getAppName())) { + logger.error("please set {}.appName", SchedulerxProperties.CONFIG_PREFIX); + throw new IOException(String.format("please set %s.appName", SchedulerxProperties.CONFIG_PREFIX)); + } + + if (StringUtils.isEmpty(properties.getAppKey())) { + logger.error("please set {}.appKey", SchedulerxProperties.CONFIG_PREFIX); + throw new IOException(String.format("please set %s.appKey", SchedulerxProperties.CONFIG_PREFIX)); + } + + if (StringUtils.isEmpty(properties.getGroupId())) { + logger.error("please set {}.groupId", SchedulerxProperties.CONFIG_PREFIX); + throw new IOException(String.format("please set %s.groupId", SchedulerxProperties.CONFIG_PREFIX)); + } + + CreateAppGroupRequest request = new CreateAppGroupRequest(); + request.setNamespace(properties.getNamespace()); + request.setNamespaceSource(getNamespaceSource()); + request.setAppName(properties.getAppName()); + request.setGroupId(properties.getGroupId()); + request.setAppKey(properties.getAppKey()); + if (StringUtils.isNotEmpty(properties.getAlarmChannel())) { + MonitorConfig monitorConfig = new MonitorConfig(); + monitorConfig.setSendChannel(properties.getAlarmChannel()); + request.setMonitorConfigJson(JsonUtil.toJson(monitorConfig)); + } + if (!properties.getAlarmUsers().isEmpty()) { + List contactInfos = new ArrayList(properties.getAlarmUsers().values()); + request.setMonitorContactsJson(JsonUtil.toJson(contactInfos)); + } + CreateAppGroupResponse response = client.getAcsResponse(request); + if (response.getSuccess()) { + logger.info(JsonUtil.toJson(response)); + return true; + } + else { + throw new IOException(response.getMessage()); + } + } + + /** + * Get job config info. + * + * @param client pop client + * @param jobName job name + * @param namespaceSource namespace source + * @return job config info. + * @throws Exception get job config info exception. + */ + private JobConfigInfo getJob(DefaultAcsClient client, String jobName, String namespaceSource) throws Exception { + GetJobInfoRequest request = new GetJobInfoRequest(); + request.setNamespace(properties.getNamespace()); + request.setNamespaceSource(namespaceSource); + request.setGroupId(properties.getGroupId()); + request.setJobId(0L); + request.setJobName(jobName); + GetJobInfoResponse response = client.getAcsResponse(request); + if (response.getSuccess()) { + return response.getData().getJobConfigInfo(); + } + return null; + } + + /** + * create job. + * + * @param client pop client + * @param jobName job name + * @param jobProperty job property + * @param namespaceSource namespace source + * @throws Exception create job exception + */ + private void createJob(DefaultAcsClient client, String jobName, JobProperty jobProperty, String namespaceSource) throws Exception { + CreateJobRequest request = new CreateJobRequest(); + request.setNamespace(properties.getNamespace()); + request.setNamespaceSource(namespaceSource); + request.setGroupId(properties.getGroupId()); + request.setName(jobName); + request.setParameters(jobProperty.getJobParameter()); + + if (jobProperty.getJobType().equals(JobType.JAVA.getKey())) { + request.setJobType(JobType.JAVA.getKey()); + request.setClassName(jobProperty.getClassName()); + } + else { + request.setJobType(jobProperty.getJobType()); + } + + if (SchedulerxConstants.JOB_MODEL_MAPREDUCE_ALIAS.equals(jobProperty.getJobModel())) { + request.setExecuteMode(ExecuteMode.BATCH.getKey()); + } + else { + request.setExecuteMode(jobProperty.getJobModel()); + } + if (StringUtils.isNotEmpty(jobProperty.getDescription())) { + request.setDescription(jobProperty.getDescription()); + } + + if (StringUtils.isNotEmpty(jobProperty.getContent())) { + request.setContent(jobProperty.getContent()); + } + + if (StringUtils.isNotEmpty(jobProperty.getCron()) && StringUtils.isNotEmpty(jobProperty.getOneTime())) { + throw new IOException("cron and oneTime shouldn't set together"); + } + if (StringUtils.isNotEmpty(jobProperty.getCron())) { + CronExpression cronExpression = new CronExpression(jobProperty.getCron()); + Date now = new Date(); + Date nextData = cronExpression.getTimeAfter(now); + Date next2Data = cronExpression.getTimeAfter(nextData); + if (nextData != null && next2Data != null) { + long interval = TimeUnit.MILLISECONDS.toSeconds((next2Data.getTime() - nextData.getTime())); + if (interval < SchedulerxConstants.SECOND_DELAY_MAX_VALUE) { + request.setTimeType(TimeType.SECOND_DELAY.getValue()); + request.setTimeExpression(String.valueOf(interval < SchedulerxConstants.SECOND_DELAY_MIN_VALUE ? + SchedulerxConstants.SECOND_DELAY_MIN_VALUE : interval)); + } + else { + request.setTimeType(TimeType.CRON.getValue()); + request.setTimeExpression(jobProperty.getCron()); + } + } + else { + request.setTimeType(TimeType.CRON.getValue()); + request.setTimeExpression(jobProperty.getCron()); + } + } + else if (StringUtils.isNotEmpty(jobProperty.getOneTime())) { + request.setTimeType(TimeType.ONE_TIME.getValue()); + request.setTimeExpression(jobProperty.getOneTime()); + } + else { + request.setTimeType(TimeType.API.getValue()); + } + + if (jobProperty.getTimeType() != null) { + request.setTimeType(jobProperty.getTimeType()); + if (StringUtils.isNotEmpty(jobProperty.getTimeExpression())) { + request.setTimeExpression(jobProperty.getTimeExpression()); + } + } + + request.setTimeoutEnable(true); + request.setTimeoutKillEnable(true); + request.setSendChannel(SchedulerxConstants.JOB_ALARM_CHANNEL_DEFAULT); + request.setFailEnable(true); + request.setTimeout(SchedulerxConstants.JOB_TIMEOUT_DEFAULT); + request.setMaxAttempt(SchedulerxConstants.JOB_RETRY_COUNT_DEFAULT); + request.setAttemptInterval(SchedulerxConstants.JOB_RETRY_INTERVAL_DEFAULT); + CreateJobResponse response = client.getAcsResponse(request); + if (response.getSuccess()) { + logger.info("create schedulerx job successfully, jobId={}, jobName={}", response.getData().getJobId(), jobName); + } + else { + throw new IOException("create schedulerx job failed, jobName=" + jobName + ", message=" + response.getMessage()); + } + } + + /** + * update job. + * + * @param client pop client + * @param jobConfigInfo job config info + * @param jobProperty job property + * @param namespaceSource namespace source + * @throws Exception update job exception + */ + private void updateJob(DefaultAcsClient client, JobConfigInfo jobConfigInfo, JobProperty jobProperty, String namespaceSource) throws Exception { + String executeMode = jobProperty.getJobModel(); + if (SchedulerxConstants.JOB_MODEL_MAPREDUCE_ALIAS.equals(jobProperty.getJobModel())) { + executeMode = ExecuteMode.BATCH.getKey(); + } + int timeType; + String timeExpression = null; + if (StringUtils.isNotEmpty(jobProperty.getCron()) && StringUtils.isNotEmpty(jobProperty.getOneTime())) { + throw new IOException("cron and oneTime shouldn't set together"); + } + if (StringUtils.isNotEmpty(jobProperty.getCron())) { + CronExpression cronExpression = new CronExpression(jobProperty.getCron()); + Date now = new Date(); + Date nextData = cronExpression.getTimeAfter(now); + Date next2Data = cronExpression.getTimeAfter(nextData); + if (nextData != null && next2Data != null) { + long interval = TimeUnit.MILLISECONDS.toSeconds((next2Data.getTime() - nextData.getTime())); + if (interval < SchedulerxConstants.SECOND_DELAY_MAX_VALUE) { + timeType = TimeType.SECOND_DELAY.getValue(); + timeExpression = String.valueOf(interval < SchedulerxConstants.SECOND_DELAY_MIN_VALUE ? + SchedulerxConstants.SECOND_DELAY_MIN_VALUE : interval); + } + else { + timeType = TimeType.CRON.getValue(); + timeExpression = jobProperty.getCron(); + } + } + else { + timeType = TimeType.CRON.getValue(); + timeExpression = jobProperty.getCron(); + } + } + else if (StringUtils.isNotEmpty(jobProperty.getOneTime())) { + timeType = TimeType.ONE_TIME.getValue(); + timeExpression = jobProperty.getOneTime(); + } + else { + timeType = TimeType.API.getValue(); + } + + if (!jobConfigInfo.getDescription().equals(jobProperty.getDescription()) + || !jobConfigInfo.getClassName().equals(jobProperty.getClassName()) + || !jobConfigInfo.getParameters().equals(jobProperty.getJobParameter()) + || !jobConfigInfo.getExecuteMode().equals(executeMode) + || jobConfigInfo.getTimeConfig().getTimeType() != timeType + || !jobConfigInfo.getTimeConfig().getTimeExpression().equals(timeExpression)) { + + UpdateJobRequest request = new UpdateJobRequest(); + request.setNamespace(properties.getNamespace()); + request.setNamespaceSource(namespaceSource); + request.setGroupId(properties.getGroupId()); + request.setJobId(jobConfigInfo.getJobId()); + request.setName(jobConfigInfo.getName()); + request.setParameters(jobProperty.getJobParameter()); + + //java任务 + if (jobProperty.getJobType().equals(JobType.JAVA.getKey())) { + request.setClassName(jobProperty.getClassName()); + } + request.setExecuteMode(executeMode); + if (StringUtils.isNotEmpty(jobProperty.getDescription())) { + request.setDescription(jobProperty.getDescription()); + } + request.setTimeType(timeType); + request.setTimeExpression(timeExpression); + + request.setTimeoutEnable(true); + request.setTimeoutKillEnable(true); + request.setSendChannel(SchedulerxConstants.JOB_ALARM_CHANNEL_DEFAULT); + request.setFailEnable(true); + request.setTimeout(SchedulerxConstants.JOB_TIMEOUT_DEFAULT); + request.setMaxAttempt(SchedulerxConstants.JOB_RETRY_COUNT_DEFAULT); + request.setAttemptInterval(SchedulerxConstants.JOB_RETRY_INTERVAL_DEFAULT); + + UpdateJobResponse response = client.getAcsResponse(request); + if (response.getSuccess()) { + logger.info("update schedulerx job successfully, jobId={}, jobName={}", jobConfigInfo.getJobId(), jobConfigInfo.getName()); + } + else { + throw new IOException("update schedulerx job failed, jobName=" + jobConfigInfo.getName() + ", message=" + response.getMessage()); + } + } + } + + /** + * Get namespace source. + * + * @return namespace source + */ + private String getNamespaceSource() { + if (StringUtils.isEmpty(properties.getNamespaceSource())) { + return SchedulerxConstants.NAMESPACE_SOURCE_SPRINGBOOT; + } + return properties.getNamespaceSource(); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/ScheduledJobSyncConfigurer.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/ScheduledJobSyncConfigurer.java new file mode 100644 index 000000000..5f7075fdf --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/service/ScheduledJobSyncConfigurer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx.service; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.alibaba.cloud.scheduling.schedulerx.JobProperty; +import com.alibaba.cloud.scheduling.schedulerx.SchedulerxProperties; +import com.alibaba.schedulerx.common.domain.ExecuteMode; +import com.alibaba.schedulerx.common.domain.JobType; +import com.alibaba.schedulerx.common.domain.Pair; +import com.alibaba.schedulerx.common.domain.TimeType; +import com.alibaba.schedulerx.common.util.JsonUtil; +import com.alibaba.schedulerx.common.util.StringUtils; +import com.alibaba.schedulerx.scheduling.annotation.SchedulerX; +import com.alibaba.schedulerx.worker.domain.SpringScheduleProfile; +import com.alibaba.schedulerx.worker.log.LogFactory; +import com.alibaba.schedulerx.worker.log.Logger; +import com.alibaba.schedulerx.worker.processor.springscheduling.SchedulerxSchedulingConfigurer; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.Task; +import org.springframework.scheduling.support.ScheduledMethodRunnable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Spring scheduled job sync. + * + * @author yaohui + * @create 2022/8/17 下午2:21 + **/ +public class ScheduledJobSyncConfigurer implements SchedulingConfigurer { + + private static final Logger logger = LogFactory.getLogger(ScheduledJobSyncConfigurer.class); + + @Autowired + private JobSyncService jobSyncService; + + @Autowired + private SchedulerxProperties properties; + + @Autowired + private SchedulerxSchedulingConfigurer schedulerxSchedulingConfigurer; + + @Value("${" + SchedulerxProperties.CONFIG_PREFIX + ".task-overwrite:false}") + private Boolean overwrite = false; + + @Value("${" + SchedulerxProperties.CONFIG_PREFIX + ".task-model-default:broadcast}") + private String defaultModel = ExecuteMode.BROADCAST.getKey(); + + private boolean isValidModel(String mode) { + if (mode == null) { + return false; + } + return (ExecuteMode.BROADCAST.getKey().equals(mode) || ExecuteMode.STANDALONE.getKey().equals(mode)); + } + + private JobProperty convertToJobProperty(Task task, Object target, Method method) { + JobProperty jobProperty = new JobProperty(); + Class targetClass = AopProxyUtils.ultimateTargetClass(target); + if (ClassUtils.isCglibProxyClass(targetClass)) { + targetClass = ClassUtils.getUserClass(target); + } + String jobName = targetClass.getSimpleName() + "_" + method.getName(); + String model = this.defaultModel; + + if (task != null && task instanceof CronTask) { + String expression = ((CronTask) task).getExpression(); + jobProperty.setCron(expression); + } + + if (task != null && task instanceof IntervalTask) { + long interval = ((IntervalTask) task).getInterval() / 1000; + interval = interval < 1 ? 1 : interval; + if (interval < 60) { + jobProperty.setTimeType(TimeType.SECOND_DELAY.getValue()); + } + else { + jobProperty.setTimeType(TimeType.FIXED_RATE.getValue()); + } + jobProperty.setTimeExpression(String.valueOf(interval)); + } + + SchedulerX schedulerXMethod = AnnotatedElementUtils.getMergedAnnotation(method, SchedulerX.class); + if (schedulerXMethod != null) { + if (StringUtils.isNotEmpty(schedulerXMethod.name())) { + jobName = schedulerXMethod.name(); + } + if (isValidModel(schedulerXMethod.model())) { + model = schedulerXMethod.model(); + } + if (StringUtils.isNotEmpty(schedulerXMethod.cron())) { + jobProperty.setCron(schedulerXMethod.cron()); + } + if (schedulerXMethod.fixedRate() > 0) { + long interval = schedulerXMethod.timeUnit().toSeconds(schedulerXMethod.fixedRate()); + interval = interval < 1 ? 1 : interval; + if (interval < 60) { + jobProperty.setTimeType(TimeType.SECOND_DELAY.getValue()); + } + else { + jobProperty.setTimeType(TimeType.FIXED_RATE.getValue()); + } + jobProperty.setTimeExpression(String.valueOf(interval)); + } + } + + jobProperty.setJobName(jobName); + jobProperty.setJobType(JobType.SPRINGSCHEDULE.getKey()); + jobProperty.setJobModel(model); + SpringScheduleProfile profile = new SpringScheduleProfile(); + profile.setClassName(targetClass.getName()); + profile.setMethod(method.getName()); + jobProperty.setContent(JsonUtil.toJson(profile)); + jobProperty.setOverwrite(overwrite); + return jobProperty; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + logger.info("spring scheduled job is not empty, start to sync jobs..."); + try { + Map jobs = new HashMap<>(); + if (!CollectionUtils.isEmpty(taskRegistrar.getCronTaskList())) { + for (CronTask cronTask : taskRegistrar.getCronTaskList()) { + if (cronTask.getRunnable() instanceof ScheduledMethodRunnable) { + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) cronTask.getRunnable(); + JobProperty jobProperty = convertToJobProperty(cronTask, runnable.getTarget(), runnable.getMethod()); + jobs.put(jobProperty.getJobName(), jobProperty); + } + } + } + if (!CollectionUtils.isEmpty(taskRegistrar.getFixedDelayTaskList())) { + for (IntervalTask intervalTask : taskRegistrar.getFixedDelayTaskList()) { + if (intervalTask.getRunnable() instanceof ScheduledMethodRunnable) { + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) intervalTask.getRunnable(); + JobProperty jobProperty = convertToJobProperty(intervalTask, runnable.getTarget(), runnable.getMethod()); + jobs.put(jobProperty.getJobName(), jobProperty); + } + } + } + if (!CollectionUtils.isEmpty(taskRegistrar.getFixedRateTaskList())) { + for (IntervalTask intervalTask : taskRegistrar.getFixedRateTaskList()) { + if (intervalTask.getRunnable() instanceof ScheduledMethodRunnable) { + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) intervalTask.getRunnable(); + JobProperty jobProperty = convertToJobProperty(intervalTask, runnable.getTarget(), runnable.getMethod()); + jobs.put(jobProperty.getJobName(), jobProperty); + } + } + } + + // 获取仅SchedulerX注解任务 + Collection> schedulerXTasks = schedulerxSchedulingConfigurer.getSchedulerXTaskTargets(); + if (schedulerXTasks != null && schedulerXTasks.size() > 0) { + for (Pair task : schedulerXTasks) { + JobProperty jobProperty = convertToJobProperty(null, task.getFirst(), task.getSecond()); + jobs.put(jobProperty.getJobName(), jobProperty); + } + } + + jobSyncService.syncJobs(jobs, properties.getNamespaceSource()); + logger.info("spring scheduled job is not empty, sync jobs finished."); + } + catch (Exception e) { + logger.info("spring scheduled job is not empty, sync jobs failed.", e); + throw new RuntimeException(e); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpression.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpression.java new file mode 100644 index 000000000..bb286c847 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpression.java @@ -0,0 +1,1540 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx.util; + +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +import org.springframework.util.Assert; + +/** + * @author yaohui + */ +public final class CronExpression { + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map monthMap = new HashMap<>(20); + protected static final Map dayMap = new HashMap<>(60); + + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet seconds; + protected transient TreeSet minutes; + protected transient TreeSet hours; + protected transient TreeSet daysOfMonth; + protected transient TreeSet months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + /** + * MAX_YEAR. + */ + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * MIN_CAL. + */ + public static final Calendar MIN_CAL = Calendar.getInstance(); + + static { + MIN_CAL.set(1970, 0, 1); + } + + /** + * MIN_DATE. + */ + public static final Date MIN_DATE = MIN_CAL.getTime(); + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws ParseException if the string expression cannot be parsed into a valid + * CronExpression + */ + public CronExpression(final String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression. + */ + public boolean isSatisfiedBy(final Date date) { + final Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + final Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + final Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time after the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time. + */ + public Date getNextValidTimeAfter(final Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time after the given date/time which does + * not satisfy the expression. + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time. + */ + public Date getNextInvalidTimeAfter(final Date date) { + long difference = 1000; + + //move back to the nearest second so differences will be accurate + final Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if (newDate == null) { + break; + } + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this CronExpression + * will be resolved. + * + * @return the time zone. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + public void setTimeZone(final TimeZone timeZone) { + this.timeZone = timeZone; + } + + @Override + public String toString() { + return cronExpression; + } + + /** + * check cron expression is valid or not. + * + * @param cronExpression cron expression + * @return return{@code true}if Cron expression is valid,otherwise return{@code false}. + */ + public static boolean isValidExpression(final String cronExpression) { + try { + new CronExpression(cronExpression); + } + catch (final ParseException pe) { + return false; + } + return true; + } + + public static void validateExpression(final String cronExpression) throws ParseException { + new CronExpression(cronExpression); + } + + protected void buildExpression(final String expression) throws ParseException { + expressionParsed = true; + try { + if (seconds == null) { + seconds = new TreeSet<>(); + } + if (minutes == null) { + minutes = new TreeSet<>(); + } + if (hours == null) { + hours = new TreeSet<>(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet<>(); + } + if (months == null) { + months = new TreeSet<>(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet<>(); + } + if (years == null) { + years = new TreeSet<>(); + } + + int exprOn = SECOND; + + final StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + final String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if (exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if (exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if (exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') + 1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + final StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + final String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + final TreeSet dow = getSet(DAY_OF_WEEK); + final TreeSet dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + final boolean dayOfMSpec = !dom.contains(NO_SPEC); + final boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } + catch (final ParseException pe) { + throw pe; + } + catch (final Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); + } + } + + protected int storeExpressionVals(final int pos, final String s, final int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3 && (s.charAt(i + 3) == '-')) { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + Assert.isTrue(eval > 0, "Invalid Month value: '" + sub + "'"); + } + } + else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + Assert.isTrue(eval >= 0, "Invalid Day-of-Week value: '" + sub + "'"); + } + else if (c == '#') { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + Assert.isTrue(nthdayOfWeek > 0 && nthdayOfWeek < 6, "A numeric value between 1 and 5 must follow the '#' option"); + } + else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + } + else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specfied for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + final int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specfied for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } + else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } + else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, i); + } + else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, i); + } + else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, i); + } + else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, i); + } + else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, i); + } + } + else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } + else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if (type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if (c == '-') { + final ValueSet vs = getValue(0, s, i + 1); + lastdayOffset = vs.value; + if (lastdayOffset > 30) { + throw new ParseException("Offset from last day must be <= 30", i + 1); + } + i = vs.pos; + } + if (s.length() > i) { + c = s.charAt(i); + if (c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } + else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } + else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + final ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } + else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + protected int checkNext(final int pos, final String s, final int val, final int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if (val < 1 || val > 7) { + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + } + lastdayOfWeek = true; + } + else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + final TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } + else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if (val > 31) { + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + } + final TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } + catch (final Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + final TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + final int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + final ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && (s.charAt(i) == '/')) { + i++; + c = s.charAt(i); + final int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + final ValueSet vs = getValue(v2, s, i); + final int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } + else { + addToSet(val, end, v2, type); + return i; + } + } + else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + i++; + c = s.charAt(i); + final int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + final ValueSet vs = getValue(v2, s, i); + final int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } + else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + final StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(final java.util.Set set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + final StringBuilder buf = new StringBuilder(); + + final Iterator itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + final Integer iVal = itr.next(); + final String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected String getExpressionSetSummary(final java.util.ArrayList list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + final StringBuilder buf = new StringBuilder(); + + final Iterator itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + final Integer iVal = itr.next(); + final String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected int skipWhiteSpace(int i, final String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + // empty + } + + return i; + } + + protected int findNextWhiteSpace(int i, final String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + // empty + } + + return i; + } + + protected void addToSet(final int val, final int end, int incr, final int type) + throws ParseException { + + final TreeSet set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } + else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } + else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } + else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } + else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } + else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } + else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } + else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } + else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } + else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } + else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND: + max = 60; + break; + case MINUTE: + max = 60; + break; + case HOUR: + max = 24; + break; + case MONTH: + max = 12; + break; + case DAY_OF_WEEK: + max = 7; + break; + case DAY_OF_MONTH: + max = 31; + break; + case YEAR: + throw new IllegalArgumentException("Start year must be less than stop year"); + default: + throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } + else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet getSet(final int type) throws ParseException { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + throw new ParseException("Invalid type value: '" + type + "'", -1); + } + } + + protected ValueSet getValue(final int v, final String s, int i) { + char c = s.charAt(i); + final StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + final ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(final String s, final int i) { + final int endOfVal = findNextWhiteSpace(i, s); + final String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(final String s) { + final Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + protected int getDayOfWeekNumber(final String s) { + final Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + final Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + if (afterTime == null) { + return null; + } + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + //if (endTime != null && cl.getTime().after(endTime)) return null; + if (cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } + else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } + else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } + else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + final boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + final boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if (!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + day = t > day ? 1 : day; + if (t > day && ++mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + } + else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + final Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + final int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + final int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } + else if (dow == Calendar.SATURDAY) { + day -= 1; + } + else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } + else if (dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + final Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } + else if (nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + final Calendar tcal = Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + final int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + final int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if (dow == Calendar.SATURDAY && day == 1) { + day += 2; + } + else if (dow == Calendar.SATURDAY) { + day -= 1; + } + else if (dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } + else if (dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + final Date nTime = tcal.getTime(); + if (nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } + else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + final int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } + else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } + else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + final int dow = daysOfWeek.first(); // desired + // d-o-w + final int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + final int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + day += daysToAdd; + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + } + else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + final int dow = daysOfWeek.first(); // desired + // d-o-w + final int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } + else { + final int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + final int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + else if (daysToAdd > 0) { // are we switching days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } + else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } + else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } + else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(final Calendar cal, final int hour) { + cal.set(Calendar.HOUR_OF_DAY, hour); + if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(Calendar.HOUR_OF_DAY, hour + 1); + } + } + + protected Date getTimeBefore(final Date targetDate) { + final Calendar cl = Calendar.getInstance(getTimeZone()); + + // CronTrigger does not deal with milliseconds, so truncate target + cl.setTime(targetDate); + cl.set(Calendar.MILLISECOND, 0); + final Date targetDateNoMs = cl.getTime(); + + // to match this + Date start = targetDateNoMs; + final long minIncrement = findMinIncrement(); + Date prevFireTime; + do { + final Date prevCheckDate = new Date(start.getTime() - minIncrement); + prevFireTime = getTimeAfter(prevCheckDate); + if (prevFireTime == null || prevFireTime.before(MIN_DATE)) { + return null; + } + start = prevCheckDate; + } while (prevFireTime.compareTo(targetDateNoMs) >= 0); + return prevFireTime; + } + + public Date getPrevFireTime(final Date targetDate) { + return getTimeBefore(targetDate); + } + + private long findMinIncrement() { + if (seconds.size() != 1) { + return minInSet(seconds) * 1000; + } + else if (seconds.first() == ALL_SPEC_INT) { + return 1000; + } + if (minutes.size() != 1) { + return minInSet(minutes) * 60000; + } + else if (minutes.first() == ALL_SPEC_INT) { + return 60000; + } + if (hours.size() != 1) { + return minInSet(hours) * 3600000; + } + else if (hours.first() == ALL_SPEC_INT) { + return 3600000; + } + return 86400000; + } + + private int minInSet(final TreeSet set) { + int previous = 0; + int min = Integer.MAX_VALUE; + boolean first = true; + for (final int value : set) { + if (first) { + previous = value; + first = false; + continue; + } + else { + final int diff = value - previous; + if (diff < min) { + min = diff; + } + } + } + return min; + } + + protected boolean isLeapYear(final int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + protected int getLastDayOfMonth(final int monthNum, final int year) { + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + private class ValueSet { + public int value; + public int pos; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/shedlock/ShedLockAutoConfigure.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/shedlock/ShedLockAutoConfigure.java new file mode 100644 index 000000000..eb480469b --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/java/com/alibaba/cloud/scheduling/shedlock/ShedLockAutoConfigure.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.shedlock; + +import javax.sql.DataSource; + +import com.alibaba.cloud.scheduling.SchedulingConstants; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +/** + * @author yaohui + **/ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@EnableSchedulerLock(defaultLockAtMostFor = "1m") +@ConditionalOnBean(DataSource.class) +@ConditionalOnProperty(name = SchedulingConstants.SCHEDULING_CONFIG_DISTRIBUTED_MODE_KEY, havingValue = "shedlock") +public class ShedLockAutoConfigure { + + private static final String SCHEDULED_LOCK_SCHEMA_PATH = "shedlock/schema/schema-mysql.sql"; + + @Bean + @ConditionalOnMissingBean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider(JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() + .build()); + } + + @Bean + @ConditionalOnMissingBean + public DataSourceInitializer shedLockDataSourceInitializer(DataSource dataSource) { + ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator(); + resourceDatabasePopulator.addScript(new ClassPathResource(SCHEDULED_LOCK_SCHEMA_PATH)); + DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); + dataSourceInitializer.setDataSource(dataSource); + dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator); + return dataSourceInitializer; + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..0d03ded27 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.alibaba.cloud.scheduling.schedulerx.SchedulerxAutoConfigure,\ +com.alibaba.cloud.scheduling.shedlock.ShedLockAutoConfigure diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/shedlock/schema/schema-mysql.sql b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/shedlock/schema/schema-mysql.sql new file mode 100644 index 000000000..c369b4c0e --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/main/resources/shedlock/schema/schema-mysql.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL, + locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name)); diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/test/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpressionTest.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/test/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpressionTest.java new file mode 100644 index 000000000..4e415b290 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-schedulerx/src/test/java/com/alibaba/cloud/scheduling/schedulerx/util/CronExpressionTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed 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 + * + * https://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 com.alibaba.cloud.scheduling.schedulerx.util; + +import java.text.ParseException; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author yaohui + */ +class CronExpressionTest { + + @Test + void isValidExpression() { + assertThat(CronExpression.isValidExpression("0 0 0 * * ?")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 */5 * * * ?")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 0 8 1 JAN ?")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 THU")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 THU-SAT")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 FRI#1")).isEqualTo(true); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 FRI#5")).isEqualTo(true); + // false + assertThat(CronExpression.isValidExpression("0 0 8 1 JAW ?")).isEqualTo(false); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 THP-SAT")).isEqualTo(false); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 FRI#0")).isEqualTo(false); + assertThat(CronExpression.isValidExpression("0 0 8 ? 10 FRI#6")).isEqualTo(false); + } + + @Test + void getTimeAfter() throws ParseException { + CronExpression cronExpression = new CronExpression("0 0 8 1 JAN ?"); + Date nextDate = cronExpression.getTimeAfter(new Date()); + System.out.println(nextDate); + assertThat(nextDate).isNotNull(); + + cronExpression = new CronExpression("0 */5 * * * ?"); + nextDate = cronExpression.getTimeAfter(new Date()); + System.out.println(nextDate); + assertThat(nextDate).isNotNull(); + } + + @Test + void getTimeBefore() throws ParseException { + CronExpression cronExpression = new CronExpression("0 0 8 1 JAN ?"); + Date beforeDate = cronExpression.getTimeBefore(new Date()); + System.out.println(beforeDate); + assertThat(beforeDate).isNotNull(); + + cronExpression = new CronExpression("0 */5 * * * ?"); + beforeDate = cronExpression.getTimeBefore(new Date()); + System.out.println(beforeDate); + assertThat(beforeDate).isNotNull(); + } + +}