Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds missing endpoints and polish #1594

Merged
merged 7 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scheduler/boot-scheduler-quartz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ You can also run the application using Maven as follows:
* Swagger UI: http://localhost:8080/swagger-ui.html
* Actuator Endpoint: http://localhost:8080/actuator
* PGAdmin : http://localhost:5050

* UI: [http://localhost:8080/index](http://localhost:8080/index)
4 changes: 4 additions & 0 deletions scheduler/boot-scheduler-quartz/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- webJars start -->
<!-- <dependency>-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -11,7 +12,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
private final ApplicationProperties properties;

@Override
public void addCorsMappings(CorsRegistry registry) {
public void addCorsMappings(@NonNull CorsRegistry registry) {
registry.addMapping(properties.getCors().getPathPattern())
.allowedMethods(properties.getCors().getAllowedMethods())
.allowedHeaders(properties.getCors().getAllowedHeaders())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.scheduler.quartz.job;

import com.scheduler.quartz.model.response.ScheduleJob;
import com.scheduler.quartz.service.OddEvenService;
import java.util.ArrayList;
import java.util.List;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
Expand All @@ -15,8 +12,6 @@ public class SampleJob implements Job {

private final OddEvenService oddEvenService;

public static final List<ScheduleJob> JOB_LIST = new ArrayList<>();

public SampleJob(OddEvenService oddEvenService) {
this.oddEvenService = oddEvenService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.quartz.TriggerBuilder.newTrigger;

import com.scheduler.quartz.model.response.ScheduleJob;
import java.util.UUID;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -22,10 +23,9 @@ JobDetail registerOddJob() {
.storeDurably()
.requestRecovery()
.build();
String jobId = String.valueOf(SampleJob.JOB_LIST.size() + 1);
String jobId = UUID.randomUUID().toString();
ScheduleJob scheduleJob = new ScheduleJob(jobId, "oddEvenJob", GROUP_NAME, null, null, "Sample OddEvenJob");
jobDetail.getJobDataMap().put("scheduleJob", jobId);
SampleJob.JOB_LIST.add(scheduleJob);
return jobDetail;
}

Expand All @@ -36,7 +36,7 @@ Trigger triggerOddJob(JobDetail registerOddJob) {
.forJob(registerOddJob.getKey())
.startAt(futureDate(10, DateBuilder.IntervalUnit.SECOND))
.withSchedule(simpleSchedule()
.withIntervalInSeconds(60) // Run every 2 seconds
.withIntervalInSeconds(120) // Run every 120 seconds
.repeatForever()
.withMisfireHandlingInstructionFireNow())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public static Message success(String msg) {
return new Message(true, msg);
}

public Message() {
super();
}

public Message(boolean valid, String msg) {
super();
this.valid = valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.scheduler.quartz.model.response;

import java.io.Serializable;
import jakarta.validation.constraints.NotBlank;
import lombok.With;

public record ScheduleJob(
@With String jobId, String jobName, String jobGroup, String jobStatus, String cronExpression, String desc)
implements Serializable {}
@With String jobId,
@NotBlank(message = "Job Name can't be blank") String jobName,
String jobGroup,
String jobStatus,
String cronExpression,
String desc) {}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
Expand All @@ -22,6 +23,7 @@
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

Expand All @@ -32,13 +34,8 @@ public class JobsService {
private final Scheduler scheduler;
public static final String GROUP_NAME = "sample-group";

public JobsService(Scheduler scheduler) {
this.scheduler = scheduler;
}

public void deleteJob(ScheduleJob scheduleJob) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(scheduleJob.jobName(), scheduleJob.jobGroup());
scheduler.deleteJob(jobKey);
public JobsService(SchedulerFactoryBean schedulerFactoryBean) {
this.scheduler = schedulerFactoryBean.getScheduler();
}

public List<ScheduleJob> getJobs() {
Expand Down Expand Up @@ -113,34 +110,74 @@ private void updateJobCronExpression(ScheduleJob scheduleJob) throws SchedulerEx
}

private void addJob(ScheduleJob scheduleJob) throws SchedulerException {
// Create TriggerKey for the job
TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.jobName(), GROUP_NAME);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);

// Throw exception if the job already exists
if (trigger != null) {
throw new SchedulerException("job already exists!");
throw new SchedulerException(
"Job already exists with name '" + scheduleJob.jobName() + "' in group '" + GROUP_NAME + "'");
}

// simulate job info db persist operation
ScheduleJob withJobId = scheduleJob.withJobId(String.valueOf(SampleJob.JOB_LIST.size() + 1));
SampleJob.JOB_LIST.add(withJobId);
ScheduleJob withJobId = scheduleJob.withJobId(UUID.randomUUID().toString());

// Build the JobDetail with recovery and durability
JobDetail jobDetail = JobBuilder.newJob(SampleJob.class)
.withIdentity(withJobId.jobName(), GROUP_NAME)
.withDescription(
StringUtils.hasText(scheduleJob.desc()) ? scheduleJob.desc() : "No description provided")
.storeDurably()
.requestRecovery()
.build();
jobDetail.getJobDataMap().put("scheduleJob", withJobId.jobId());

// Build the Trigger with Cron expression and associate it with the job
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(withJobId.cronExpression());
trigger = TriggerBuilder.newTrigger()
.withIdentity(withJobId.jobName() + "-trigger", GROUP_NAME)
.withSchedule(cronScheduleBuilder)
.withDescription(
StringUtils.hasText(scheduleJob.desc()) ? scheduleJob.desc() : "No description provided")
.withSchedule(cronScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires())
.build();

scheduler.scheduleJob(jobDetail, trigger);
JobKey jobKey = JobKey.jobKey(scheduleJob.jobName(), scheduleJob.jobGroup());
log.info("Scheduled job with key: {}", jobKey);
}

public void pauseJob(ScheduleJob scheduleJob) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(scheduleJob.jobName(), scheduleJob.jobGroup());
validateJobExists(jobKey);
scheduler.pauseJob(jobKey);
log.info("Paused job with key: {}", jobKey);
}

public void resumeJob(ScheduleJob scheduleJob) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(scheduleJob.jobName(), scheduleJob.jobGroup());
validateJobExists(jobKey);
scheduler.resumeJob(jobKey);
log.info("Resumed job with key: {}", jobKey);
}

public void runJob(ScheduleJob job) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(job.jobName(), job.jobGroup());
validateJobExists(jobKey);
scheduler.triggerJob(jobKey);
log.info("Triggered job with key: {}", jobKey);
}

public void deleteJob(ScheduleJob scheduleJob) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(scheduleJob.jobName(), scheduleJob.jobGroup());
validateJobExists(jobKey);
scheduler.deleteJob(jobKey);
log.info("Deleted job with key: {}", jobKey);
}

private void validateJobExists(JobKey jobKey) throws SchedulerException {
if (!scheduler.checkExists(jobKey)) {
throw new SchedulerException("Job does not exist with key: " + jobKey);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import com.scheduler.quartz.model.response.JobStatus;
import com.scheduler.quartz.model.response.ScheduleJob;
import com.scheduler.quartz.service.JobsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.Valid;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -35,7 +39,7 @@ List<JobStatus> getJobsStatuses() throws SchedulerException {
}

@PostMapping(value = "/saveOrUpdate")
public Message saveOrUpdate(ScheduleJob job) {
public Message saveOrUpdate(@RequestBody @Valid ScheduleJob job) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, please help me to write integrationTests by extending AbstractIntegrationTest for all endpoints in this controller

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Below is an example of how you could write integration tests for all the endpoints in JobsController by extending AbstractIntegrationTest. Adjust package names, imports, and any custom logic to match your project’s structure and requirements.


import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@activeprofiles("test")
class JobsControllerIntegrationTest extends AbstractIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Test
void testGetJobs() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api"))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testGetJobsStatuses() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api/statuses"))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testSaveOrUpdate() throws Exception {
    String requestBody = """
        {
            "jobName": "SampleJob",
            "cronExpression": "0/5 * * * * ?",
            "jobId": "12345",
            "description": "Test job description"
        }
    """;

    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/saveOrUpdate")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testPauseJob() throws Exception {
    String requestBody = """
        {
            "jobName": "SampleJob",
            "jobId": "12345"
        }
    """;

    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/pauseJob")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testResumeJob() throws Exception {
    String requestBody = """
        {
            "jobName": "SampleJob",
            "jobId": "12345"
        }
    """;

    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/resumeJob")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testRunJob() throws Exception {
    String requestBody = """
        {
            "jobName": "SampleJob",
            "jobId": "12345"
        }
    """;

    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/runJob")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

@Test
void testDeleteJob() throws Exception {
    String requestBody = """
        {
            "jobName": "SampleJob",
            "jobId": "12345"
        }
    """;

    mockMvc.perform(
            MockMvcRequestBuilders.delete("/api/deleteJob")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON));
}

}


Adjust the JSON payloads and field names as needed based on your ScheduleJob definition. If you have additional validation rules or fields, include them in the request bodies. These tests ensure that each endpoint is accessible and returns an appropriate response code and content type.

log.info("saveOrUpdateJob params : {}", job);
Message message = Message.failure();
try {
Expand All @@ -49,7 +53,7 @@ public Message saveOrUpdate(ScheduleJob job) {
}

@PostMapping(value = "/pauseJob")
public Message pauseJob(ScheduleJob job) {
public Message pauseJob(@RequestBody @Valid ScheduleJob job) {
log.info("pauseJob params = {}", job);
Message message = Message.failure();
try {
Expand All @@ -62,8 +66,44 @@ public Message pauseJob(ScheduleJob job) {
return message;
}

@Operation(summary = "Resume a scheduled job")
@ApiResponse(responseCode = "200", description = "Job resumed successfully")
@ApiResponse(responseCode = "400", description = "Invalid job parameters")
@ApiResponse(responseCode = "500", description = "Internal server error")
@PostMapping(value = "/resumeJob")
public Message resumeJob(@RequestBody @Valid ScheduleJob job) {
log.info("resumeJob params = {}", job);
Message message = Message.failure();
try {
jobsService.resumeJob(job);
message = Message.success();
} catch (Exception e) {
message.setMsg(e.getMessage());
log.error("resumeJob ex:", e);
}
return message;
}

@Operation(summary = "Trigger immediate execution of a job")
@ApiResponse(responseCode = "200", description = "Job triggered successfully")
@ApiResponse(responseCode = "400", description = "Invalid job parameters")
@ApiResponse(responseCode = "500", description = "Internal server error")
@PostMapping(value = "/runJob")
public Message runJob(@RequestBody @Valid ScheduleJob job) {
log.info("runJob params = {}", job);
Message message = Message.failure();
try {
jobsService.runJob(job);
message = Message.success();
} catch (Exception e) {
message.setMsg(e.getMessage());
log.error("runJob ex:", e);
}
return message;
}

@DeleteMapping(value = "/deleteJob")
public Message deleteJob(ScheduleJob job) {
public Message deleteJob(@RequestBody @Valid ScheduleJob job) {
log.info("deleteJob params : {}", job);
Message message = Message.failure();
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
spring.application.name=boot-scheduler-quartz
server.port=8080
server.shutdown=graceful
spring.main.allow-bean-definition-overriding=true
spring.jmx.enabled=false

spring.mvc.problemdetails.enabled=true
spring.threads.virtual.enabled=true

################ Actuator #####################
management.endpoints.web.exposure.include=configprops,env,health,info,logfile,loggers,metrics,prometheus
Expand All @@ -14,6 +15,8 @@ spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.data.jpa.repositories.bootstrap-mode=deferred
spring.datasource.hikari.auto-commit=false
spring.datasource.hikari.pool-name=HikariPool-${spring.application.name}
spring.datasource.hikari.data-source-properties.ApplicationName=${spring.application.name}
spring.jpa.hibernate.ddl-auto=none
#spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Expand All @@ -34,4 +37,14 @@ spring.quartz.properties.org.quartz.scheduler.instanceName=${spring.application.
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
#spring.quartz.properties.org.quartz.jobStore.useProperties=true
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
spring.threads.virtual.enabled=true
## Default is RamJobStore, if you want to use JDBC Job Store, you need to set the following properties
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.dataSource=quartzDataSource
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.provider=hikaricp
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.driver=${spring.datasource.driver-class-name}
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.URL=${spring.datasource.url}
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.user=${spring.datasource.username}
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.password=${spring.datasource.password}
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.maxConnections=10
spring.quartz.properties.org.quartz.dataSource.quartzDataSource.validationQuery=select 1

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@

<logger name="com.scheduler.quartz" level="DEBUG"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.quartz" level="INFO" />

</configuration>
Loading
Loading