diff --git a/pom.xml b/pom.xml index 2d8e109..300a087 100644 --- a/pom.xml +++ b/pom.xml @@ -33,14 +33,29 @@ org.springframework.boot - spring-boot-starter-test - test + spring-boot-starter-data-jpa + + + com.mysql + mysql-connector-j + runtime + + + net.javacrumbs.shedlock + shedlock-provider-jdbc + 5.15.1 org.projectlombok lombok true + + org.testng + testng + RELEASE + compile + 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/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/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/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..cfdcec0 --- /dev/null +++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java @@ -0,0 +1,82 @@ +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 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.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class TaskSchedulingServiceImpl implements TaskSchedulingService { + private final TaskScheduler taskScheduler; + private final LockProvider lockProvider; + private final ScheduleTaskRepository scheduleTaskRepository; + private final Map> tasks = new ConcurrentHashMap<>(); + + @PostConstruct + public void initializeScheduledTasks() { + scheduleTaskRepository.findAllByIsActiveTrue().forEach(this::scheduleTask); + } + + + @Override + public void scheduleTask(ScheduleTask scheduleTask) { + 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); + LockAssert.TestHelper.makeAllAssertsPass(true); + try { + lock.ifPresent(simpleLock -> { + try { + LockAssert.assertLocked(); + // Execute the actual task logic here + log.info("Executing Task {}", lockName); + } catch (Exception e) { + log.error("Error executing task: {}", scheduleTask.getId(), e); + } + }); + } finally { + lock.ifPresent(SimpleLock::unlock); + log.info("Release Task {}", lockName); + } + }; + + 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 a494062..87719d9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,19 @@ spring: - profiles: - active: local application: name: QuickStartSpringBoot + datasource: + url: jdbc:mysql://coofun.local:3306/schedular + username: root + password: root + jpa: + hibernate: + ddl-auto: update + show-sql: true server: port: 8080 +app: + default: + minute: 00 + hour: 03 + 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() { - } - -}