db-scheduler
db-scheduler copied to clipboard
Avoiding circular reference if task calling service == task creating service
[ v ] I am running the latest version [ v ] I checked the documentation and found no answer [ v ] I checked to make sure that this issue has not already been filed
Probably the symptoms of problem were decribed in this issue but no problem details were discovered
Expected Behavior
No problems in case when task calling service is task creating service. The app starts well
Current Behavior
I have rating service (ratingService) in Spring boot app. This ratingService has method update, that updates the statistic of one user (adding some points). But in the end of this method I need to create task that will recalculate the places of all users according to new rating points of user that have been added (long recalculating of fat table). The new task (recalculateRatingJobTask) calls rating service, but the other method - updateAll. In ratingService the SchedulerClient is injected, in recalculateRatingJobTask the ratingService is injected With default autoconfiguration I get circular reference:
┌─────┐
| ratingService
↑ ↓
| com.github.kagkarlsson.scheduler.boot.autoconfigure.DbSchedulerAutoConfiguration
↑ ↓
| recalculateRatingJobTask (@Bean from config)
└─────┘
Problem: The problem is connected with com.github.kagkarlsson.scheduler.boot.autoconfigure.DbSchedulerAutoConfiguration. The constructor of bean
public DbSchedulerAutoConfiguration(
DbSchedulerProperties dbSchedulerProperties,
DataSource dataSource,
List<Task<?>> configuredTasks) {
contains collection of all tasks that are injected. To avoid the problem and not divide rating service we need to create our own configuration:
@Configuration
class SchedulerClientConfig {
@Bean
fun jacksonSerializer(objectMapper: ObjectMapper) = JacksonSerializer(objectMapper)
@Bean
fun dbSchedulerCustomizer(jacksonSerializer: JacksonSerializer) = object : DbSchedulerCustomizer {
override fun serializer(): Optional<Serializer> {
return Optional.of(jacksonSerializer)
}
}
@Bean
fun schedulerClient(
dataSource: DataSource, configuredTasks: List<Task<Any>>,
jacksonSerializer: JacksonSerializer, config: DbSchedulerProperties,
customizer: DbSchedulerCustomizer
): SchedulerClient {
log.info(
"Creating db-scheduler using tasks from Spring context: {}",
configuredTasks
)
// Ensure that we are using a transactional aware data source
val transactionalDataSource = configureDataSource(dataSource)
// Instantiate a new builder
val builder = Scheduler.create(transactionalDataSource, configuredTasks)
.serializer(jacksonSerializer)
.tableName(config.tableName)
.threads(config.threads)
.pollingInterval(config.pollingInterval)
.heartbeatInterval(config.heartbeatInterval)
.jdbcCustomization(
customizer
.jdbcCustomization()
.orElse(AutodetectJdbcCustomization(transactionalDataSource))
)
.deleteUnresolvedAfter(config.deleteUnresolvedAfter)
.failureLogging(config.failureLoggerLevel, config.isFailureLoggerLogStackTrace)
.shutdownMaxWait(config.shutdownMaxWait)
// Polling strategy
when (config.pollingStrategy) {
PollingStrategyConfig.Type.FETCH -> {
builder.pollUsingFetchAndLockOnExecute(
config.pollingStrategyLowerLimitFractionOfThreads,
config.pollingStrategyUpperLimitFractionOfThreads
)
}
PollingStrategyConfig.Type.LOCK_AND_FETCH -> {
builder.pollUsingLockAndFetch(
config.pollingStrategyLowerLimitFractionOfThreads,
config.pollingStrategyUpperLimitFractionOfThreads
)
}
else -> {
throw IllegalArgumentException(
"Unknown polling-strategy: " + config.pollingStrategy
)
}
}
// 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.schedulerName != null) {
builder.schedulerName(SchedulerName.Fixed(config.schedulerName))
}
// Use custom JdbcCustomizer if provided.
if (config.isImmediateExecutionEnabled) {
builder.enableImmediateExecution()
}
// Use custom executor service if provided
// Use custom executor service if provided
customizer.executorService().ifPresent { executorService: ExecutorService? ->
builder.executorService(
executorService
)
}
// Use custom due executor if provided
customizer.dueExecutor().ifPresent { dueExecutor: ExecutorService ->
builder.dueExecutor(
dueExecutor
)
}
// Use housekeeper executor service if provided
customizer.housekeeperExecutor()
.ifPresent { housekeeperExecutor: ScheduledExecutorService? ->
builder.housekeeperExecutor(
housekeeperExecutor
)
}
// // Add recurring jobs and jobs that implements OnStartup
// builder.startTasks(DbSchedulerAutoConfiguration.startupTasks(configuredTasks))
// // Expose metrics
// builder.statsRegistry(registry)
return builder.build()
}
private fun configureDataSource(existingDataSource: DataSource): DataSource {
if (existingDataSource is 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 TransactionAwareDataSourceProxy(existingDataSource)
}
}
in our case we need all features of autoconfiguration and we avoid circular reference with this. But what if the new version of db-scheduler will have changes in this autoconfiguration - we have to change it
What if DbSchedulerAutoConfiguration will be divided to different classes (may be customizers) - in that way we'll have possibility of flexible configuration. Now in other app we have several places of such problem and we decide to divide service classes instead of creating own configuration. And now this is the cause of missunderstandable classes
Steps to Reproduce
- Spring boot app with default autoconfig
- The task is created from service that is used by task
- After running the app it is stopped by circular reference
Context
- DB-Scheduler Version : 12.4.0
- Java Version : 17
- Spring Boot (check for Yes) : Yes, 2.7.6
- Database and Version : PostgreSql 14.5
I can see your pain, but not sure what the solution should be 🤔
You can try @Lazy
annotation when injecting SchedulerClient into your service
If I understand correctly, the problem is this:
ratingService -> scheduler -> tasks -> ... -> ratingService
^
What are these dependencies for?
Why can't it be done like this:
ratingService -> scheduler
someBackgroundWorker -> tasks -> ... -> ratingService