Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
strelchm opened this issue Apr 17, 2024 · 3 comments

Comments

@strelchm
Copy link

strelchm commented Apr 17, 2024

[ 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

  • DB-Scheduler Version : 12.4.0
  • Java Version : 17
  • Spring Boot (check for Yes) : Yes, 2.7.6
  • Database and Version : PostgreSql 14.5
@kagkarlsson
Copy link
Owner

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

@ucw
Copy link

ucw commented Apr 18, 2024

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

@DmitrySadchikov
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants