Commit a07c8e6e authored by Stephane Nicoll's avatar Stephane Nicoll

Merge pull request #10364 from vpavic

* pr/10364:
  Polish "Add Quartz actuator endpoint"
  Add Quartz actuator endpoint

Closes gh-10364
parents 095ff188 b11602ae
......@@ -86,6 +86,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-jms")
optional("org.springframework:spring-messaging")
......
[[quartz]]
= Quartz (`quartz`)
The `quartz` endpoint provides information about jobs and triggers that are managed by the Quartz Scheduler.
[[quartz-report]]
== Retrieving Registered Groups
Jobs and triggers are managed in groups.
To retrieve the list of registered job and trigger groups, make a `GET` request to `/actuator/quartz`, as shown in the following curl-based example:
include::{snippets}/quartz/report/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/report/http-response.adoc[]
[[quartz-report-response-structure]]
=== Response Structure
The response contains the groups names for registered jobs and triggers.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/report/response-fields.adoc[]
[[quartz-job-groups]]
== Retrieving Registered Job Names
To retrieve the list of registered job names, make a `GET` request to `/actuator/quartz/jobs`, as shown in the following curl-based example:
include::{snippets}/quartz/jobs/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/jobs/http-response.adoc[]
[[quartz-job-groups-response-structure]]
=== Response Structure
The response contains the registered job names for each group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/jobs/response-fields.adoc[]
[[quartz-trigger-groups]]
== Retrieving Registered Trigger Names
To retrieve the list of registered trigger names, make a `GET` request to `/actuator/quartz/triggers`, as shown in the following curl-based example:
include::{snippets}/quartz/triggers/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/quartz/triggers/http-response.adoc[]
[[quartz-trigger-groups-response-structure]]
=== Response Structure
The response contains the registered trigger names for each group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/triggers/response-fields.adoc[]
[[quartz-job-group]]
== Retrieving Overview of a Job Group
To retrieve an overview of the jobs in a particular group, make a `GET` request to `/actuator/quartz/jobs/\{groupName}`, as shown in the following curl-based example:
include::{snippets}/quartz/job-group/curl-request.adoc[]
The preceding example retrieves the summary for jobs in the `samples` group.
The resulting response is similar to the following:
include::{snippets}/quartz/job-group/http-response.adoc[]
[[quartz-job-group-response-structure]]
=== Response Structure
The response contains an overview of jobs in a particular group.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/job-group/response-fields.adoc[]
[[quartz-trigger-group]]
== Retrieving Overview of a Trigger Group
To retrieve an overview of the triggers in a particular group, make a `GET` request to `/actuator/quartz/triggers/\{groupName}`, as shown in the following curl-based example:
include::{snippets}/quartz/trigger-group/curl-request.adoc[]
The preceding example retrieves the summary for triggers in the `tests` group.
The resulting response is similar to the following:
include::{snippets}/quartz/trigger-group/http-response.adoc[]
[[quartz-trigger-group-response-structure]]
=== Response Structure
The response contains an overview of triggers in a particular group.
Trigger implementation specific details are available.
The following table describes the structure of the response:
[cols="3,1,3"]
include::{snippets}/quartz/trigger-group/response-fields.adoc[]
[[quartz-job]]
== Retrieving Details of a Job
To retrieve the details about a particular job, make a `GET` request to `/actuator/quartz/jobs/\{groupName}/\{jobName}`, as shown in the following curl-based example:
include::{snippets}/quartz/job-details/curl-request.adoc[]
The preceding example retrieves the details of the job identified by the `samples` group and `jobOne` name.
The resulting response is similar to the following:
include::{snippets}/quartz/job-details/http-response.adoc[]
If a key in the data map is identified as sensitive, its value is sanitized.
[[quartz-job-response-structure]]
=== Response Structure
The response contains the full details of a job including a summary of the triggers associated with it, if any.
The triggers are sorted by next fire time and priority.
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/job-details/response-fields.adoc[]
[[quartz-trigger]]
== Retrieving Details of a Trigger
To retrieve the details about a particular trigger, make a `GET` request to `/actuator/quartz/triggers/\{groupName}/\{triggerName}`, as shown in the following curl-based example:
include::{snippets}/quartz/trigger-details-cron/curl-request.adoc[]
The preceding example retrieves the details of trigger identified by the `samples` group and `example` name.
The resulting response has a common structure and a specific additional object according to the trigger implementation.
There are five supported types:
* `cron` for `CronTrigger`
* `simple` for `SimpleTrigger`
* `dailyTimeInterval` for `DailyTimeIntervalTrigger`
* `calendarInterval` for `CalendarIntervalTrigger`
* `custom` for any other trigger implementations
[[quartz-trigger-cron]]
=== Cron Trigger Response Structure
A cron trigger defines the cron expression that is used to determine when it has to fire.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-cron/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-cron/response-fields.adoc[]
[[quartz-trigger-simple]]
=== Simple Trigger Response Structure
A simple trigger is used to fire a Job at a given moment in time, and optionally repeated at a specified interval.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-simple/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-simple/response-fields.adoc[]
[[quartz-trigger-daily-time-interval]]
=== Daily Time Interval Trigger Response Structure
A daily time interval trigger is used to fire a Job based upon daily repeating time intervals.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-daily-time-interval/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-daily-time-interval/response-fields.adoc[]
[[quartz-trigger-calendar-interval]]
=== Calendar Interval Trigger Response Structure
A daily time interval trigger is used to fire a Job based upon repeating calendar time intervals.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-calendar-interval/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-calendar-interval/response-fields.adoc[]
[[quartz-trigger-custom]]
=== Custom Trigger Response Structure
A custom trigger is any other implementation.
The resulting response for such a trigger implementation is similar to the following:
include::{snippets}/quartz/trigger-details-custom/http-response.adoc[]
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/quartz/trigger-details-custom/response-fields.adoc[]
......@@ -67,6 +67,7 @@ include::endpoints/loggers.adoc[leveloffset=+1]
include::endpoints/mappings.adoc[leveloffset=+1]
include::endpoints/metrics.adoc[leveloffset=+1]
include::endpoints/prometheus.adoc[leveloffset=+1]
include::endpoints/quartz.adoc[leveloffset=+1]
include::endpoints/scheduledtasks.adoc[leveloffset=+1]
include::endpoints/sessions.adoc[leveloffset=+1]
include::endpoints/shutdown.adoc[leveloffset=+1]
......
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link QuartzEndpoint}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
* @since 2.5.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Scheduler.class)
@AutoConfigureAfter(QuartzAutoConfiguration.class)
@ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class)
public class QuartzEndpointAutoConfiguration {
@Bean
@ConditionalOnBean(Scheduler.class)
@ConditionalOnMissingBean
public QuartzEndpoint quartzEndpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
@Bean
@ConditionalOnBean(QuartzEndpoint.class)
@ConditionalOnMissingBean
public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) {
return new QuartzEndpointWebExtension(endpoint);
}
}
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for actuator Quartz Scheduler concerns.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;
......@@ -80,6 +80,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsA
org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisReactiveHealthContributorAutoConfiguration,\
......
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.quartz;
import org.junit.jupiter.api.Test;
import org.quartz.Scheduler;
import org.springframework.boot.actuate.quartz.QuartzEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link QuartzEndpointAutoConfiguration}.
*
* @author Vedran Pavic
* @author Stephane Nicoll
*/
class QuartzEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(QuartzEndpointAutoConfiguration.class));
@Test
void endpointIsAutoConfigured() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.withPropertyValues("management.endpoints.web.exposure.include=quartz")
.run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class));
}
@Test
void endpointIsNotAutoConfiguredIfSchedulerIsNotAvailable() {
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=quartz")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointNotAutoConfiguredWhenNotExposed() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointCanBeDisabled() {
this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class))
.withPropertyValues("management.endpoint.quartz.enabled:false")
.run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class));
}
@Test
void endpointBacksOffWhenUserProvidedEndpointIsPresent() {
this.contextRunner.withUserConfiguration(CustomEndpointConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class).hasBean("customEndpoint"));
}
@Configuration(proxyBeanMethods = false)
static class CustomEndpointConfiguration {
@Bean
CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
}
private static final class CustomEndpoint extends QuartzEndpoint {
private CustomEndpoint() {
super(mock(Scheduler.class));
}
}
}
......@@ -48,6 +48,7 @@ dependencies {
optional("org.mongodb:mongodb-driver-reactivestreams")
optional("org.mongodb:mongodb-driver-sync")
optional("org.neo4j.driver:neo4j-java-driver")
optional("org.quartz-scheduler:quartz")
optional("org.springframework:spring-jdbc")
optional("org.springframework:spring-messaging")
optional("org.springframework:spring-webflux")
......
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import org.quartz.SchedulerException;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzGroups;
/**
* {@link EndpointWebExtension @EndpointWebExtension} for the {@link QuartzEndpoint}.
*
* @author Stephane Nicoll
* @since 2.5.0
*/
@EndpointWebExtension(endpoint = QuartzEndpoint.class)
public class QuartzEndpointWebExtension {
private final QuartzEndpoint delegate;
public QuartzEndpointWebExtension(QuartzEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<QuartzGroups> quartzJobOrTriggerGroups(@Selector String jobsOrTriggers)
throws SchedulerException {
return handle(jobsOrTriggers, this.delegate::quartzJobGroups, this.delegate::quartzTriggerGroups);
}
@ReadOperation
public WebEndpointResponse<Object> quartzJobOrTriggerGroup(@Selector String jobsOrTriggers, @Selector String group)
throws SchedulerException {
return handle(jobsOrTriggers, () -> this.delegate.quartzJobGroupSummary(group),
() -> this.delegate.quartzTriggerGroupSummary(group));
}
@ReadOperation
public WebEndpointResponse<Object> quartzJobOrTrigger(@Selector String jobsOrTriggers, @Selector String group,
@Selector String name) throws SchedulerException {
return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name),
() -> this.delegate.quartzTrigger(group, name));
}
private <T> WebEndpointResponse<T> handle(String jobsOrTriggers, ResponseSupplier<T> jobAction,
ResponseSupplier<T> triggerAction) throws SchedulerException {
if ("jobs".equals(jobsOrTriggers)) {
return handleNull(jobAction.get());
}
if ("triggers".equals(jobsOrTriggers)) {
return handleNull(triggerAction.get());
}
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
private <T> WebEndpointResponse<T> handleNull(T value) {
if (value != null) {
return new WebEndpointResponse<>(value);
}
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
}
@FunctionalInterface
private interface ResponseSupplier<T> {
T get() throws SchedulerException;
}
}
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Actuator support for Quartz Scheduler.
*/
package org.springframework.boot.actuate.quartz;
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.quartz;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map.Entry;
import net.minidev.json.JSONArray;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.CalendarIntervalTrigger;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.DelegatingJob;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Integration tests for {@link QuartzEndpoint} exposed by Jersey, Spring MVC, and
* WebFlux.
*
* @author Stephane Nicoll
*/
class QuartzEndpointWebIntegrationTests {
private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne", "samples")
.usingJobData(new JobDataMap(Collections.singletonMap("name", "test"))).withDescription("A sample job")
.build();
private static final JobDetail jobTwo = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobTwo", "samples")
.build();
private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree").build();
private static final CronTrigger triggerOne = TriggerBuilder.newTrigger().withDescription("Once a day 3AM")
.withIdentity("triggerOne").withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build();
private static final SimpleTrigger triggerTwo = TriggerBuilder.newTrigger().withDescription("Once a day")
.withIdentity("triggerTwo", "tests").withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build();
private static final CalendarIntervalTrigger triggerThree = TriggerBuilder.newTrigger()
.withDescription("Once a week").withIdentity("triggerThree", "tests")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)).build();
@WebEndpointTest
void quartzReport(WebTestClient client) {
client.get().uri("/actuator/quartz").exchange().expectStatus().isOk().expectBody().jsonPath("jobs.groups")
.isEqualTo(new JSONArray().appendElement("samples").appendElement("DEFAULT"))
.jsonPath("triggers.groups").isEqualTo(new JSONArray().appendElement("DEFAULT").appendElement("tests"));
}
@WebEndpointTest
void quartzJobNames(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs").exchange().expectStatus().isOk().expectBody()
.jsonPath("groups.samples.jobs")
.isEqualTo(new JSONArray().appendElement("jobOne").appendElement("jobTwo"))
.jsonPath("groups.DEFAULT.jobs").isEqualTo(new JSONArray().appendElement("jobThree"));
}
@WebEndpointTest
void quartzTriggerNames(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers").exchange().expectStatus().isOk().expectBody()
.jsonPath("groups.DEFAULT.paused").isEqualTo(false).jsonPath("groups.DEFAULT.triggers")
.isEqualTo(new JSONArray().appendElement("triggerOne")).jsonPath("groups.tests.paused").isEqualTo(false)
.jsonPath("groups.tests.triggers")
.isEqualTo(new JSONArray().appendElement("triggerTwo").appendElement("triggerThree"));
}
@WebEndpointTest
void quartzTriggersOrJobsAreAllowed(WebTestClient client) {
client.get().uri("/actuator/quartz/something-elese").exchange().expectStatus().isBadRequest();
}
@WebEndpointTest
void quartzJobGroupSummary(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("samples").jsonPath("jobs.jobOne.className").isEqualTo(Job.class.getName())
.jsonPath("jobs.jobTwo.className").isEqualTo(DelegatingJob.class.getName());
}
@WebEndpointTest
void quartzJobGroupSummaryWithUnknownGroup(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzTriggerGroupSummary(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/tests").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("tests").jsonPath("paused").isEqualTo("false").jsonPath("triggers.cron")
.isEmpty().jsonPath("triggers.simple.triggerTwo.interval").isEqualTo(86400000)
.jsonPath("triggers.dailyTimeInterval").isEmpty()
.jsonPath("triggers.calendarInterval.triggerThree.interval").isEqualTo(604800000)
.jsonPath("triggers.custom").isEmpty();
}
@WebEndpointTest
void quartzTriggerGroupSummaryWithUnknownGroup(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzJobDetail(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples/jobOne").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("samples").jsonPath("name").isEqualTo("jobOne").jsonPath("data.name")
.isEqualTo("test");
}
@WebEndpointTest
void quartzJobDetailWithUnknownKey(WebTestClient client) {
client.get().uri("/actuator/quartz/jobs/samples/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void quartzTriggerDetail(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/DEFAULT/triggerOne").exchange().expectStatus().isOk().expectBody()
.jsonPath("group").isEqualTo("DEFAULT").jsonPath("name").isEqualTo("triggerOne").jsonPath("description")
.isEqualTo("Once a day 3AM").jsonPath("state").isEqualTo("NORMAL").jsonPath("type").isEqualTo("cron")
.jsonPath("simple").doesNotExist().jsonPath("calendarInterval").doesNotExist().jsonPath("dailyInterval")
.doesNotExist().jsonPath("custom").doesNotExist().jsonPath("cron.expression").isEqualTo("0 0 3 ? * *");
}
@WebEndpointTest
void quartzTriggerDetailWithUnknownKey(WebTestClient client) {
client.get().uri("/actuator/quartz/triggers/tests/does-not-exist").exchange().expectStatus().isNotFound();
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
Scheduler scheduler() throws SchedulerException {
Scheduler scheduler = mock(Scheduler.class);
mockJobs(scheduler, jobOne, jobTwo, jobThree);
mockTriggers(scheduler, triggerOne, triggerTwo, triggerThree);
return scheduler;
}
@Bean
QuartzEndpoint endpoint(Scheduler scheduler) {
return new QuartzEndpoint(scheduler);
}
@Bean
QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) {
return new QuartzEndpointWebExtension(endpoint);
}
private void mockJobs(Scheduler scheduler, JobDetail... jobs) throws SchedulerException {
MultiValueMap<String, JobKey> jobKeys = new LinkedMultiValueMap<>();
for (JobDetail jobDetail : jobs) {
JobKey key = jobDetail.getKey();
given(scheduler.getJobDetail(key)).willReturn(jobDetail);
jobKeys.add(key.getGroup(), key);
}
given(scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet()));
for (Entry<String, List<JobKey>> entry : jobKeys.entrySet()) {
given(scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
void mockTriggers(Scheduler scheduler, Trigger... triggers) throws SchedulerException {
MultiValueMap<String, TriggerKey> triggerKeys = new LinkedMultiValueMap<>();
for (Trigger trigger : triggers) {
TriggerKey key = trigger.getKey();
given(scheduler.getTrigger(key)).willReturn(trigger);
given(scheduler.getTriggerState(key)).willReturn(TriggerState.NORMAL);
triggerKeys.add(key.getGroup(), key);
}
given(scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet()));
for (Entry<String, List<TriggerKey>> entry : triggerKeys.entrySet()) {
given(scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey())))
.willReturn(new LinkedHashSet<>(entry.getValue()));
}
}
}
}
......@@ -112,6 +112,9 @@ The following technology-agnostic endpoints are available:
| `mappings`
| Displays a collated list of all `@RequestMapping` paths.
|`quartz`
|Shows information about Quartz Scheduler jobs.
| `scheduledtasks`
| Displays the scheduled tasks in your application.
......@@ -272,6 +275,10 @@ The following table shows the default exposure for the built-in endpoints:
| N/A
| No
| `quartz`
| Yes
| No
| `scheduledtasks`
| Yes
| No
......
......@@ -6,8 +6,10 @@ plugins {
description = "Spring Boot Quartz smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-quartz"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc"))
runtimeOnly("com.h2database:h2")
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -16,9 +16,16 @@
package smoketest.quartz;
import java.util.Calendar;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.CronScheduleBuilder;
import org.quartz.DailyTimeIntervalScheduleBuilder;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.TimeOfDay;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
......@@ -34,18 +41,50 @@ public class SampleQuartzApplication {
}
@Bean
public JobDetail sampleJobDetail() {
return JobBuilder.newJob(SampleJob.class).withIdentity("sampleJob").usingJobData("name", "World").storeDurably()
.build();
public JobDetail helloJobDetail() {
return JobBuilder.newJob(SampleJob.class).withIdentity("helloJob", "samples").usingJobData("name", "World")
.storeDurably().build();
}
@Bean
public JobDetail anotherJobDetail() {
return JobBuilder.newJob(SampleJob.class).withIdentity("anotherJob", "samples").usingJobData("name", "Everyone")
.storeDurably().build();
}
@Bean
public Trigger everyTwoSecTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("sampleTrigger")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever()).build();
}
@Bean
public Trigger everyDayTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("every-day", "samples")
.withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build();
}
@Bean
public Trigger sampleJobTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2)
.repeatForever();
public Trigger threeAmWeekdaysTrigger() {
return TriggerBuilder.newTrigger().forJob("anotherJob", "samples").withIdentity("3am-weekdays", "samples")
.withSchedule(CronScheduleBuilder.atHourAndMinuteOnGivenDaysOfWeek(3, 0, 1, 2, 3, 4, 5)).build();
}
return TriggerBuilder.newTrigger().forJob(sampleJobDetail()).withIdentity("sampleTrigger")
.withSchedule(scheduleBuilder).build();
@Bean
public Trigger onceAWeekTrigger() {
return TriggerBuilder.newTrigger().forJob("anotherJob", "samples").withIdentity("once-a-week", "samples")
.withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1))
.build();
}
@Bean
public Trigger everyHourWorkingHourTuesdayAndThursdayTrigger() {
return TriggerBuilder.newTrigger().forJob("helloJob", "samples").withIdentity("every-hour-tue-thu", "samples")
.withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR))
.build();
}
}
spring.quartz.job-store-type=jdbc
management.endpoints.web.exposure.include=health,quartz
\ No newline at end of file
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -38,8 +38,9 @@ import static org.hamcrest.Matchers.containsString;
class SampleQuartzApplicationTests {
@Test
void quartzJobIsTriggered(CapturedOutput output) throws InterruptedException {
try (ConfigurableApplicationContext context = SpringApplication.run(SampleQuartzApplication.class)) {
void quartzJobIsTriggered(CapturedOutput output) {
try (ConfigurableApplicationContext context = SpringApplication.run(SampleQuartzApplication.class,
"--server.port=0")) {
Awaitility.waitAtMost(Duration.ofSeconds(5)).until(output::toString, containsString("Hello World!"));
}
}
......
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.quartz;
import java.util.Map;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.api.MapAssert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Web tests for {@link SampleQuartzApplication}.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SampleQuartzApplicationWebTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void quartzGroupNames() {
Map<String, Object> content = getContent("/actuator/quartz");
assertThat(content).containsOnlyKeys("jobs", "triggers");
}
@Test
void quartzJobGroups() {
Map<String, Object> content = getContent("/actuator/quartz/jobs");
assertThat(content).containsOnlyKeys("groups");
assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("samples");
}
@Test
void quartzTriggerGroups() {
Map<String, Object> content = getContent("/actuator/quartz/triggers");
assertThat(content).containsOnlyKeys("groups");
assertThat(content).extractingByKey("groups", nestedMap()).containsOnlyKeys("DEFAULT", "samples");
}
@Test
void quartzJobDetail() {
Map<String, Object> content = getContent("/actuator/quartz/jobs/samples/helloJob");
assertThat(content).containsEntry("name", "helloJob").containsEntry("group", "samples");
}
@Test
void quartzJobDetailWhenNameDoesNotExistReturns404() {
ResponseEntity<String> response = this.restTemplate.getForEntity("/actuator/quartz/jobs/samples/does-not-exist",
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void quartzTriggerDetail() {
Map<String, Object> content = getContent("/actuator/quartz/triggers/samples/3am-weekdays");
assertThat(content).contains(entry("group", "samples"), entry("name", "3am-weekdays"), entry("state", "NORMAL"),
entry("type", "cron"));
}
@Test
void quartzTriggerDetailWhenNameDoesNotExistReturns404() {
ResponseEntity<String> response = this.restTemplate
.getForEntity("/actuator/quartz/triggers/samples/does-not-exist", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
private Map<String, Object> getContent(String path) {
ResponseEntity<Map<String, Object>> entity = asMapEntity(this.restTemplate.getForEntity(path, Map.class));
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
return entity.getBody();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static <K, V> ResponseEntity<Map<K, V>> asMapEntity(ResponseEntity<Map> entity) {
return (ResponseEntity) entity;
}
@SuppressWarnings("rawtypes")
private static InstanceOfAssertFactory<Map, MapAssert<String, Object>> nestedMap() {
return InstanceOfAssertFactories.map(String.class, Object.class);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment