Skip to content

Commit

Permalink
spring-boot: introduce DbSchedulerStopper
Browse files Browse the repository at this point in the history
This lets the user control when the scheduler should be stopped. By
default it stops the scheduler when Spring issues the
ContextClosedEvent.

Fixes #423
  • Loading branch information
evenh committed Oct 14, 2023
1 parent bbe25d7 commit 98154ea
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerProperties;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStarter;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStopper;
import com.github.kagkarlsson.scheduler.boot.config.shutdown.ContextClosedStopper;
import com.github.kagkarlsson.scheduler.boot.config.startup.ContextReadyStart;
import com.github.kagkarlsson.scheduler.boot.config.startup.ImmediateStart;
import com.github.kagkarlsson.scheduler.exceptions.SerializationException;
Expand Down Expand Up @@ -100,7 +102,7 @@ StatsRegistry noopStatsRegistry() {
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean
@DependsOnDatabaseInitialization
@Bean(destroyMethod = "stop")
@Bean
public Scheduler scheduler(DbSchedulerCustomizer customizer, StatsRegistry registry) {
log.info("Creating db-scheduler using tasks from Spring context: {}", configuredTasks);

Expand Down Expand Up @@ -192,6 +194,13 @@ public DbSchedulerStarter dbSchedulerStarter(Scheduler scheduler) {
return new ImmediateStart(scheduler);
}

@ConditionalOnBean(Scheduler.class)
@ConditionalOnMissingBean
@Bean
public DbSchedulerStopper dbSchedulerStopper(Scheduler scheduler) {
return new ContextClosedStopper(scheduler);
}

private static DataSource configureDataSource(DataSource existingDataSource) {
if (existingDataSource instanceof TransactionAwareDataSourceProxy) {
log.debug("Using an already transaction aware DataSource");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (C) Gustav Karlsson
*
* <p>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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 com.github.kagkarlsson.scheduler.boot.config;

public interface DbSchedulerStopper {
void doStop();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) Gustav Karlsson
*
* <p>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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 com.github.kagkarlsson.scheduler.boot.config.shutdown;

import com.github.kagkarlsson.scheduler.Scheduler;
import com.github.kagkarlsson.scheduler.SchedulerState;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStopper;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractSchedulerStopper implements DbSchedulerStopper {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final Scheduler scheduler;

protected AbstractSchedulerStopper(Scheduler scheduler) {
this.scheduler = Objects.requireNonNull(scheduler, "A scheduler must be provided");
}

@Override
public void doStop() {
SchedulerState state = scheduler.getSchedulerState();

if (state.isShuttingDown()) {
log.warn("Scheduler is already shutting down - will not attempting to stop");
return;
}

if (!state.isStarted()) {
log.info("Scheduler not started - will not attempt to start");
return;
}

log.info("Triggering scheduler stop");
scheduler.stop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) Gustav Karlsson
*
* <p>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
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 com.github.kagkarlsson.scheduler.boot.config.shutdown;

import com.github.kagkarlsson.scheduler.Scheduler;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;

public class ContextClosedStopper extends AbstractSchedulerStopper {
public ContextClosedStopper(Scheduler scheduler) {
super(scheduler);
}

@EventListener(ContextClosedEvent.class)
public void whenContextIsStopped() {
doStop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerProperties;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStarter;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStopper;
import com.github.kagkarlsson.scheduler.boot.config.shutdown.AbstractSchedulerStopper;
import com.github.kagkarlsson.scheduler.boot.config.shutdown.ContextClosedStopper;
import com.github.kagkarlsson.scheduler.boot.config.startup.AbstractSchedulerStarter;
import com.github.kagkarlsson.scheduler.boot.config.startup.ContextReadyStart;
import com.github.kagkarlsson.scheduler.boot.config.startup.ImmediateStart;
Expand Down Expand Up @@ -40,6 +43,9 @@
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.EventListener;

public class DbSchedulerAutoConfigurationTest {
private static final Logger log = LoggerFactory.getLogger(DbSchedulerAutoConfigurationTest.class);
Expand Down Expand Up @@ -195,6 +201,30 @@ public void it_should_support_custom_starting_strategies() {
});
}

@Test
public void it_should_shut_down_with_spring_context_by_default() {
ctxRunner
.run(
(AssertableApplicationContext ctx) -> {
assertThat(ctx).hasSingleBean(Scheduler.class);

assertThat(ctx).hasSingleBean(ContextClosedStopper.class);
});
}

@Test
public void it_should_support_custom_stopping_strategies() {
ctxRunner
.withUserConfiguration(CustomStopperConfiguration.class)
.run(
(AssertableApplicationContext ctx) -> {
assertThat(ctx).hasSingleBean(Scheduler.class);

assertThat(ctx).hasSingleBean(DbSchedulerStopper.class);
assertThat(ctx).doesNotHaveBean(ContextClosedStopper.class);
});
}

@Test
public void it_should_provide_micrometer_registry_if_micrometer_is_present() {
ctxRunner
Expand Down Expand Up @@ -289,6 +319,28 @@ static class SomeCustomStarter extends AbstractSchedulerStarter {
}
}

@Configuration
static class CustomStopperConfiguration extends SingleTaskConfiguration {
@Bean
DbSchedulerStopper someCustomStarter(Scheduler scheduler) {
return new SomeCustomStopper(scheduler);
}

static class SomeCustomStopper extends AbstractSchedulerStopper {
SomeCustomStopper(Scheduler scheduler) {
super(scheduler);
log.info("Creating a custom stopper");
}

@EventListener(ContextClosedEvent.class)
public void letMeThinkAboutIt() throws Exception {
log.info("Thinking 5 seconds before stopping the scheduler");
Thread.sleep(5_000);
doStop();
}
}
}

@Configuration
static class CustomStatsRegistry extends SingleTaskConfiguration {
@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import com.github.kagkarlsson.scheduler.Scheduler;
import com.github.kagkarlsson.scheduler.boot.actuator.DbSchedulerHealthIndicator;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerStopper;
import com.github.kagkarlsson.scheduler.boot.config.shutdown.ContextClosedStopper;
import com.github.kagkarlsson.scheduler.task.Task;
import com.github.kagkarlsson.scheduler.task.TaskInstance;
import java.time.Instant;
Expand Down Expand Up @@ -50,6 +52,11 @@ public void it_should_have_a_scheduler_bean() {
assertThat(ctx).hasSingleBean(Scheduler.class);
}

@Test
public void it_should_have_a_stopper_bean() {
assertThat(ctx).hasSingleBean(ContextClosedStopper.class);
}

@Test
public void it_should_have_two_tasks_exposed_as_beans() {
assertThat(ctx.getBeansOfType(Task.class).values()).hasSizeGreaterThan(10);
Expand Down

0 comments on commit 98154ea

Please sign in to comment.