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

Make @Scheduled's fixedDelay work with methods returning Mono #23533

Closed
DavidDuwaer opened this issue Aug 28, 2019 · 5 comments
Closed

Make @Scheduled's fixedDelay work with methods returning Mono #23533

DavidDuwaer opened this issue Aug 28, 2019 · 5 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement

Comments

@DavidDuwaer
Copy link

DavidDuwaer commented Aug 28, 2019

Context

Consider this method

@Scheduled(fixedDelay = 1000)
void job()
{
    // some syncronous work
}

The job() method can be counted upon to never be called until the previous run of job() is finished.

Now consider this method

@Scheduled(fixedDelay = 1000)
void job()
{
    someAsyncMethodReturningAMono()
       .subscribe()
}

Here, job() would be called 1000ms after the previous run of job() returns, not when the the Mono returned by someAsyncMethodReturningAMono() would terminate. If the asynchronous work takes just over a bit longer than 1000ms, it may run at the same time as previous runs. This can be mitigated by using the .block() statement to subscribe to someAsyncMethodReturningAMono(), effectively making job() syncronous again, but it would be better to have a non-blocking answer to this.

Proposal

To let @Scheduled's fixedDelay property, when the annotated method returns a Mono or a Flux, start the delay count from the moment the returned Mono or Flux terminates instead of when the method returns the Mono, so that one can write this:

@Scheduled(fixedDelay = 1000)
Mono job()
{
    return someAsyncMethodReturningAMono();
}

This code would resonate with WebFlux users nicely.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Aug 28, 2019
@DavidDuwaer DavidDuwaer changed the title @Scheduled's fixedDelay work with async (i.e. returning Mono<Void>) methods Make @Scheduled.fixedDelay work with async (i.e. returning Mono<Void>) methods Aug 28, 2019
@DavidDuwaer DavidDuwaer changed the title Make @Scheduled.fixedDelay work with async (i.e. returning Mono<Void>) methods Make @Scheduled's fixedDelay work with methods returning Mono<Void> Aug 28, 2019
@DavidDuwaer DavidDuwaer changed the title Make @Scheduled's fixedDelay work with methods returning Mono<Void> Make @Scheduled's fixedDelay work with methods returning Mono Aug 28, 2019
@jhoeller jhoeller added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement labels Sep 5, 2019
@jhoeller jhoeller self-assigned this Sep 5, 2019
@jhoeller jhoeller added this to the 5.2 GA milestone Sep 5, 2019
@jhoeller jhoeller removed the status: waiting-for-triage An issue we've not yet triaged or decided on label Sep 5, 2019
@jhoeller jhoeller modified the milestones: 5.2 GA, 5.3 M1 Sep 9, 2019
@jhoeller jhoeller modified the milestones: 5.3 M1, 5.3 M2 Feb 24, 2020
@jhoeller jhoeller modified the milestones: 5.3 M2, 5.x Backlog Jul 21, 2020
@jhoeller jhoeller modified the milestones: 6.x Backlog, 6.0.x Nov 1, 2021
@s50600822
Copy link

It does not make sense to me when someone wrote an async method then try to make it run in sync manner. Why couple that into the scheduling.

@DavidDuwaer
Copy link
Author

DavidDuwaer commented Dec 27, 2022

Someone has a choice to return a Mono/Flux from the method. Returning it would denote they want Spring to consider its asynchronous completion. The user can opt not to return the mono.

Furthermore I'm only proposing an API, not an implementation. The implementation could either be truly async or cause a thread to block; that is independent of my proposal.

Your argument applies the same on WebFlux asynchronous REST controller methods. Do you also think those do not make sense?

@jhoeller jhoeller modified the milestones: 6.0.x, 6.1.x Jan 11, 2023
@simonbasle simonbasle assigned simonbasle and unassigned jhoeller Jan 27, 2023
@simonbasle
Copy link
Contributor

