kagkarlsson / db-scheduler

Persistent cluster-friendly scheduler for Java
Apache License 2.0
1.23k stars 188 forks source link

Avoiding circular reference if task calling service == task creating service #479

Open strelchm opened 4 months ago

strelchm commented 4 months ago

[ 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

  1. Spring boot app with default autoconfig
  2. The task is created from service that is used by task
  3. After running the app it is stopped by circular reference

Context

kagkarlsson commented 4 months ago

I can see your pain, but not sure what the solution should be 🤔

ucw commented 4 months ago

You can try @Lazy annotation when injecting SchedulerClient into your service

DmitrySadchikov commented 4 months ago

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