From 0be69e9e04574ee36d9cfe09cc556a9beff7a86c Mon Sep 17 00:00:00 2001 From: Faiz Akram <156657523+faizorg@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:30:23 +0530 Subject: [PATCH 1/8] added jar file --- pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pom.xml b/pom.xml index 2adc8ca..841d170 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,15 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.mysql + mysql-connector-j + runtime + org.springframework.boot spring-boot-starter-test From 8734e3aa5a8adc1ffdb0a78d1286a1bf50c70397 Mon Sep 17 00:00:00 2001 From: Faiz Akram <156657523+faizorg@users.noreply.github.com> Date: Tue, 2 Apr 2024 07:48:48 +0530 Subject: [PATCH 2/8] Schedule Tasks Dynamically --- pom.xml | 8 ++ src/main/java/com/app/config/RedisConfig.java | 41 ++++++++++ .../java/com/app/config/SchedulerConfig.java | 21 +++++ .../app/controller/ScheduleController.java | 30 +++++++ src/main/java/com/app/dto/Schedule.java | 14 ++++ src/main/java/com/app/model/ScheduleTask.java | 45 +++++++++++ src/main/java/com/app/model/ScheduleType.java | 5 ++ .../repository/ScheduleTaskRepository.java | 10 +++ .../com/app/service/RedisLockService.java | 15 ++++ .../com/app/service/RedisLockServiceImpl.java | 55 +++++++++++++ .../java/com/app/service/ScheduleService.java | 11 +++ .../com/app/service/ScheduleServiceImpl.java | 58 ++++++++++++++ .../app/service/TaskSchedulingService.java | 12 +++ .../service/TaskSchedulingServiceImpl.java | 72 +++++++++++++++++ .../com/app/utils/CreateCronExpression.java | 79 +++++++++++++++++++ src/main/resources/application.yml | 24 +++++- 16 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/app/config/RedisConfig.java create mode 100644 src/main/java/com/app/config/SchedulerConfig.java create mode 100644 src/main/java/com/app/controller/ScheduleController.java create mode 100644 src/main/java/com/app/dto/Schedule.java create mode 100644 src/main/java/com/app/model/ScheduleTask.java create mode 100644 src/main/java/com/app/model/ScheduleType.java create mode 100644 src/main/java/com/app/repository/ScheduleTaskRepository.java create mode 100644 src/main/java/com/app/service/RedisLockService.java create mode 100644 src/main/java/com/app/service/RedisLockServiceImpl.java create mode 100644 src/main/java/com/app/service/ScheduleService.java create mode 100644 src/main/java/com/app/service/ScheduleServiceImpl.java create mode 100644 src/main/java/com/app/service/TaskSchedulingService.java create mode 100644 src/main/java/com/app/service/TaskSchedulingServiceImpl.java create mode 100644 src/main/java/com/app/utils/CreateCronExpression.java diff --git a/pom.xml b/pom.xml index 0346b33..b61d0dd 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,14 @@ mysql-connector-j runtime + + org.springframework.boot + spring-boot-starter-data-redis + + + redis.clients + jedis + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/app/config/RedisConfig.java b/src/main/java/com/app/config/RedisConfig.java new file mode 100644 index 0000000..a6f1af3 --- /dev/null +++ b/src/main/java/com/app/config/RedisConfig.java @@ -0,0 +1,41 @@ +package com.app.config; + + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.StringUtils; + +@Configuration +public class RedisConfig { + private String hostName; + private Integer port; + private String password; + + public RedisConfig(@Value("${spring.data.redis.host}") String hostName, @Value("${spring.data.redis.port}") + Integer port, @Value("${spring.data.redis.password}") String password) { + this.hostName = hostName; + this.port = port; + this.password = password; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(hostName, port); + if (!StringUtils.isEmpty(password)) + redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); + return new JedisConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + return template; + } +} diff --git a/src/main/java/com/app/config/SchedulerConfig.java b/src/main/java/com/app/config/SchedulerConfig.java new file mode 100644 index 0000000..9a89c1d --- /dev/null +++ b/src/main/java/com/app/config/SchedulerConfig.java @@ -0,0 +1,21 @@ +package com.app.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(10); // Define the pool size + scheduler.setThreadNamePrefix("scheduled-task-"); + scheduler.initialize(); + return scheduler; + } +} diff --git a/src/main/java/com/app/controller/ScheduleController.java b/src/main/java/com/app/controller/ScheduleController.java new file mode 100644 index 0000000..e391a00 --- /dev/null +++ b/src/main/java/com/app/controller/ScheduleController.java @@ -0,0 +1,30 @@ +package com.app.controller; + +import com.app.dto.Schedule; +import com.app.service.ScheduleService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("schedule") +public class ScheduleController { + + private final ScheduleService ScheduleService; + + @PostMapping("add") + public ResponseEntity addSchedule(@RequestBody Schedule schedule) { + return new ResponseEntity<>(ScheduleService.addSchedule(schedule), HttpStatus.OK); + } + + @PutMapping("update") + public ResponseEntity updateSchedule(@RequestBody Schedule schedule) { + return new ResponseEntity<>(ScheduleService.updateSchedule(schedule), HttpStatus.OK); + } + @DeleteMapping("delete") + public ResponseEntity deleteSchedule(@RequestParam Long id) { + return new ResponseEntity<>(ScheduleService.deleteSchedule(id), HttpStatus.OK); + } +} diff --git a/src/main/java/com/app/dto/Schedule.java b/src/main/java/com/app/dto/Schedule.java new file mode 100644 index 0000000..d5fa7ca --- /dev/null +++ b/src/main/java/com/app/dto/Schedule.java @@ -0,0 +1,14 @@ +package com.app.dto; + +import com.app.model.ScheduleType; +import lombok.Data; + +@Data +public class Schedule { + private Long id; + private String name; + private String parameters; + private Boolean isActive; + private Integer day; + private ScheduleType scheduleType; +} diff --git a/src/main/java/com/app/model/ScheduleTask.java b/src/main/java/com/app/model/ScheduleTask.java new file mode 100644 index 0000000..995b32a --- /dev/null +++ b/src/main/java/com/app/model/ScheduleTask.java @@ -0,0 +1,45 @@ +package com.app.model; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "schedule") +@Data +@DynamicInsert +@DynamicUpdate +public class ScheduleTask { + @Id + @Column(name = "id", updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ScheduleType scheduleType; + + @Column + private LocalDateTime scheduleTime; + + @Column(columnDefinition = "TEXT", nullable = false) + private String customScheduleDetails; + + private LocalDateTime lastRun; + + @Column + private LocalDateTime nextRun; + + @Column + private Boolean isActive; + + @Column(columnDefinition = "TEXT") + private String parameters; + +} diff --git a/src/main/java/com/app/model/ScheduleType.java b/src/main/java/com/app/model/ScheduleType.java new file mode 100644 index 0000000..cd495f3 --- /dev/null +++ b/src/main/java/com/app/model/ScheduleType.java @@ -0,0 +1,5 @@ +package com.app.model; + +public enum ScheduleType { + MINUTE, HOUR, DAILY, WEEKLY, MONTHLY; +} diff --git a/src/main/java/com/app/repository/ScheduleTaskRepository.java b/src/main/java/com/app/repository/ScheduleTaskRepository.java new file mode 100644 index 0000000..d500b37 --- /dev/null +++ b/src/main/java/com/app/repository/ScheduleTaskRepository.java @@ -0,0 +1,10 @@ +package com.app.repository; + +import com.app.model.ScheduleTask; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ScheduleTaskRepository extends JpaRepository { + List findAllByIsActiveTrue(); +} diff --git a/src/main/java/com/app/service/RedisLockService.java b/src/main/java/com/app/service/RedisLockService.java new file mode 100644 index 0000000..3ba88bf --- /dev/null +++ b/src/main/java/com/app/service/RedisLockService.java @@ -0,0 +1,15 @@ +package com.app.service; + +import java.util.concurrent.TimeUnit; + +public interface RedisLockService { + + boolean shouldDeferExecution(String taskKey); + + boolean extendLock(String lockKey, String lockValue, long extensionTime); + + void updateLastExecutionTime(String taskKey); + + boolean acquireLock(String lockKey, String lockValue, long expireTime); + void releaseLock(String lockKey, String lockValue); +} diff --git a/src/main/java/com/app/service/RedisLockServiceImpl.java b/src/main/java/com/app/service/RedisLockServiceImpl.java new file mode 100644 index 0000000..4b3d76c --- /dev/null +++ b/src/main/java/com/app/service/RedisLockServiceImpl.java @@ -0,0 +1,55 @@ +package com.app.service; + +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +@Service +@AllArgsConstructor +public class RedisLockServiceImpl implements RedisLockService { + private final StringRedisTemplate stringRedisTemplate; + private final long defermentPeriod = 5000; + @Override + public boolean shouldDeferExecution(String taskKey) { + ValueOperations ops = stringRedisTemplate.opsForValue(); + String lastExecutionTimeStr = ops.get(taskKey + ":lastExecution"); + if (lastExecutionTimeStr != null) { + long lastExecutionTime = Long.parseLong(lastExecutionTimeStr); + long currentTime = Instant.now().toEpochMilli(); + return (currentTime - lastExecutionTime) < defermentPeriod; + } + return false; + } + @Override + public boolean extendLock(String lockKey, String lockValue, long extensionTime) { + ValueOperations ops = stringRedisTemplate.opsForValue(); + String currentValue = ops.get(lockKey); + if (lockValue.equals(currentValue)) { + stringRedisTemplate.expire(lockKey, extensionTime, TimeUnit.MILLISECONDS); + return true; + } + return false; + } + @Override + public void updateLastExecutionTime(String taskKey) { + ValueOperations ops = stringRedisTemplate.opsForValue(); + ops.set(taskKey + ":lastExecution", String.valueOf(Instant.now().toEpochMilli())); + } + @Override + public boolean acquireLock(String lockKey, String lockValue, long timeout) { + ValueOperations ops = stringRedisTemplate.opsForValue(); + Boolean success = ops.setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS); + return Boolean.TRUE.equals(success); + } + @Override + public void releaseLock(String lockKey, String lockValue) { + String currentValue = stringRedisTemplate.opsForValue().get(lockKey); + if (lockValue.equals(currentValue)) { + stringRedisTemplate.delete(lockKey); + } + } +} diff --git a/src/main/java/com/app/service/ScheduleService.java b/src/main/java/com/app/service/ScheduleService.java new file mode 100644 index 0000000..2ab9218 --- /dev/null +++ b/src/main/java/com/app/service/ScheduleService.java @@ -0,0 +1,11 @@ +package com.app.service; + +import com.app.dto.Schedule; + +public interface ScheduleService { + Schedule addSchedule(Schedule schedule); + + Schedule deleteSchedule(Long id); + + Schedule updateSchedule(Schedule schedule); +} diff --git a/src/main/java/com/app/service/ScheduleServiceImpl.java b/src/main/java/com/app/service/ScheduleServiceImpl.java new file mode 100644 index 0000000..3f826ee --- /dev/null +++ b/src/main/java/com/app/service/ScheduleServiceImpl.java @@ -0,0 +1,58 @@ +package com.app.service; + +import com.app.dto.Schedule; +import com.app.model.ScheduleTask; +import com.app.repository.ScheduleTaskRepository; +import com.app.utils.CreateCronExpression; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class ScheduleServiceImpl implements ScheduleService { + private final CreateCronExpression createCronExpression; + private final TaskSchedulingService taskSchedulingService; + private final ScheduleTaskRepository scheduleTaskRepository; + + @Override + public Schedule addSchedule(Schedule schedule) { + String cron = switch (schedule.getScheduleType()) { + case MINUTE -> createCronExpression.generateEveryMinuteCronExpression(); + case HOUR -> createCronExpression.generateHourlyCronExpression(); + case DAILY -> createCronExpression.generateDailyCronExpression(); + case WEEKLY -> createCronExpression.generateWeeklyCronExpression(schedule.getDay()); + case MONTHLY -> createCronExpression.generateMonthlyCronExpression(schedule.getDay()); + }; + ScheduleTask scheduleTask = new ScheduleTask(); + scheduleTask.setCustomScheduleDetails(cron); + scheduleTask.setName(schedule.getName()); + scheduleTask.setScheduleType(schedule.getScheduleType()); + scheduleTask.setIsActive(schedule.getIsActive()); + scheduleTask = scheduleTaskRepository.save(scheduleTask); + log.info(cron); + schedule.setId(scheduleTask.getId()); + taskSchedulingService.scheduleTask(scheduleTask); + return schedule; + } + + @Override + public Schedule deleteSchedule(Long id) { + Optional scheduleTaskOptional = scheduleTaskRepository.findById(id); + if(scheduleTaskOptional.isPresent()){ + ScheduleTask scheduleTask = scheduleTaskOptional.get(); + scheduleTask.setIsActive(false); + scheduleTask = scheduleTaskRepository.save(scheduleTask); + taskSchedulingService.cancelScheduledTask(scheduleTask.getId()); + } + return null; + } + + @Override + public Schedule updateSchedule(Schedule schedule) { + return null; + } +} diff --git a/src/main/java/com/app/service/TaskSchedulingService.java b/src/main/java/com/app/service/TaskSchedulingService.java new file mode 100644 index 0000000..a0d92e7 --- /dev/null +++ b/src/main/java/com/app/service/TaskSchedulingService.java @@ -0,0 +1,12 @@ +package com.app.service; + +import com.app.model.ScheduleTask; + +public interface TaskSchedulingService { + + void scheduleTask(ScheduleTask scheduleTask); + + void cancelScheduledTask(Long taskId); + + void addOrUpdateTask(ScheduleTask scheduleTask); +} diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java new file mode 100644 index 0000000..81e3392 --- /dev/null +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -0,0 +1,72 @@ +package com.app.service; + + +import com.app.model.ScheduleTask; +import com.app.repository.ScheduleTaskRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class TaskSchedulingServiceImpl implements TaskSchedulingService { + private final RedisLockService redisLockService; + private final TaskScheduler taskScheduler; + private final ScheduleTaskRepository scheduleTaskRepository; + private Map> tasks = new ConcurrentHashMap<>(); + + @PostConstruct + public void initializeScheduledTasks() { + scheduleTaskRepository.findAllByIsActiveTrue().forEach(this::scheduleTask); + } + + + @Override + public void scheduleTask(ScheduleTask scheduleTask) { + String lockKey = "lock:" + scheduleTask.getId(); + Runnable taskWrapper = () -> { + String lockValue = UUID.randomUUID().toString(); + if (redisLockService.shouldDeferExecution(lockKey)) { + log.info("Execution deferred due to recent execution."); + return; + } else if (!redisLockService.acquireLock(lockKey, lockValue, 30000)) { + log.info("Task " + scheduleTask.getId() + " is already running. Skipping execution."); + return; + } + try { + // Task execution logic here + log.info("Executing Task " + scheduleTask.getId()); + redisLockService.updateLastExecutionTime(lockKey); + } finally { + redisLockService.releaseLock(lockKey, lockValue); + } + }; + ScheduledFuture future = taskScheduler.schedule(taskWrapper, new CronTrigger(scheduleTask.getCustomScheduleDetails())); + tasks.put(scheduleTask.getId(), future); + } + + @Override + public void cancelScheduledTask(Long taskId) { + ScheduledFuture future = tasks.get(taskId); + if (future != null) { + future.cancel(false); + tasks.remove(taskId); + } + } + + @Override + public void addOrUpdateTask(ScheduleTask scheduleTask) { + cancelScheduledTask(scheduleTask.getId()); // Cancel the current task if it's already scheduled + scheduleTask(scheduleTask); // Reschedule it + } + +} diff --git a/src/main/java/com/app/utils/CreateCronExpression.java b/src/main/java/com/app/utils/CreateCronExpression.java new file mode 100644 index 0000000..ea8c980 --- /dev/null +++ b/src/main/java/com/app/utils/CreateCronExpression.java @@ -0,0 +1,79 @@ +package com.app.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +@Component +public class CreateCronExpression { + + private Integer hour; + private Integer minute; + + public CreateCronExpression(@Value("${app.default.hour}") Integer hour, @Value("${app.default.minute}") Integer minute) { + this.hour = hour; + this.minute = minute; + } + + public String generateEveryMinuteCronExpression() { + // This will run at the start of every minute + return "0 * * * * ?"; + } + + + public String generateHourlyCronExpression() { + // Validate the input + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Minute must be between 0 and 59"); + } + + // Construct the cron expression + // This will run at the start of the specified minute past every hour + return String.format(Locale.US, "0 %d * * * ?", minute); + } + + public String generateDailyCronExpression() { + // Validate the input + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException("Hour must be between 0 and 23"); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Minute must be between 0 and 59"); + } + // Construct the cron expression + return String.format(Locale.US, "%d %d * * *", minute, hour); + } + + public String generateWeeklyCronExpression(int dayOfWeek) { + // Validate the input + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException("Hour must be between 0 and 23"); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Minute must be between 0 and 59"); + } + if (dayOfWeek < 1 || dayOfWeek > 7) { + throw new IllegalArgumentException("Day of week must be between 1 (Sunday) and 7 (Saturday)"); + } + // Construct the cron expression with the second field included + return String.format(Locale.US, "0 %d %d * * %d", minute, hour, dayOfWeek - 1); + } + + + public String generateMonthlyCronExpression(int dayOfMonth) { + // Validate the input + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException("Hour must be between 0 and 23"); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Minute must be between 0 and 59"); + } + if (dayOfMonth < 1 || dayOfMonth > 31) { + throw new IllegalArgumentException("Day of month must be between 1 and 31"); + } + + // Construct the cron expression + return String.format(Locale.US, "%d %d %d * *", minute, hour, dayOfMonth); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 56fbd1d..7d44b54 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,24 @@ spring: - application: - name: QuickStartSpringBoot + data: + redis: + host: localhost + port: 6379 + password: + application: + name: QuickStartSpringBoot + datasource: + url: jdbc:mysql://coofun.local:3306/test + username: root + password: root + jpa: + hibernate: + ddl-auto: update + show-sql: true server: - port: 8080 + port: 8080 + +app: + default: + minute: 00 + hour: 03 From 329f7654a3dc9550822aabcc0028bd6677ffff5d Mon Sep 17 00:00:00 2001 From: Faiz Akram <156657523+faizorg@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:00:28 +0530 Subject: [PATCH 3/8] Schedule Tasks Dynamically --- pom.xml | 11 +++ src/main/java/com/app/config/RedisConfig.java | 6 ++ .../com/app/service/RedisLockService.java | 15 ---- .../com/app/service/RedisLockServiceImpl.java | 55 -------------- .../service/TaskSchedulingServiceImpl.java | 71 +++++++++++++------ 5 files changed, 67 insertions(+), 91 deletions(-) delete mode 100644 src/main/java/com/app/service/RedisLockService.java delete mode 100644 src/main/java/com/app/service/RedisLockServiceImpl.java diff --git a/pom.xml b/pom.xml index b61d0dd..22e11bf 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,17 @@ redis.clients jedis + + net.javacrumbs.shedlock + shedlock-spring + 4.0.0 + + + net.javacrumbs.shedlock + shedlock-provider-redis-spring + 4.0.0 + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/app/config/RedisConfig.java b/src/main/java/com/app/config/RedisConfig.java index a6f1af3..fc67648 100644 --- a/src/main/java/com/app/config/RedisConfig.java +++ b/src/main/java/com/app/config/RedisConfig.java @@ -1,6 +1,8 @@ package com.app.config; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,4 +40,8 @@ public RedisTemplate redisTemplate() { template.setConnectionFactory(redisConnectionFactory()); return template; } + @Bean + public LockProvider lockProvider() { + return new RedisLockProvider(redisConnectionFactory()); + } } diff --git a/src/main/java/com/app/service/RedisLockService.java b/src/main/java/com/app/service/RedisLockService.java deleted file mode 100644 index 3ba88bf..0000000 --- a/src/main/java/com/app/service/RedisLockService.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.app.service; - -import java.util.concurrent.TimeUnit; - -public interface RedisLockService { - - boolean shouldDeferExecution(String taskKey); - - boolean extendLock(String lockKey, String lockValue, long extensionTime); - - void updateLastExecutionTime(String taskKey); - - boolean acquireLock(String lockKey, String lockValue, long expireTime); - void releaseLock(String lockKey, String lockValue); -} diff --git a/src/main/java/com/app/service/RedisLockServiceImpl.java b/src/main/java/com/app/service/RedisLockServiceImpl.java deleted file mode 100644 index 4b3d76c..0000000 --- a/src/main/java/com/app/service/RedisLockServiceImpl.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.app.service; - -import lombok.AllArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; - -@Service -@AllArgsConstructor -public class RedisLockServiceImpl implements RedisLockService { - private final StringRedisTemplate stringRedisTemplate; - private final long defermentPeriod = 5000; - @Override - public boolean shouldDeferExecution(String taskKey) { - ValueOperations ops = stringRedisTemplate.opsForValue(); - String lastExecutionTimeStr = ops.get(taskKey + ":lastExecution"); - if (lastExecutionTimeStr != null) { - long lastExecutionTime = Long.parseLong(lastExecutionTimeStr); - long currentTime = Instant.now().toEpochMilli(); - return (currentTime - lastExecutionTime) < defermentPeriod; - } - return false; - } - @Override - public boolean extendLock(String lockKey, String lockValue, long extensionTime) { - ValueOperations ops = stringRedisTemplate.opsForValue(); - String currentValue = ops.get(lockKey); - if (lockValue.equals(currentValue)) { - stringRedisTemplate.expire(lockKey, extensionTime, TimeUnit.MILLISECONDS); - return true; - } - return false; - } - @Override - public void updateLastExecutionTime(String taskKey) { - ValueOperations ops = stringRedisTemplate.opsForValue(); - ops.set(taskKey + ":lastExecution", String.valueOf(Instant.now().toEpochMilli())); - } - @Override - public boolean acquireLock(String lockKey, String lockValue, long timeout) { - ValueOperations ops = stringRedisTemplate.opsForValue(); - Boolean success = ops.setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS); - return Boolean.TRUE.equals(success); - } - @Override - public void releaseLock(String lockKey, String lockValue) { - String currentValue = stringRedisTemplate.opsForValue().get(lockKey); - if (lockValue.equals(currentValue)) { - stringRedisTemplate.delete(lockKey); - } - } -} diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java index 81e3392..ae3e7c2 100644 --- a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -6,12 +6,17 @@ import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import net.javacrumbs.shedlock.core.LockAssert; +import net.javacrumbs.shedlock.core.LockConfiguration; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.core.SimpleLock; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; +import java.time.Instant; import java.util.Map; -import java.util.UUID; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; @@ -19,8 +24,8 @@ @RequiredArgsConstructor @Log4j2 public class TaskSchedulingServiceImpl implements TaskSchedulingService { - private final RedisLockService redisLockService; private final TaskScheduler taskScheduler; + private final LockProvider lockProvider; private final ScheduleTaskRepository scheduleTaskRepository; private Map> tasks = new ConcurrentHashMap<>(); @@ -32,28 +37,52 @@ public void initializeScheduledTasks() { @Override public void scheduleTask(ScheduleTask scheduleTask) { - String lockKey = "lock:" + scheduleTask.getId(); - Runnable taskWrapper = () -> { - String lockValue = UUID.randomUUID().toString(); - if (redisLockService.shouldDeferExecution(lockKey)) { - log.info("Execution deferred due to recent execution."); - return; - } else if (!redisLockService.acquireLock(lockKey, lockValue, 30000)) { - log.info("Task " + scheduleTask.getId() + " is already running. Skipping execution."); - return; - } - try { - // Task execution logic here - log.info("Executing Task " + scheduleTask.getId()); - redisLockService.updateLastExecutionTime(lockKey); - } finally { - redisLockService.releaseLock(lockKey, lockValue); - } - }; - ScheduledFuture future = taskScheduler.schedule(taskWrapper, new CronTrigger(scheduleTask.getCustomScheduleDetails())); +// Runnable taskWrapper = () -> { +// String lockName = scheduleTask.getName()+" - "+scheduleTask.getId(); +// Instant lockAtMostUntil = Instant.now().plusSeconds(600); // Lock for 10 minutes +// Instant lockAtLeastUntil = Instant.now().plusSeconds(300); +// LockConfiguration config = new LockConfiguration(lockName, lockAtMostUntil, lockAtLeastUntil); +// Optional lock = lockProvider.lock(config); +// try { +// lock.ifPresentOrElse(simpleLock -> { +// try { +// LockAssert.assertLocked(); +// // Execute the actual task logic here +// log.info("Executing Task " + scheduleTask.getId()); +// } catch (Exception e) { +// log.error("Error executing task: " + scheduleTask.getId(), e); +// } +// }, () -> { +// log.info("Could not acquire lock for task " + scheduleTask.getId()); +// }); +// } finally { +// lock.ifPresent(SimpleLock::unlock); +// } +// }; + ScheduledFuture future = taskScheduler.schedule(() -> this.executeTask(scheduleTask), new CronTrigger(scheduleTask.getCustomScheduleDetails())); tasks.put(scheduleTask.getId(), future); } + private void executeTask(ScheduleTask scheduleTask) { + String lockName = "myDynamicTask"; + Instant lockAtMostUntil = Instant.now().plusSeconds(60); // Lock for 10 minutes + Instant lockAtLeastUntil = Instant.now().plusSeconds(30); // Minimum lock time + + LockConfiguration config = new LockConfiguration(lockName, lockAtMostUntil, lockAtLeastUntil); + Optional lock = lockProvider.lock(config); + try { + lock.ifPresentOrElse(simpleLock -> { + LockAssert.assertLocked(); + // Task logic here + log.info("Executing Task " + scheduleTask.getId()); + }, () -> { + System.out.println("Could not acquire lock for task"); + }); + } finally { + lock.ifPresent(SimpleLock::unlock); + } + } + @Override public void cancelScheduledTask(Long taskId) { ScheduledFuture future = tasks.get(taskId); From a598ea62f1ca68807af46a71deb58cc44d9384e9 Mon Sep 17 00:00:00 2001 From: Faiz Akram <156657523+faizorg@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:00:44 +0530 Subject: [PATCH 4/8] Schedule Tasks Dynamically --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 22e11bf..659e264 100644 --- a/pom.xml +++ b/pom.xml @@ -41,12 +41,12 @@ net.javacrumbs.shedlock shedlock-spring - 4.0.0 + 5.12.0 net.javacrumbs.shedlock shedlock-provider-redis-spring - 4.0.0 + 5.12.0 From ab878e81e893b2ea879a340a856126559608b9de Mon Sep 17 00:00:00 2001 From: Faiz Akram <156657523+faizorg@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:13:52 +0530 Subject: [PATCH 5/8] Schedule Tasks Dynamically --- .../service/TaskSchedulingServiceImpl.java | 65 +++++++------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java index ae3e7c2..0bbebb5 100644 --- a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -14,6 +14,7 @@ import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; +import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.Optional; @@ -37,50 +38,28 @@ public void initializeScheduledTasks() { @Override public void scheduleTask(ScheduleTask scheduleTask) { -// Runnable taskWrapper = () -> { -// String lockName = scheduleTask.getName()+" - "+scheduleTask.getId(); -// Instant lockAtMostUntil = Instant.now().plusSeconds(600); // Lock for 10 minutes -// Instant lockAtLeastUntil = Instant.now().plusSeconds(300); -// LockConfiguration config = new LockConfiguration(lockName, lockAtMostUntil, lockAtLeastUntil); -// Optional lock = lockProvider.lock(config); -// try { -// lock.ifPresentOrElse(simpleLock -> { -// try { -// LockAssert.assertLocked(); -// // Execute the actual task logic here -// log.info("Executing Task " + scheduleTask.getId()); -// } catch (Exception e) { -// log.error("Error executing task: " + scheduleTask.getId(), e); -// } -// }, () -> { -// log.info("Could not acquire lock for task " + scheduleTask.getId()); -// }); -// } finally { -// lock.ifPresent(SimpleLock::unlock); -// } -// }; - ScheduledFuture future = taskScheduler.schedule(() -> this.executeTask(scheduleTask), new CronTrigger(scheduleTask.getCustomScheduleDetails())); - tasks.put(scheduleTask.getId(), future); - } - - private void executeTask(ScheduleTask scheduleTask) { - String lockName = "myDynamicTask"; - Instant lockAtMostUntil = Instant.now().plusSeconds(60); // Lock for 10 minutes - Instant lockAtLeastUntil = Instant.now().plusSeconds(30); // Minimum lock time + Runnable taskWrapper = () -> { + String lockName = scheduleTask.getName()+" - "+scheduleTask.getId(); + Instant createdAt = Instant.now(); + LockConfiguration config = new LockConfiguration(createdAt, lockName, Duration.ofMinutes(1), Duration.ofSeconds(10)); + Optional lock = lockProvider.lock(config); + try { + lock.ifPresent(simpleLock -> { + try { + LockAssert.assertLocked(); + // Execute the actual task logic here + log.info("Executing Task " + scheduleTask.getId()); + } catch (Exception e) { + log.error("Error executing task: " + scheduleTask.getId(), e); + } + }); + } finally { + lock.ifPresent(SimpleLock::unlock); + } + }; - LockConfiguration config = new LockConfiguration(lockName, lockAtMostUntil, lockAtLeastUntil); - Optional lock = lockProvider.lock(config); - try { - lock.ifPresentOrElse(simpleLock -> { - LockAssert.assertLocked(); - // Task logic here - log.info("Executing Task " + scheduleTask.getId()); - }, () -> { - System.out.println("Could not acquire lock for task"); - }); - } finally { - lock.ifPresent(SimpleLock::unlock); - } + ScheduledFuture future = taskScheduler.schedule(taskWrapper, new CronTrigger(scheduleTask.getCustomScheduleDetails())); + tasks.put(scheduleTask.getId(), future); } @Override From 047be00630b0e17ce4ac6282d734c687bb27a462 Mon Sep 17 00:00:00 2001 From: "IIPL\\14261" Date: Wed, 4 Sep 2024 22:48:01 +0530 Subject: [PATCH 6/8] Correct task locking logic --- pom.xml | 29 ++++-------- src/main/java/com/app/config/RedisConfig.java | 47 ------------------- .../com/app/config/ShedLockConfiguration.java | 16 +++++++ .../service/TaskSchedulingServiceImpl.java | 6 ++- src/main/resources/application.yml | 7 +-- .../QuickStartSpringBootApplicationTests.java | 13 ----- 6 files changed, 29 insertions(+), 89 deletions(-) delete mode 100644 src/main/java/com/app/config/RedisConfig.java create mode 100644 src/main/java/com/app/config/ShedLockConfiguration.java delete mode 100644 src/test/java/com/app/QuickStartSpringBootApplicationTests.java diff --git a/pom.xml b/pom.xml index 659e264..df73e86 100644 --- a/pom.xml +++ b/pom.xml @@ -30,35 +30,22 @@ mysql-connector-j runtime - - org.springframework.boot - spring-boot-starter-data-redis - - - redis.clients - jedis - - - net.javacrumbs.shedlock - shedlock-spring - 5.12.0 - net.javacrumbs.shedlock - shedlock-provider-redis-spring - 5.12.0 - - - - org.springframework.boot - spring-boot-starter-test - test + shedlock-provider-jdbc + 5.15.1 org.projectlombok lombok true + + org.testng + testng + RELEASE + compile + diff --git a/src/main/java/com/app/config/RedisConfig.java b/src/main/java/com/app/config/RedisConfig.java deleted file mode 100644 index fc67648..0000000 --- a/src/main/java/com/app/config/RedisConfig.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.app.config; - - -import net.javacrumbs.shedlock.core.LockProvider; -import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisPassword; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.util.StringUtils; - -@Configuration -public class RedisConfig { - private String hostName; - private Integer port; - private String password; - - public RedisConfig(@Value("${spring.data.redis.host}") String hostName, @Value("${spring.data.redis.port}") - Integer port, @Value("${spring.data.redis.password}") String password) { - this.hostName = hostName; - this.port = port; - this.password = password; - } - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(hostName, port); - if (!StringUtils.isEmpty(password)) - redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); - return new JedisConnectionFactory(redisStandaloneConfiguration); - } - - @Bean - public RedisTemplate redisTemplate() { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(redisConnectionFactory()); - return template; - } - @Bean - public LockProvider lockProvider() { - return new RedisLockProvider(redisConnectionFactory()); - } -} diff --git a/src/main/java/com/app/config/ShedLockConfiguration.java b/src/main/java/com/app/config/ShedLockConfiguration.java new file mode 100644 index 0000000..c80e269 --- /dev/null +++ b/src/main/java/com/app/config/ShedLockConfiguration.java @@ -0,0 +1,16 @@ +package com.app.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +@Configuration +public class ShedLockConfiguration { + + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcLockProvider(dataSource); + } +} diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java index 0bbebb5..96da133 100644 --- a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -43,18 +43,20 @@ public void scheduleTask(ScheduleTask scheduleTask) { Instant createdAt = Instant.now(); LockConfiguration config = new LockConfiguration(createdAt, lockName, Duration.ofMinutes(1), Duration.ofSeconds(10)); Optional lock = lockProvider.lock(config); + LockAssert.TestHelper.makeAllAssertsPass(true); try { lock.ifPresent(simpleLock -> { try { LockAssert.assertLocked(); // Execute the actual task logic here - log.info("Executing Task " + scheduleTask.getId()); + log.info("Executing Task {}", lockName); } catch (Exception e) { - log.error("Error executing task: " + scheduleTask.getId(), e); + log.error("Error executing task: {}", scheduleTask.getId(), e); } }); } finally { lock.ifPresent(SimpleLock::unlock); + log.info("Release Task {}", lockName); } }; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7d44b54..505c4c1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,13 +1,8 @@ spring: - data: - redis: - host: localhost - port: 6379 - password: application: name: QuickStartSpringBoot datasource: - url: jdbc:mysql://coofun.local:3306/test + url: jdbc:mysql://coofun.local:3306/schedular username: root password: root jpa: diff --git a/src/test/java/com/app/QuickStartSpringBootApplicationTests.java b/src/test/java/com/app/QuickStartSpringBootApplicationTests.java deleted file mode 100644 index 0137031..0000000 --- a/src/test/java/com/app/QuickStartSpringBootApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.app; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class QuickStartSpringBootApplicationTests { - - @Test - void contextLoads() { - } - -} From ab30306d60d3baad87ec7fe9a6fb15e1cffb2be6 Mon Sep 17 00:00:00 2001 From: "IIPL\\14261" Date: Wed, 4 Sep 2024 22:49:34 +0530 Subject: [PATCH 7/8] Update message --- src/main/java/com/app/service/TaskSchedulingServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java index 96da133..cfdcec0 100644 --- a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -28,7 +28,7 @@ public class TaskSchedulingServiceImpl implements TaskSchedulingService { private final TaskScheduler taskScheduler; private final LockProvider lockProvider; private final ScheduleTaskRepository scheduleTaskRepository; - private Map> tasks = new ConcurrentHashMap<>(); + private final Map> tasks = new ConcurrentHashMap<>(); @PostConstruct public void initializeScheduledTasks() { From 0edf2d5e0d6f97e19e6f3e1fdcb7423b27619325 Mon Sep 17 00:00:00 2001 From: "IIPL\\14261" Date: Wed, 4 Sep 2024 23:01:29 +0530 Subject: [PATCH 8/8] Update message --- src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 505c4c1..9653425 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,7 +11,6 @@ spring: show-sql: true server: port: 8080 - app: default: minute: 00