simonbasle commented Jan 31, 2023

Currently investigating this. There are several hurdles to overcome which I will below. For reference here is the code that would emulate fixedDelay and fixedRate in the reactive world (based on Reactor operators).

This assumes that the annotated method returns a Publisher<T> (e.g. a Mono<Void>) and that unlike in the current arrangement, that particular kind of return value is used by the annotation processor.

fixedDelay

Assuming we resolved a Duration fixedDelay and a Duration initialDelay (which is ZERO if not configured):

Publisher<?> p; // conceptually the publisher returned by the annotated method

Mono<Void> iterationMono = Flux.from(p).then();
Flux<Void> scheduledFlux = Mono.delay(this.fixedDelay).then(iterationMono).repeat();

if (!this.initialDelay.isZero()) {
	scheduledFlux = Flux.concat(
			Mono.delay(this.initialDelay).then(iterationMono),
			scheduledFlux
	);
}

scheduledFlux.subscribe(it -> {}, ex -> this.logger.error("Unexpected error occurred in scheduled reactive task", ex)

fixedRate

Assuming the same but with a resolved Duration fixedRate:

Publisher<?> p;
Mono<Void> iterationMono = Flux.from(p).then();
Flux<Void> scheduledFlux = Flux.interval(this.initialDelay, this.fixedRate).flatMap(it -> iterationMono);

scheduledFlux.subscribe(it -> {}, ex -> this.logger.error("Unexpected error occurred in scheduled reactive task", ex)

Scope of change

I would consider the following out of scope for this change:

  • supporting cron
  • modifying the ScheduledTaskRegistrar API

I would also challenge the scope of the change with the following questions:

  • should return types other than Publisher be considered?
  • should return types other than subclasses of Publisher<T> be considered ? (see ReactiveAdapterRegistry Caveat below)
  • should we consider <Void> only publishers, i.e. empty ones, or any Publisher<T>? if the later, the onNext events will be ignored and this caveat should be documented

Caveats

Optional dependencies

  • needs Reactor at runtime to implement scheduling
  • needs Reactive Streams at runtime

ReactiveAdapterRegistry

Ideally the support would cover not only straight Publisher-returning methods or Mono/Flux-returning methods but also other reactive return types as long as there is a ReactiveAdapter for them and they support deferred mode (ie. the return value can be subscribed to repeatedly).

  • CompletableFuture is therefore out of scope
  • RxJava Maybe, Completable and Single could typically be converted. note these don't implement Publisher.

Unfortunately, the ReactiveAdapterRegistry doesn't seem to be typically available when the @Scheduled annotation gets processed. It is therefore quite complicated to distinguish methods for which a reactive approach is needed vs classic methods, unless they return a type plainly assignable from Publisher.

A mitigation could be to add an explicit boolean reactiveSupport opt-in parameter to @Scheduled, but that feels a bit on the nose.

This also plays into the next caveat.

Late scheduling, tracking and cancelling

In the current arrangement, a ScheduledTaskRegistrar is used to both track the repeated tasks and also lazily trigger the initial iteration if the infrastructure isn't ready (no TaskScheduler yet).

The issue is that the registrar API only covers tasks that are TriggerTask or IntervalTask, which it wraps in a ScheduledTask, with a Future attached for cancellation during the destroy phase.
This is not really applicable for reactive types. Using a Future would be wasteful, and it would be preferable to track subscription's Disposable for cancellation.

Yet, the late scheduling logic could be useful. A mitigation would be to create a TriggerTask with an immediate Trigger which only triggers once. That task would create the Flux<Void> and subscribe to it. It would also track the Disposable independently, to be stored alongside the ScheduledTask in the bean processor for cancellation.

Note that adding this level of indirection might lead to the ReactiveAdapterRegistry being available, which solves the previous caveat at the expense of added complexity and internal boilerplate.

simonbasle added a commit to simonbasle/spring-framework that referenced this issue Feb 3, 2023
This commit adds support for `@Scheduled` annotation on reactive methods
in fixed delay or fixed rate mode. Cron mode is not supported.

Reactive methods are methods that return a Publisher or a subclass of
Publisher. This is only considered if Reactor (and Reactive Streams)
is present at runtime.

This is implemented using Reactor operators, as a `Flux<Void>` that
repeatedly flatmaps to the `Publisher` in the background, re-subscribing
to it according to the `@Scheduled` configuration. The method which
creates the `Publisher` is only called once.

If the `Publisher` errors, the exception is logged at warn level and
otherwise ignored. As a result the underlying `Flux` will continue
re-subscribing to the `Publisher`.

Note that if Reactor is not present, the standard processing applies
and the method itself will repeatedly be scheduled for execution,
ignoring the returned `Publisher` and not even subscribing to it. This
effectively becomes a no-op operation unless the method has other side
effects.

Closes spring-projectsgh-23533
@DavidDuwaer
Copy link
Author

Thanks for looking into it Simon!

Addressing your 3 points on scope

I would also challenge the scope of the change with the following questions:

  • Should return types other than Publisher be considered?
    Should return types other than subclasses of Publisher be considered ? (see ReactiveAdapterRegistry Caveat below)
    Because it would enable releasing this feature earlier, because not supporting types other than Publisher is easy to communicate in the docs, and because people who would expect (i.e. without finding out about it via the docs) @Scheduler to "await" asynchronous methods are likely WebFlux users, I would say the way to go, at least right now, is to only support Publisher and Mono.
  • should we consider only publishers, i.e. empty ones, or any Publisher? if the later, the onNext events will be ignored and this caveat should be documented
    To minimize documentation dependence ("why does it not work!") / maximize plug-n-play experience, and to allow better multi-purposing of single methods to minimise code (no need to use a wrapper method that converts to Publisher<Void>), I would allow all return types. The fact that users should be familiar with the distinction between the emission of a Publisher's (one and only) value and its completion, and the danger that they might confuse the two, would not be something specific to this feature but is general to Mono and Flux already

@simonbasle
Copy link
Contributor

Superseded-by gh-29924
(let's continue the discussion in the PR which already covers a lot of ground, on the road to 6.1.0-M1)

@simonbasle simonbasle closed this as not planned Won't fix, can't repro, duplicate, stale Feb 7, 2023
@simonbasle simonbasle removed this from the 6.1.x milestone Feb 7, 2023
simonbasle added a commit to simonbasle/spring-framework that referenced this issue Apr 25, 2023
This commit adds support for `@Scheduled` annotation on reactive methods
in fixed delay or fixed rate mode. Cron mode is not supported.

Reactive methods are methods that return a Publisher or a subclass of
Publisher. This is only considered if Reactor (and Reactive Streams)
is present at runtime.

This is implemented using Reactor operators, as a `Flux<Void>` that
repeatedly flatmaps to the `Publisher` in the background, re-subscribing
to it according to the `@Scheduled` configuration. The method which
creates the `Publisher` is only called once.

If the `Publisher` errors, the exception is logged at warn level and
otherwise ignored. As a result the underlying `Flux` will continue
re-subscribing to the `Publisher`.

Note that if Reactor is not present, the standard processing applies
and the method itself will repeatedly be scheduled for execution,
ignoring the returned `Publisher` and not even subscribing to it. This
effectively becomes a no-op operation unless the method has other side
effects.

Closes spring-projectsgh-23533
simonbasle added a commit to simonbasle/spring-framework that referenced this issue May 5, 2023
This commit adds support for `@Scheduled` annotation on reactive methods
in fixed delay or fixed rate mode. Cron mode is not supported.

Reactive methods are methods that return a Publisher or a subclass of
Publisher. This is only considered if Reactor (and Reactive Streams)
is present at runtime.

This is implemented using Reactor operators, as a `Flux<Void>` that
repeatedly flatmaps to the `Publisher` in the background, re-subscribing
to it according to the `@Scheduled` configuration. The method which
creates the `Publisher` is only called once.

If the `Publisher` errors, the exception is logged at warn level and
otherwise ignored. As a result the underlying `Flux` will continue
re-subscribing to the `Publisher`.

Note that if Reactor is not present, the standard processing applies
and the method itself will repeatedly be scheduled for execution,
ignoring the returned `Publisher` and not even subscribing to it. This
effectively becomes a no-op operation unless the method has other side
effects.

Closes spring-projectsgh-23533
bclozel pushed a commit that referenced this issue Jun 5, 2023
This commit adds support for `@Scheduled` annotation on reactive
methods and Kotlin suspending functions.

Reactive methods are methods that return a `Publisher` or a subclass
of `Publisher`. The `ReactiveAdapterRegistry` is used to support many
implementations, such as `Flux`, `Mono`, `Flow`, `Single`, etc.
Methods should not take any argument and published values will be
ignored, as they are already with synchronous support.

This is implemented in `ScheduledAnnotationReactiveSupport`, which
"converts" Publishers to `Runnable`. This strategy keeps track of
active Subscriptions in the `ScheduledAnnotationBeanPostProcessor`,
in order to cancel them all in case of shutdown.
The existing scheduling support for tasks is reused, aligning the
triggering behavior with the existing support: cron, fixedDelay and
fixedRate are all supported strategies.

If the `Publisher` errors, the exception is logged at warn level and
otherwise ignored. As a result new `Runnable` instances will be
created for each execution and scheduling will continue.
The only difference with synchronous support is that error signals
will not be thrown by those `Runnable` tasks and will not be made
available to the `org.springframework.util.ErrorHandler` contract.
This is due to the asynchronous and lazy nature of Publishers.

Closes gh-23533
Closes gh-28515
bclozel added a commit that referenced this issue Jun 5, 2023
This commit adds a note in the reference documentation stating that
`ErrorHandler` infrastructure is not involved when reactive methods send
an error signal: the exception is sent as a message in the pipeline and
is not thrown from the task `Runnable`.

See gh-23533
mdeinum pushed a commit to mdeinum/spring-framework that referenced this issue Jun 29, 2023
This commit adds support for `@Scheduled` annotation on reactive
methods and Kotlin suspending functions.

Reactive methods are methods that return a `Publisher` or a subclass
of `Publisher`. The `ReactiveAdapterRegistry` is used to support many
implementations, such as `Flux`, `Mono`, `Flow`, `Single`, etc.
Methods should not take any argument and published values will be
ignored, as they are already with synchronous support.

This is implemented in `ScheduledAnnotationReactiveSupport`, which
"converts" Publishers to `Runnable`. This strategy keeps track of
active Subscriptions in the `ScheduledAnnotationBeanPostProcessor`,
in order to cancel them all in case of shutdown.
The existing scheduling support for tasks is reused, aligning the
triggering behavior with the existing support: cron, fixedDelay and
fixedRate are all supported strategies.

If the `Publisher` errors, the exception is logged at warn level and
otherwise ignored. As a result new `Runnable` instances will be
created for each execution and scheduling will continue.
The only difference with synchronous support is that error signals
will not be thrown by those `Runnable` tasks and will not be made
available to the `org.springframework.util.ErrorHandler` contract.
This is due to the asynchronous and lazy nature of Publishers.

Closes spring-projectsgh-23533
Closes spring-projectsgh-28515
mdeinum pushed a commit to mdeinum/spring-framework that referenced this issue Jun 29, 2023
This commit adds a note in the reference documentation stating that
`ErrorHandler` infrastructure is not involved when reactive methods send
an error signal: the exception is sent as a message in the pipeline and
is not thrown from the task `Runnable`.

See spring-projectsgh-23533
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants