├── .gitignore
├── README.md
├── planner
├── .gitignore
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── aav
│ │ └── planner
│ │ ├── annotation
│ │ └── SchedulerLogAndLock.java
│ │ ├── exception
│ │ └── SchedulerLogAndLockException.java
│ │ ├── model
│ │ ├── LockParam.java
│ │ └── ScheduleParams.java
│ │ ├── service
│ │ ├── DurationHandler.java
│ │ ├── LockHandler.java
│ │ ├── SchedulerMethodCallback.java
│ │ ├── SchedulerPostProcessor.java
│ │ ├── Task.java
│ │ ├── lock
│ │ │ └── LockAction.java
│ │ └── log
│ │ │ └── LogAction.java
│ │ └── utility
│ │ └── Utils.java
│ └── resources
│ └── application.properties
├── pom.xml
└── storage
└── jdbc
├── .gitignore
├── pom.xml
└── src
└── main
├── java
└── com
│ └── aav
│ └── jdbc
│ ├── JdbcStorageAction.java
│ ├── JdbcStorageClient.java
│ └── service
│ ├── AbstractLockAction.java
│ ├── AbstractLogAction.java
│ ├── SqlExecutor.java
│ └── SqlFunction.java
└── resources
└── application.properties
/.gitignore:
--------------------------------------------------------------------------------
1 | README.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | scheduler-LogAndLock
2 | ========
3 | 
4 |
5 | "scheduler-LogAndLock" provides the following capabilities:
6 | - create a schedule for running methods
7 | - pass parameters to these methods
8 | - save execution results
9 | - keep a log of launches
10 | - block simultaneous execution of a task on more than one application instance.
11 |
12 | At the moment, only SQL databases are implemented as storage.
13 |
14 | ### Get Started
15 |
16 | Install the project and add the following dependencies to your project.
17 |
18 | ~~~xml
19 |
20 |
21 | com.aav
22 | planner
23 | 1.0.3
24 |
25 | ~~~
26 |
27 | This dependency implements the work of the scheduler.
28 |
29 | You then need to add the "@SchedulerLogAndLock" annotation to the method, which will run
30 | according to your schedule.
31 |
32 | Example
33 |
34 | ~~~java
35 | import com.aav.planner.annotation.SchedulerLogAndLock;
36 | import com.aav.planner.model.ScheduleParams;
37 | ...
38 |
39 | @SchedulerLogAndLock(cron = "0 0/1 * * * *", lock = true, lockUntil = "10m")
40 | public Map example(ScheduleParams params){
41 |
42 | //any logic
43 |
44 | HashMap map=new HashMap<>();
45 | map.put("stamp",System.currentTimeMillis());
46 | return map;
47 | }
48 | ~~~
49 |
50 | The "SchedulerLogAndLock" annotation takes three parameters as input.
51 | of which:
52 | cron - accepts a standard cron expression (required)
53 | lock - enables locking between hosts (optional)
54 | lockUntil - specifies the time to hold the lock (optional)
55 | replace - overwrite last entry for running method (optional)
56 |
57 | cron can also accept the "-" parameter, in which case the schedule will not be created.
58 | If you pass parameters through application.yml you can disable your job at any time.
59 | cron = "${app.scheduler.test}" - an example of passing cron expressions through application file
60 | "lockUntil" accepts as a valid value a string like "5m", where the valid unit is s (seconds), m (
61 | minutes), h(hours).
62 |
63 | "ScheduleParams" returns the parameters of the last run. Contains information about the beginning
64 | and end of work. And there is a Map with useful data. When the work is finished, you can add any
65 | information in the Map that you need the next time you run it (such as the timestamp).
66 |
67 | ### storage
68 |
69 | #### jdbc
70 |
71 | When working with a SQL database, you need to create a table for the job log and for the lock.
72 |
73 | ~~~sql
74 | create table public.scheduler_log
75 | (
76 | id uuid not null
77 | constraint scheduler_log_pk
78 | primary key,
79 | name varchar(100),
80 | start timestamp with time zone,
81 | finish timestamp with time zone,
82 | info json
83 | );
84 | comment on table public.scheduler_log is 'job execution log';
85 |
86 | create table public.scheduler_lock
87 | (
88 | name varchar(100) not null
89 | constraint scheduler_lock_pk
90 | primary key,
91 | lock_until timestamp,
92 | host varchar(100)
93 | );
94 | comment on table public.scheduler_lock is 'job lock journal';
95 | ~~~
96 |
97 | You need to add a dependency.
98 |
99 | ~~~xml
100 |
101 |
102 | com.aav
103 | jdbc
104 | 1.0.2
105 |
106 | ~~~
107 |
108 | By default, the following schema and table names are set.
109 |
110 | ~~~java
111 | SCHEMA="public";
112 | LOG_TABLE="scheduler_log";
113 | LOCK_TABLE="scheduler_lock";
114 | ~~~
115 |
116 | But you can specify your values through the "JdbcStorageClient" constructor when creating the bean.
117 | ______________________________________
118 |
119 | Then you need to create a configuration to work with.
120 |
121 | ~~~java
122 | import com.aav.jdbc.JdbcStorageAction;
123 | import com.aav.jdbc.JdbcStorageClient;
124 | import com.aav.planner.service.SchedulerPostProcessor;
125 | import com.aav.planner.service.lock.LockAction;
126 | import com.aav.planner.service.log.LogAction;
127 | import javax.sql.DataSource;
128 | import org.springframework.context.annotation.Bean;
129 | import org.springframework.context.annotation.Configuration;
130 | import org.springframework.core.env.Environment;
131 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
132 |
133 | @Configuration
134 | public class CustomConfig {
135 |
136 | @Bean
137 | public JdbcStorageAction jdbcStorageAction(DataSource dataSource) {
138 | //return new JdbcStorageClient(dataSource, "my_schema").init();
139 | return new JdbcStorageClient(dataSource).init();
140 | }
141 |
142 | @Bean
143 | public SchedulerPostProcessor postProcessor(
144 | ThreadPoolTaskScheduler threadPoolTaskScheduler,
145 | LogAction logAction,
146 | LockAction lockAction,
147 | Environment env
148 | ) {
149 | return new SchedulerPostProcessor(threadPoolTaskScheduler, logAction, lockAction, env);
150 | }
151 | }
152 | ~~~
153 |
154 | By default, "ThreadPoolTaskScheduler" has a value of 1. Accordingly, you must understand the number of tasks,
155 | which you run at the same time, since they will be executed sequentially unless you increase
156 | the number of available threads in the pool.
157 | For example, if you have 4 jobs that run at the same time,
158 | but at the same time, 2 instances of your service are guaranteed to be raised, you can set the number of threads equal to 2.
159 | In this case, 2 tasks will be simultaneously executed on each instance.
160 |
161 | Setting example
162 | setPoolSize - sets the number of threads in the pool
163 |
164 | ~~~java
165 | @Bean
166 | public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
167 | ThreadPoolTaskScheduler threadPoolTaskScheduler
168 | =new ThreadPoolTaskScheduler();
169 | threadPoolTaskScheduler.setPoolSize(10);
170 | threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
171 | return threadPoolTaskScheduler;
172 | }
173 | ~~~
174 |
175 | You can run the application.
176 |
--------------------------------------------------------------------------------
/planner/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/planner/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | scheduler-with-LogAndLock
9 | com.aav
10 | 1.0.1
11 |
12 |
13 | planner
14 | 1.0.3
15 | planner
16 | planner
17 |
18 |
19 |
20 | org.springframework
21 | spring-context
22 | 5.3.22
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | org.apache.maven.plugins
31 | maven-jar-plugin
32 |
33 |
34 |
35 |
36 | com.aav.planner
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/annotation/SchedulerLogAndLock.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.annotation;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Inherited;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.RetentionPolicy;
7 | import java.lang.annotation.Target;
8 |
9 | @Retention(RetentionPolicy.RUNTIME)
10 | @Target(ElementType.METHOD)
11 | @Inherited
12 | public @interface SchedulerLogAndLock {
13 |
14 | /**
15 | * takes a standard cron expression
16 | */
17 | String cron();
18 |
19 | /**
20 | * enable scheduler lock
21 | */
22 | boolean lock() default false;
23 |
24 | /**
25 | * Sets logging to overwrite mode. If there are already logs for the method, then the last line
26 | * will be overwritten, if not, a new line will be created that will be overwritten.
27 | */
28 | boolean replace() default false;
29 |
30 | /**
31 | * the time for which the lock is guaranteed to be held
32 | */
33 | String lockUntil() default "0m";
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/exception/SchedulerLogAndLockException.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.exception;
2 |
3 | public class SchedulerLogAndLockException extends RuntimeException {
4 |
5 | public SchedulerLogAndLockException(String message) {
6 | super(message);
7 | }
8 |
9 | public SchedulerLogAndLockException(String message, Throwable throwable) {
10 | super(message, throwable);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/model/LockParam.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.model;
2 |
3 | import java.time.Instant;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.Setter;
8 | import lombok.ToString;
9 |
10 |
11 | /**
12 | * lock options.
13 | * "lockUntil" time until which the lock will be held
14 | */
15 | @NoArgsConstructor
16 | @AllArgsConstructor
17 | @Getter
18 | @Setter
19 | @ToString
20 | public class LockParam {
21 |
22 | private String name;
23 | private Instant lockUntil;
24 | private String host;
25 | }
26 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/model/ScheduleParams.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.model;
2 |
3 | import java.sql.Timestamp;
4 | import java.util.Map;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 |
11 | /**
12 | * information about the running time of the job and useful information transmitted by the user,
13 | * which is returned when the method is started again.
14 | */
15 | @NoArgsConstructor
16 | @AllArgsConstructor
17 | @Getter
18 | @Setter
19 | @ToString
20 | public class ScheduleParams {
21 |
22 | private Timestamp start;
23 | private Timestamp end;
24 | private Map info;
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/DurationHandler.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service;
2 |
3 | import com.aav.planner.exception.SchedulerLogAndLockException;
4 | import java.time.Duration;
5 | import java.time.temporal.ChronoUnit;
6 | import java.util.Map;
7 | import java.util.Optional;
8 | import java.util.regex.Matcher;
9 | import java.util.regex.Pattern;
10 |
11 | public class DurationHandler {
12 |
13 | private DurationHandler() {
14 | }
15 |
16 | private static final Pattern PATTERN = Pattern.compile("^([\\+]?\\d{1,10})([a-zA-Z]{0,1})$");
17 |
18 | private static final Map UNIT_MAP;
19 |
20 | static {
21 | UNIT_MAP = Map.of(
22 | "s", ChronoUnit.SECONDS,
23 | "m", ChronoUnit.MINUTES,
24 | "h", ChronoUnit.HOURS,
25 | "", ChronoUnit.SECONDS
26 | );
27 | }
28 |
29 | public static Duration getDuration(String value) {
30 | Matcher matcher = PATTERN.matcher(value);
31 | if (matcher.matches()) {
32 | long amount = Long.parseLong(matcher.group(1));
33 | ChronoUnit unit = getTemporal(matcher.group(2));
34 | return Duration.of(amount, unit);
35 | }
36 | return Duration.of(0, ChronoUnit.SECONDS);
37 | }
38 |
39 | private static ChronoUnit getTemporal(String key) {
40 | return Optional.of(UNIT_MAP.get(key))
41 | .orElseThrow(() -> new SchedulerLogAndLockException("wrong delay time value"));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/LockHandler.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service;
2 |
3 | import com.aav.planner.model.LockParam;
4 | import com.aav.planner.service.lock.LockAction;
5 | import com.aav.planner.service.log.LogAction;
6 | import java.time.Instant;
7 | import java.util.Map;
8 | import java.util.Optional;
9 | import java.util.UUID;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | public class LockHandler {
14 |
15 | private final Logger log = LoggerFactory.getLogger(getClass());
16 |
17 | private final boolean isLockEnable; // lock is used
18 | private final boolean isReplaceLog;
19 | private final LogAction logAction;
20 | private final LockAction lockAction;
21 |
22 | // this is the ID of the new entry in the logging log
23 | private UUID rowId;
24 |
25 | public LockHandler(boolean isLockEnable, boolean isReplaceLog, LogAction logAction,
26 | LockAction lockAction) {
27 | this.isLockEnable = isLockEnable;
28 | this.isReplaceLog = isReplaceLog;
29 | this.logAction = logAction;
30 | this.lockAction = lockAction;
31 | }
32 |
33 | public boolean createLockAndLog(LockParam lockParam, String methodName) {
34 | rowId = UUID.randomUUID();
35 |
36 | if (isLockEnable) {
37 | LockParam currentLock = lockAction.getLockInfo(lockParam);
38 |
39 | if (currentLock == null || currentLock.getName() == null) {
40 | if (!lockAction.lock(lockParam)) {
41 | return false;
42 | }
43 | } else {
44 | if (tryUpdateLock(currentLock, lockParam)) {
45 | if (isReplaceLog) {
46 | return tryReplaceLog(methodName);
47 | }
48 | return logAction.create(rowId, methodName);
49 | } else {
50 | return false;
51 | }
52 | }
53 | }
54 |
55 | return isReplaceLog ? tryReplaceLog(methodName) : logAction.create(rowId, methodName);
56 | }
57 |
58 | private boolean tryReplaceLog(String methodName) {
59 | final Optional replace = logAction.replace(methodName);
60 | if (replace.isPresent()) {
61 | rowId = replace.get();
62 | return true;
63 | } else {
64 | return false;
65 | }
66 | }
67 |
68 | public boolean updateCurrentRow(Map info) {
69 | return logAction.update(rowId, info);
70 | }
71 |
72 | private boolean tryUpdateLock(LockParam currentLock, LockParam lockParam) {
73 | if (currentLock.getLockUntil().isAfter(Instant.now())) {
74 | return tryLockIfCurrentHost(currentLock, lockParam);
75 | } else {
76 | lockAction.unlock(lockParam);
77 | if (!lockAction.lock(lockParam)) {
78 | log.debug("Lock already exists!");
79 | return false;
80 | } else {
81 | return true;
82 | }
83 | }
84 | }
85 |
86 |
87 | private boolean tryLockIfCurrentHost(LockParam currentLock, LockParam lockParam) {
88 | if (currentLock.getHost().equals(lockParam.getHost())) {
89 | return lockAction.updateLock(lockParam) || lockAction.lock(lockParam);
90 | } else {
91 | return false;
92 | }
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/SchedulerMethodCallback.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service;
2 |
3 | import com.aav.planner.annotation.SchedulerLogAndLock;
4 | import com.aav.planner.service.lock.LockAction;
5 | import com.aav.planner.service.log.LogAction;
6 | import java.lang.reflect.Method;
7 | import lombok.RequiredArgsConstructor;
8 | import org.springframework.core.env.Environment;
9 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
10 | import org.springframework.util.ReflectionUtils.MethodCallback;
11 |
12 | @RequiredArgsConstructor
13 | public class SchedulerMethodCallback implements MethodCallback {
14 |
15 | private final ThreadPoolTaskScheduler taskScheduler;
16 | private final Object bean;
17 | private final LogAction logAction;
18 | private final LockAction lockAction;
19 | private final Environment env;
20 |
21 | @Override
22 | public void doWith(Method method) throws IllegalArgumentException {
23 |
24 | if (method.isAnnotationPresent(SchedulerLogAndLock.class)) {
25 | Task task = new Task();
26 | task.init(method, taskScheduler, bean, logAction, lockAction, env);
27 | }
28 |
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/SchedulerPostProcessor.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service;
2 |
3 | import com.aav.planner.service.lock.LockAction;
4 | import com.aav.planner.service.log.LogAction;
5 | import org.springframework.beans.BeansException;
6 | import org.springframework.beans.factory.config.BeanPostProcessor;
7 | import org.springframework.core.env.Environment;
8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
9 | import org.springframework.util.ReflectionUtils;
10 |
11 | public class SchedulerPostProcessor implements BeanPostProcessor {
12 |
13 | private final ThreadPoolTaskScheduler taskScheduler;
14 | private final LogAction logAction;
15 | private final LockAction lockAction;
16 | private final Environment env;
17 |
18 | public SchedulerPostProcessor(ThreadPoolTaskScheduler taskScheduler, LogAction logAction,
19 | LockAction lockAction, Environment env) {
20 | this.taskScheduler = taskScheduler;
21 | this.logAction = logAction;
22 | this.lockAction = lockAction;
23 | this.env = env;
24 | }
25 |
26 | public SchedulerPostProcessor(ThreadPoolTaskScheduler taskScheduler, LogAction logAction,
27 | LockAction lockAction) {
28 | this.taskScheduler = taskScheduler;
29 | this.logAction = logAction;
30 | this.lockAction = lockAction;
31 | this.env = null;
32 | }
33 |
34 | @Override
35 | public Object postProcessBeforeInitialization(Object bean, String beanName)
36 | throws BeansException {
37 | this.configureFieldInjection(bean);
38 | return bean;
39 | }
40 |
41 | private void configureFieldInjection(Object bean) {
42 | Class> managedBeanClass = bean.getClass();
43 | SchedulerMethodCallback callback = new SchedulerMethodCallback(taskScheduler, bean, logAction,
44 | lockAction, env);
45 | ReflectionUtils.doWithMethods(managedBeanClass, callback);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/Task.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service;
2 |
3 | import static com.aav.planner.service.DurationHandler.getDuration;
4 | import static com.aav.planner.utility.Utils.HOST_NAME;
5 |
6 | import com.aav.planner.annotation.SchedulerLogAndLock;
7 | import com.aav.planner.exception.SchedulerLogAndLockException;
8 | import com.aav.planner.model.LockParam;
9 | import com.aav.planner.model.ScheduleParams;
10 | import com.aav.planner.service.lock.LockAction;
11 | import com.aav.planner.service.log.LogAction;
12 | import java.lang.reflect.Method;
13 | import java.time.Instant;
14 | import java.util.Map;
15 | import lombok.RequiredArgsConstructor;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 | import org.springframework.core.env.Environment;
19 | import org.springframework.scheduling.Trigger;
20 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
21 | import org.springframework.scheduling.support.CronTrigger;
22 |
23 | @RequiredArgsConstructor
24 | class Task {
25 |
26 | public void init(Method method, ThreadPoolTaskScheduler taskScheduler, Object bean,
27 | LogAction logAction, LockAction lockAction, Environment env) {
28 |
29 | final Class> returnType = method.getReturnType();
30 |
31 | if (!returnType.isAssignableFrom(Map.class)) {
32 | throw new SchedulerLogAndLockException("The return type must be -> Map");
33 | }
34 |
35 | String cron = getCron(method);
36 | if (cron.equals("-")) {
37 | return;
38 | } else if (cron.startsWith("$")) {
39 | if (env == null) {
40 | throw new SchedulerLogAndLockException(
41 | "Need to pass Environment to the constructor SchedulerPostProcessor");
42 | }
43 | var property = env.getProperty(cron.substring(2, cron.length() - 1));
44 | if (property == null) {
45 | throw new SchedulerLogAndLockException(
46 | String.format("Parameter '%s' not found in app config", cron));
47 | }
48 | cron = property;
49 | }
50 |
51 |
52 | Trigger cronTrigger = new CronTrigger(cron);
53 | var uniqueName = createUniqueName(bean, method);
54 | var ifLockEnabled = method.getAnnotation(SchedulerLogAndLock.class).lock();
55 | var ifReplaceLog = method.getAnnotation(SchedulerLogAndLock.class).replace();
56 | var lockUntil = method.getAnnotation(SchedulerLogAndLock.class).lockUntil();
57 |
58 | taskScheduler.schedule(
59 | new TaskBody(logAction, lockAction, bean, method, ifLockEnabled, ifReplaceLog, uniqueName,
60 | lockUntil), cronTrigger);
61 |
62 | }
63 |
64 | private String createUniqueName(Object bean, Method method) {
65 | String name = bean.getClass().getName() + "." + method.getName();
66 | return name.substring(name.length() - Math.min(name.length(), 100));
67 | }
68 |
69 |
70 | private String getCron(Method method) {
71 | String cron = method.getAnnotation(SchedulerLogAndLock.class).cron();
72 | String[] s = cron.split(" ");
73 | if (s.length != 6 && !cron.equals("-") && !cron.startsWith("$")) {
74 | throw new SchedulerLogAndLockException("Invalid cron expression");
75 | }
76 | return cron;
77 | }
78 |
79 |
80 | static class TaskBody implements Runnable {
81 |
82 | private final Logger log = LoggerFactory.getLogger(getClass());
83 |
84 | private final LogAction logAction;
85 | private final Object bean;
86 | private final Method method;
87 | private final String name;
88 | private final LockHandler lockHandler;
89 | private final String lockUntil;
90 |
91 | public TaskBody(LogAction logAction, LockAction lockAction, Object bean, Method method,
92 | boolean ifLockEnabled, boolean ifReplaceLog, String name, String lockUntil) {
93 | this.logAction = logAction;
94 | this.bean = bean;
95 | this.method = method;
96 | this.name = name;
97 | this.lockUntil = lockUntil;
98 | this.lockHandler = new LockHandler(ifLockEnabled, ifReplaceLog, logAction, lockAction);
99 | }
100 |
101 | private LockParam createLockParam() {
102 | return new LockParam(
103 | name,
104 | Instant.now().plus(getDuration(lockUntil)),
105 | HOST_NAME);
106 | }
107 |
108 | @Override
109 | public void run() {
110 | LockParam lockParam = createLockParam();
111 | try {
112 | boolean lockAndLog = lockHandler.createLockAndLog(lockParam, name);
113 | ScheduleParams lastParams = logAction.getLastRow(name);
114 | Map mapToSave;
115 | if (lockAndLog) {
116 | try {
117 | mapToSave = (Map) method.invoke(bean, lastParams);
118 |
119 | if (lockHandler.updateCurrentRow(mapToSave)) {
120 | log.debug("Job {} successfully completed", name);
121 | }
122 | } catch (Exception e) {
123 | log.error("Exception while executing task {} ", name, e);
124 | }
125 | }
126 | } catch (Exception e) {
127 | log.error("It's a failure!", e);
128 | }
129 | }
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/lock/LockAction.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service.lock;
2 |
3 | import com.aav.planner.model.LockParam;
4 |
5 | /**
6 | * available actions for blocking jobs through the database
7 | */
8 | public interface LockAction {
9 |
10 | /**
11 | * create lock
12 | *
13 | * @param param params
14 | * @return true if successful
15 | */
16 | boolean lock(LockParam param);
17 |
18 | /**
19 | * update lock
20 | *
21 | * @param param params
22 | * @return true if successful
23 | */
24 | boolean updateLock(LockParam param);
25 |
26 |
27 | /**
28 | * unlock
29 | *
30 | * @param param params
31 | * @return true if successful
32 | */
33 | boolean unlock(LockParam param);
34 |
35 | /**
36 | * getting information about an existing lock
37 | *
38 | * @param param params
39 | * @return information about lock
40 | */
41 | LockParam getLockInfo(LockParam param);
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/service/log/LogAction.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.service.log;
2 |
3 | import com.aav.planner.model.ScheduleParams;
4 | import java.util.Map;
5 | import java.util.Optional;
6 | import java.util.UUID;
7 |
8 | /**
9 | * available actions for logging events in the database
10 | */
11 | public interface LogAction {
12 |
13 | /**
14 | * this method to create a new entry in the event log when the job starts
15 | *
16 | * @return true if successful
17 | */
18 | boolean create(UUID id, String name);
19 |
20 | /**
21 | * this method to update the results of running jobs
22 | *
23 | * @param id log id
24 | * @param info useful information to save
25 | * @return true if successful
26 | */
27 | boolean update(UUID id, Map info);
28 |
29 | /**
30 | * query parameters written during the last job
31 | *
32 | * @param name method name
33 | * @return ScheduleParams
34 | */
35 | ScheduleParams getLastRow(String name);
36 |
37 | /**
38 | * Tries to find the last entry with the specified name and replace the values in it, if the entry
39 | * with the same name does not exist, then tries to create a new one.
40 | *
41 | * @param name method name
42 | * @return row id
43 | */
44 | Optional replace(String name);
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/planner/src/main/java/com/aav/planner/utility/Utils.java:
--------------------------------------------------------------------------------
1 | package com.aav.planner.utility;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 |
6 | public class Utils {
7 |
8 | private Utils() {
9 | }
10 |
11 | public static final String HOST_NAME = initHostname();
12 |
13 | private static String initHostname() {
14 | try {
15 | String host = InetAddress.getLocalHost().getHostName();
16 | return host.substring(host.length() - Math.min(host.length(), 100));
17 | } catch (UnknownHostException e) {
18 | return "unknown";
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/planner/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 | pom
7 |
8 |
9 | org.springframework.boot
10 | spring-boot-starter-parent
11 | 2.7.3
12 |
13 |
14 | com.aav
15 | scheduler-with-LogAndLock
16 | 1.0.2
17 | Scheduler-With-LogAndLock
18 | Scheduler-With-LogAndLock
19 |
20 |
21 |
22 | TimTim
23 | Artem Artemev
24 |
25 |
26 |
27 |
28 | 11
29 | 2.7.3
30 | UTF-8
31 | 1.18.24
32 | 2.0.0
33 |
34 |
35 |
36 | planner
37 | storage/jdbc
38 |
39 |
40 |
41 |
42 |
43 | org.projectlombok
44 | lombok
45 | ${lombok.version}
46 | true
47 |
48 |
49 |
50 | org.slf4j
51 | slf4j-api
52 | ${slf4j.version}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | maven-compiler-plugin
61 | 3.10.1
62 |
63 | ${java.version}
64 | ${java.version}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/storage/jdbc/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/storage/jdbc/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | scheduler-with-LogAndLock
9 | com.aav
10 | 1.0.2
11 | ../../pom.xml
12 |
13 |
14 | jdbc
15 | 1.0.2
16 | jdbc
17 | jdbc
18 |
19 |
20 |
21 | org.springframework
22 | spring-jdbc
23 | 5.3.22
24 |
25 |
26 |
27 | org.projectlombok
28 | lombok
29 | true
30 |
31 |
32 |
33 | com.aav
34 | planner
35 | 1.0.3
36 | compile
37 |
38 |
39 | com.fasterxml.jackson.core
40 | jackson-databind
41 | 2.13.3
42 |
43 |
44 |
45 | com.fasterxml.jackson.datatype
46 | jackson-datatype-jsr310
47 | 2.13.3
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | org.apache.maven.plugins
56 | maven-jar-plugin
57 |
58 |
59 |
60 |
61 | com.aav.jdbc
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/JdbcStorageAction.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc;
2 |
3 | import static java.util.Objects.requireNonNull;
4 |
5 | import com.aav.jdbc.service.AbstractLogAction;
6 | import com.aav.jdbc.service.SqlFunction;
7 | import java.sql.Connection;
8 | import java.sql.PreparedStatement;
9 | import java.sql.SQLException;
10 | import java.util.function.Function;
11 | import javax.sql.DataSource;
12 | import lombok.NonNull;
13 |
14 | public class JdbcStorageAction extends AbstractLogAction {
15 |
16 | private static final String LOG_TABLE = "scheduler_log";
17 | private static final String LOCK_TABLE = "scheduler_lock";
18 |
19 | private final DataSource dataSource;
20 |
21 | public JdbcStorageAction(@NonNull DataSource dataSource) {
22 | this(dataSource, "public", LOG_TABLE, LOCK_TABLE);
23 | }
24 |
25 | public JdbcStorageAction(@NonNull DataSource dataSource, @NonNull String schema) {
26 | this(dataSource, schema, LOG_TABLE, LOCK_TABLE);
27 | }
28 |
29 | public JdbcStorageAction(@NonNull DataSource dataSource, @NonNull String schema,
30 | @NonNull String logTable) {
31 | this(dataSource, schema, logTable, LOCK_TABLE);
32 | }
33 |
34 | public JdbcStorageAction(@NonNull DataSource dataSource, @NonNull String schema,
35 | @NonNull String logTable, @NonNull String lockTable) {
36 | super(schema, logTable, lockTable);
37 | this.dataSource = requireNonNull(dataSource, "dataSource can not be null");
38 | }
39 |
40 |
41 | @Override
42 | protected T executeQuery(String query, SqlFunction body,
43 | Function exceptionHandler
44 | ) {
45 | try (Connection connection = dataSource.getConnection()) {
46 |
47 | try (PreparedStatement statement = connection.prepareStatement(query)) {
48 | return body.apply(statement);
49 | } catch (SQLException e) {
50 | return exceptionHandler.apply(e);
51 | } finally {
52 | if (!connection.getAutoCommit()) {
53 | connection.commit();
54 | }
55 | }
56 | } catch (SQLException e) {
57 | return exceptionHandler.apply(e);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/JdbcStorageClient.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc;
2 |
3 | import static java.util.Objects.requireNonNull;
4 |
5 | import javax.sql.DataSource;
6 | import lombok.NonNull;
7 |
8 | public class JdbcStorageClient {
9 |
10 | private static final String DEF_SCHEMA = "public";
11 | private static final String LOG_TABLE = "scheduler_log";
12 | private static final String LOCK_TABLE = "scheduler_lock";
13 |
14 | private final DataSource dataSource;
15 | private final String schema;
16 | private final String logTable;
17 | private final String lockTable;
18 |
19 | public JdbcStorageClient(@NonNull DataSource dataSource) {
20 | this(dataSource, DEF_SCHEMA, LOG_TABLE, LOCK_TABLE);
21 | }
22 |
23 | public JdbcStorageClient(@NonNull DataSource dataSource, @NonNull String schema) {
24 | this(dataSource, schema, LOG_TABLE, LOCK_TABLE);
25 | }
26 |
27 | public JdbcStorageClient(@NonNull DataSource dataSource, @NonNull String schema,
28 | @NonNull String logTable) {
29 | this(dataSource, schema, logTable, LOCK_TABLE);
30 | }
31 |
32 | public JdbcStorageClient(@NonNull DataSource dataSource, @NonNull String schema,
33 | @NonNull String logTable, @NonNull String lockTable) {
34 | requireNonNull(schema, "schema can not be null");
35 | requireNonNull(logTable, "logTable can not be null");
36 | requireNonNull(lockTable, "lockTable can not be null");
37 | this.dataSource = requireNonNull(dataSource, "dataSource can not be null");
38 | this.schema = schema;
39 | this.logTable = logTable;
40 | this.lockTable = lockTable;
41 | }
42 |
43 | public JdbcStorageAction init() {
44 | return new JdbcStorageAction(dataSource, schema, logTable, lockTable);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/service/AbstractLockAction.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc.service;
2 |
3 | import com.aav.planner.model.LockParam;
4 | import com.aav.planner.service.lock.LockAction;
5 | import java.sql.ResultSet;
6 | import java.sql.SQLException;
7 | import java.sql.Timestamp;
8 | import org.springframework.lang.NonNull;
9 |
10 | public abstract class AbstractLockAction extends SqlExecutor implements LockAction {
11 |
12 | private final String table;
13 |
14 | protected AbstractLockAction(@org.springframework.lang.NonNull String schema,
15 | @NonNull String lockTable) {
16 | this.table = schema + "." + lockTable;
17 | }
18 |
19 | @Override
20 | public boolean lock(LockParam param) {
21 | String query = "INSERT INTO " + table + " (name, lock_until, host) VALUES (?,?,?)";
22 | return executeQuery(query, statement -> {
23 | statement.setString(1, param.getName());
24 | statement.setTimestamp(2, Timestamp.from(param.getLockUntil()));
25 | statement.setString(3, param.getHost());
26 | return statement.executeUpdate() > 0;
27 | }, this::insertExceptionWithOutStackTrace);
28 | }
29 |
30 | @Override
31 | public boolean updateLock(LockParam param) {
32 | String query = "UPDATE " + table + " SET lock_until = ? WHERE name = ? AND host = ?";
33 | return executeQuery(query, statement -> {
34 | statement.setTimestamp(1, Timestamp.from(param.getLockUntil()));
35 | statement.setString(2, param.getName());
36 | statement.setString(3, param.getHost());
37 | return statement.executeUpdate() > 0;
38 | }, this::updateExceptionWithOutStackTrace);
39 | }
40 |
41 | @Override
42 | public boolean unlock(LockParam param) {
43 | String query = "DELETE FROM " + table + " WHERE name = ?";
44 | return executeQuery(query, statement -> {
45 | statement.setString(1, param.getName());
46 | return statement.executeUpdate() > 0;
47 | }, this::deleteExceptionWithOutStackTrace);
48 | }
49 |
50 | @Override
51 | public LockParam getLockInfo(LockParam param) {
52 | String query = "SELECT * FROM " + table + " WHERE name = ?";
53 | return executeQuery(query, statement -> {
54 | statement.setString(1, param.getName());
55 | return parseResult(statement.executeQuery());
56 | }, this::handleQueryLockInfoException);
57 | }
58 |
59 | private LockParam parseResult(ResultSet resultSet) throws SQLException {
60 | LockParam response = new LockParam();
61 | if (resultSet.next()) {
62 | response.setName(resultSet.getString(1));
63 | Timestamp timestamp = resultSet.getTimestamp(2);
64 | response.setLockUntil(timestamp.toInstant());
65 | response.setHost(resultSet.getString(3));
66 | }
67 | return response;
68 | }
69 |
70 |
71 | private LockParam handleQueryLockInfoException(SQLException e) {
72 | log.error(
73 | "An exception occurred while getting data from the database about the current lock", e);
74 | return null;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/service/AbstractLogAction.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc.service;
2 |
3 | import com.aav.planner.model.ScheduleParams;
4 | import com.aav.planner.service.log.LogAction;
5 | import com.fasterxml.jackson.core.JsonProcessingException;
6 | import com.fasterxml.jackson.databind.ObjectMapper;
7 | import java.sql.ResultSet;
8 | import java.sql.SQLException;
9 | import java.sql.Timestamp;
10 | import java.time.Instant;
11 | import java.util.HashMap;
12 | import java.util.Map;
13 | import java.util.Optional;
14 | import java.util.UUID;
15 | import java.util.function.Function;
16 | import java.util.function.Supplier;
17 | import lombok.NonNull;
18 |
19 | public abstract class AbstractLogAction extends AbstractLockAction implements LogAction {
20 |
21 | private final String table;
22 | private final ObjectMapper objectMapper = new ObjectMapper();
23 |
24 | protected AbstractLogAction(@NonNull String schema, @NonNull String logTable,
25 | @NonNull String lockTable) {
26 | super(schema, lockTable);
27 | this.table = schema + "." + logTable;
28 |
29 | }
30 |
31 | @Override
32 | public boolean create(UUID id, String name) {
33 | String query = "INSERT INTO " + table + " (id, name, start) VALUES (?,?,?)";
34 | return executeQuery(query, statement -> {
35 | statement.setObject(1, id);
36 | statement.setString(2, name);
37 | statement.setTimestamp(3, Timestamp.from(Instant.now()));
38 | return statement.executeUpdate() > 0;
39 | }, this::insertExceptionWithOutStackTrace);
40 | }
41 |
42 | @Override
43 | public boolean update(UUID id, Map info) {
44 | String query = "UPDATE " + table + " SET finish = ?, info = (?::json) WHERE id = ?";
45 | return executeQuery(query, statement -> {
46 | statement.setTimestamp(1, Timestamp.from(Instant.now()));
47 | statement.setString(2, mapToString(info));
48 | statement.setObject(3, id);
49 | return statement.executeUpdate() > 0;
50 | }, this::updateExceptionWithOutStackTrace);
51 | }
52 |
53 | @Override
54 | public ScheduleParams getLastRow(String name) {
55 | String query = "SELECT * FROM " + table + " WHERE finish IS NOT NULL AND name = ?"
56 | + " ORDER BY finish DESC limit 1";
57 | return executeQuery(query, statement -> {
58 | statement.setString(1, name);
59 | return parseResultLog(statement.executeQuery());
60 | }, this::handleGetLastRowException);
61 |
62 | }
63 |
64 | @Override
65 | public Optional replace(String name) {
66 | Optional last = getLastRowId(name);
67 |
68 | return Optional.of(last)
69 | .filter(Optional::isPresent)
70 | .map(Optional::get)
71 | .map(updateRow())
72 | .orElseGet(createRow(name));
73 | }
74 |
75 | private Supplier> createRow(String name) {
76 | return () -> {
77 | UUID id = UUID.randomUUID();
78 | if (create(id, name)) {
79 | return Optional.of(id);
80 | } else {
81 | return Optional.empty();
82 | }
83 | };
84 | }
85 |
86 | private Function> updateRow() {
87 | return el -> {
88 | UUID id = UUID.fromString(el);
89 | if (replaceLastRow(id)) {
90 | return Optional.of(id);
91 | }
92 | return Optional.empty();
93 | };
94 | }
95 |
96 | private boolean replaceLastRow(UUID id) {
97 | String query = "UPDATE " + table + " SET start = ?, finish = ?, info = ? "
98 | + "WHERE id = ?";
99 | return executeQuery(query, statement -> {
100 | statement.setTimestamp(1, Timestamp.from(Instant.now()));
101 | statement.setNull(2, java.sql.Types.NULL);
102 | statement.setNull(3, java.sql.Types.NULL);
103 | statement.setObject(4, id);
104 | return statement.executeUpdate() > 0;
105 | }, this::updateExceptionWithOutStackTrace);
106 | }
107 |
108 | private String mapToString(Map info) {
109 | if (info != null) {
110 | try {
111 | return objectMapper.writeValueAsString(info);
112 | } catch (JsonProcessingException e) {
113 | log.error("Map parse exception", e);
114 | }
115 | }
116 | return null;
117 | }
118 |
119 | private Optional getLastRowId(String name) {
120 | var query = "SELECT id FROM " + table + " WHERE finish IS NOT NULL AND name = ?"
121 | + " ORDER BY finish DESC limit 1";
122 | return executeQuery(query, statement -> {
123 | statement.setString(1, name);
124 | return getIdFromResultSet(statement.executeQuery());
125 | }, this::handleGetIdException);
126 | }
127 |
128 | private Optional getIdFromResultSet(ResultSet resultSet) throws SQLException {
129 | if (resultSet.next()) {
130 | return Optional.of(resultSet.getString(1));
131 | }
132 |
133 | return Optional.empty();
134 | }
135 |
136 | @SuppressWarnings(value = "unchecked")
137 | private ScheduleParams parseResultLog(ResultSet resultSet) throws SQLException {
138 |
139 | ScheduleParams response = new ScheduleParams();
140 | if (resultSet.next()) {
141 |
142 | response.setStart(resultSet.getTimestamp(3));
143 | response.setEnd(resultSet.getTimestamp(4));
144 | String info = resultSet.getString(5);
145 | if (info != null) {
146 | try {
147 | response.setInfo(objectMapper.readValue(info, HashMap.class));
148 | } catch (JsonProcessingException e) {
149 | log.error("Json parse exception", e);
150 | }
151 | }
152 | }
153 |
154 | return response;
155 | }
156 |
157 | ScheduleParams handleGetLastRowException(SQLException e) {
158 | log.error("unexpected exception when getting data about the last record in the database", e);
159 | return null;
160 | }
161 |
162 | Optional handleGetIdException(SQLException e) {
163 | log.error("unexpected exception when getting id about the last record in the database", e);
164 | return Optional.empty();
165 | }
166 |
167 | }
168 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/service/SqlExecutor.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc.service;
2 |
3 | import java.sql.PreparedStatement;
4 | import java.sql.SQLException;
5 | import java.util.function.Function;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | public abstract class SqlExecutor {
10 |
11 | protected final Logger log = LoggerFactory.getLogger(getClass());
12 |
13 | protected abstract T executeQuery(String query, SqlFunction body,
14 | Function exceptionHandler
15 | );
16 |
17 | boolean insertExceptionWithOutStackTrace(SQLException e) {
18 | log.error("exception while saving to db");
19 | log.error(e.getMessage());
20 | return false;
21 | }
22 |
23 | boolean insertException(SQLException e) {
24 | log.error("exception while saving to db", e);
25 | return false;
26 | }
27 |
28 | boolean deleteExceptionWithOutStackTrace(SQLException e) {
29 | log.error("exception on deletion from database");
30 | log.error(e.getMessage());
31 | return false;
32 | }
33 |
34 | boolean updateExceptionWithOutStackTrace(SQLException e) {
35 | log.error("exception when trying to update data in database");
36 | log.error(e.getMessage());
37 | return false;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/storage/jdbc/src/main/java/com/aav/jdbc/service/SqlFunction.java:
--------------------------------------------------------------------------------
1 | package com.aav.jdbc.service;
2 |
3 | import java.sql.SQLException;
4 |
5 | @FunctionalInterface
6 | public interface SqlFunction {
7 |
8 | R apply(T t) throws SQLException;
9 |
10 | }
--------------------------------------------------------------------------------
/storage/jdbc/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------