├── .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 | ![Apache License 2](https://img.shields.io/badge/license-ASF2-blue.svg) 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 | --------------------------------------------------------------------------------