All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.kagkarlsson.scheduler.boot.autoconfigure.DbSchedulerAutoConfiguration Maven / Gradle / Ivy

There is a newer version: 15.1.2
Show newest version
/**
 * Copyright (C) Gustav Karlsson
 *
 * 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
 *
 * http://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 com.github.kagkarlsson.scheduler.boot.autoconfigure;

import com.github.kagkarlsson.scheduler.PollingStrategyConfig;
import com.github.kagkarlsson.scheduler.Scheduler;
import com.github.kagkarlsson.scheduler.SchedulerBuilder;
import com.github.kagkarlsson.scheduler.SchedulerName;
import com.github.kagkarlsson.scheduler.Serializer;
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.startup.ContextReadyStart;
import com.github.kagkarlsson.scheduler.boot.config.startup.ImmediateStart;
import com.github.kagkarlsson.scheduler.exceptions.SerializationException;
import com.github.kagkarlsson.scheduler.stats.StatsRegistry;
import com.github.kagkarlsson.scheduler.task.OnStartup;
import com.github.kagkarlsson.scheduler.task.Task;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ConfigurableObjectInputStream;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;

@Configuration
@EnableConfigurationProperties(DbSchedulerProperties.class)
@AutoConfigurationPackage
@AutoConfigureAfter({
    DataSourceAutoConfiguration.class,
})
@ConditionalOnBean(DataSource.class)
@ConditionalOnProperty(value = "db-scheduler.enabled", matchIfMissing = true)
public class DbSchedulerAutoConfiguration {
    private static final Logger log = LoggerFactory.getLogger(DbSchedulerAutoConfiguration.class);
    private static Predicate> shouldBeStarted = task -> task instanceof OnStartup;

    private final DbSchedulerProperties config;
    private final DataSource existingDataSource;
    private final List> configuredTasks;

    public DbSchedulerAutoConfiguration(DbSchedulerProperties dbSchedulerProperties,
                                        DataSource dataSource, List> configuredTasks) {
        this.config = Objects.requireNonNull(dbSchedulerProperties, "Can't configure db-scheduler without required configuration");
        this.existingDataSource = Objects.requireNonNull(dataSource, "An existing javax.sql.DataSource is required");
        this.configuredTasks = Objects.requireNonNull(configuredTasks, "At least one Task must be configured");
    }

    /**
     * Provide an empty customizer if not present in the context.
     */
    @ConditionalOnMissingBean
    @Bean
    public DbSchedulerCustomizer noopCustomizer() {
        return new DbSchedulerCustomizer() {
        };
    }

    /**
     * Will typically be created if Spring Boot Actuator is not on the classpath.
     */
    @ConditionalOnMissingBean(StatsRegistry.class)
    @Bean
    StatsRegistry noopStatsRegistry() {
        log.debug("Missing StatsRegistry bean in context, creating a no-op StatsRegistry");
        return StatsRegistry.NOOP;
    }

    @ConditionalOnBean(DataSource.class)
    @ConditionalOnMissingBean
    @Bean(destroyMethod = "stop")
    public Scheduler scheduler(DbSchedulerCustomizer customizer, StatsRegistry registry) {
        log.info("Creating db-scheduler using tasks from Spring context: {}", configuredTasks);

        // Ensure that we are using a transactional aware data source
        DataSource transactionalDataSource = configureDataSource(existingDataSource);

        // Instantiate a new builder
        final SchedulerBuilder builder = Scheduler.create(transactionalDataSource, nonStartupTasks(configuredTasks));

        builder.threads(config.getThreads());

        // Polling
        builder.pollingInterval(config.getPollingInterval());

        // Polling strategy
        if (config.getPollingStrategy() == PollingStrategyConfig.Type.FETCH) {
            builder.pollUsingFetchAndLockOnExecute(
                config.getPollingStrategyLowerLimitFractionOfThreads(),
                config.getPollingStrategyUpperLimitFractionOfThreads());
        } else if (config.getPollingStrategy() == PollingStrategyConfig.Type.LOCK_AND_FETCH) {
            builder.pollUsingLockAndFetch(
                config.getPollingStrategyLowerLimitFractionOfThreads(),
                config.getPollingStrategyUpperLimitFractionOfThreads());
        } else {
            throw new IllegalArgumentException("Unknown polling-strategy: " + config.getPollingStrategy());
        }

        builder.heartbeatInterval(config.getHeartbeatInterval());

        // Use scheduler name implementation from customizer if available, otherwise use
        // configured scheduler name (String). If both is absent, use the library default
        if (customizer.schedulerName().isPresent()) {
            builder.schedulerName(customizer.schedulerName().get());
        } else if (config.getSchedulerName() != null) {
            builder.schedulerName(new SchedulerName.Fixed(config.getSchedulerName()));
        }

        builder.tableName(config.getTableName());

        // Use custom serializer if provided. Otherwise use devtools friendly serializer.
        builder.serializer(customizer.serializer().orElse(SPRING_JAVA_SERIALIZER));

        if (config.isImmediateExecutionEnabled()) {
            builder.enableImmediateExecution();
        }

        // Use custom executor service if provided
        customizer.executorService().ifPresent(builder::executorService);

        builder.deleteUnresolvedAfter(config.getDeleteUnresolvedAfter());

        // Add recurring jobs and jobs that implements OnStartup
        builder.startTasks(startupTasks(configuredTasks));

        // Expose metrics
        builder.statsRegistry(registry);

        // Failure logging
        builder.failureLogging(config.getFailureLoggerLevel(), config.isFailureLoggerLogStackTrace());

        // Shutdown max wait
        builder.shutdownMaxWait(config.getShutdownMaxWait());

        return builder.build();
    }

    @ConditionalOnBean(Scheduler.class)
    @ConditionalOnMissingBean
    @Bean
    public DbSchedulerStarter dbSchedulerStarter(Scheduler scheduler) {
        if (config.isDelayStartupUntilContextReady()) {
            return new ContextReadyStart(scheduler);
        }

        return new ImmediateStart(scheduler);
    }

    private static DataSource configureDataSource(DataSource existingDataSource) {
        if (existingDataSource instanceof TransactionAwareDataSourceProxy) {
            log.debug("Using an already transaction aware DataSource");
            return existingDataSource;
        }

        log.debug("The configured DataSource is not transaction aware: '{}'. Wrapping in TransactionAwareDataSourceProxy.", existingDataSource);

        return new TransactionAwareDataSourceProxy(existingDataSource);
    }

    @SuppressWarnings("unchecked")
    private static  & OnStartup> List startupTasks(List> tasks) {
        return tasks.stream()
            .filter(shouldBeStarted)
            .map(task -> (T) task)
            .collect(Collectors.toList());
    }

    private static List> nonStartupTasks(List> tasks) {
        return tasks.stream()
            .filter(shouldBeStarted.negate())
            .collect(Collectors.toList());
    }

    /**
     * {@link Serializer} compatible with Spring Boot Devtools.
     *
     * @see 
     *      Devtools known limitations
     */
    private static final Serializer SPRING_JAVA_SERIALIZER = new Serializer() {

        public byte[] serialize(Object data) {
            if (data == null)
                return null;
            try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
                 ObjectOutput out = new ObjectOutputStream(bos)) {
                out.writeObject(data);
                return bos.toByteArray();
            } catch (Exception e) {
                throw new SerializationException("Failed to serialize object", e);
            }
        }

        public  T deserialize(Class clazz, byte[] serializedData) {
            if (serializedData == null)
                return null;
            try (ByteArrayInputStream bis = new ByteArrayInputStream(serializedData);
                 ObjectInput in = new ConfigurableObjectInputStream(bis, Thread.currentThread().getContextClassLoader())) {
                return clazz.cast(in.readObject());
            } catch (Exception e) {
                throw new SerializationException("Failed to deserialize object", e);
            }
        }
    };

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy