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

Non-reentrant grain timers #8955

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ReubenBond
Copy link
Member

@ReubenBond ReubenBond commented Apr 21, 2024

Fixes #2574

This builds on PRs which need to be merged first.

This PR currently adds a new bool reentrant parameter to RegisterTimer:

protected IGrainTimer RegisterTimer(
    Func<object?, Task> asyncCallback,
    object? state,
    TimeSpan dueTime,
    TimeSpan period,
    bool reentrant = true) // <-- new

The parameter defaults to true to preserves the current behavior. We should not merge it as-is, instead we should decide how we want to expose non-reentrant timers. We could use a different API altogether. At the very least, we should use an overload to retain binary compatibility.

Microsoft Reviewers: Open in CodeFlow

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch 3 times, most recently from 7a68791 to a7f2806 Compare April 21, 2024 23:20
@ReubenBond ReubenBond changed the title WIP: Non-reentrant grain timers Non-reentrant grain timers Apr 21, 2024
@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch from a7f2806 to 7b4fa11 Compare April 22, 2024 04:34
@Rohansi
Copy link

Rohansi commented Apr 22, 2024

It's been a while since I've used Orleans but I think the current timer semantics are to only start counting for the next interval after the current callback completes. I'm not familiar with the internals here but that should definitely be maintained for non-reentrant timers. I saw OneWay mentioned as a possible solution which may affect that so just wanted to mention it.

@rkargMsft
Copy link

A new method RegisterGrainTimer could be an option for exposing non-reentrant as an option as it's like the existing Timer, but more Grain-y, and expose options for the new and existing behavior. The existing RegisterTimer can remain and remain with the old behavior. Maybe add an analyzer that upgrades to the new RegisterGrainTimer method before maybe obsoleting the old one.

That could help make it more clear that this has the ability to be more Grain-integrated (ex. non-reentrant by default, lives for the life of the activation only) than a "normal" .NET timer.

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch 7 times, most recently from 588cd19 to 2767c63 Compare April 28, 2024 19:20
@ReubenBond
Copy link
Member Author

ReubenBond commented Apr 29, 2024

An alternative is to extract the parameters out into an "options" object like this:

/// <summary>
/// Functionality for managing grain timers.
/// </summary>
public interface ITimerRegistry
{
// Existing API:
    IGrainTimer RegisterTimer(IGrainContext grainContext, Func<object?, Task> callback, object? state, TimeSpan dueTime, TimeSpan period);
    
// New API - generic, accepts timer options struct
    IGrainTimer RegisterGrainTimer<T>(IGrainContext grainContext, Func<T, Task> callback, T state, TimerCreationOptions options);
}

/// <summary>
/// Options for registering grain timers.
/// </summary>
public readonly struct TimerCreationOptions()
{
    /// <summary>
    /// The amount of time to delay before the timer callback is invoked.
    /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from starting.
    /// Specify <see cref="TimeSpan.Zero"/> to invoke the callback promptly.
    /// </summary>
    public required TimeSpan DueTime { get; init; }

    /// <summary>
    /// The time interval between invocations of callback.
    /// Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.
    /// </summary>
    public TimeSpan Period { get; init; } = Timeout.InfiniteTimeSpan;

    /// <summary>
    /// Gets a value indicating whether callbacks scheduled by this timer are allowed to interleave execution with other timers and grain calls.
    /// Defaults to <see langword="false"/>.
    /// </summary>
    /// <remarks>
    /// If this value is <see langword="false"/>, the timer callback will be treated akin to a grain call. If the grain scheduling this timer is reentrant
    /// (i.e., it has the <see cref="ReentrantAttribute"/> attributed applied to its implementation class), the timer callback will be allowed
    /// to interleave with other grain calls and timers regardless of the value of this property.
    /// If this value is <see langword="true"/>, the timer callback will be allowed to interleave with other timers and grain calls.
    /// </remarks>
    public bool Interleave { get; init; }

    /// <summary>
    /// Gets a value indicating whether callbacks scheduled by this timer should extend the lifetime of the grain activation.
    /// Defaults to <see langword="false"/>.
    /// </summary>
    /// <remarks>
    /// If this value is <see langword="false"/>, timer callbacks will not extend a grain activation's lifetime.
    /// If a grain is only processing timer callbacks and no other messages, the grain will be collected after its idle collection period expires.
    /// If this value is <see langword="true"/>, timer callback will extend a grain activation's lifetime.
    /// If the timer period is shorter than the grain's idle collection period, the grain will not be collected due to idleness.
    /// </remarks>
    public bool KeepAlive { get; init; }
}

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch from 47bfa8b to d43d20e Compare April 30, 2024 04:43
@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch from bcfaa40 to 0750f0c Compare May 11, 2024 17:45
@ReubenBond
Copy link
Member Author

Added a KeepAlive option, since we don't want to change the grain timer's default semantics. Set it to true to make timers keep grains alive. This opens the path to implement sending requests which do not activate a grain or extend its lifetime (that needs a proposal on the API + semantics first)

@benjaminpetit
Copy link
Member

An alternative is to extract the parameters out into an "options" object like this: [...]

I like that. We can still provide the "old" api for a while so people can migrate, or create their own extension to keep compatibility.

I also like @rkargMsft suggestion to call the new api RegisterGrainTimer, to clearly emphasis that it's not a traditional timer.

@ReubenBond
Copy link
Member Author

ReubenBond commented May 14, 2024

I like that. We can still provide the "old" api for a while so people can migrate or create their own extension to keep compatibility.

@benjaminpetit in regard to maintaining compat, do you also think we should retain the existing implementation?

I also like @rkargMsft suggestion to call the new api RegisterGrainTimer, to clearly emphasis that it's not a traditional timer.

Done, I've updated the API proposal snippet above. Do you feel we should also rename the options arg type, TimerCreationOptions, to something like GrainTimerCreationOptions?

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch from a4ee203 to 186d654 Compare May 14, 2024 19:28
@ElanHasson
Copy link
Contributor

I like this approach!

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch 2 times, most recently from c1ee894 to d5f5769 Compare May 15, 2024 20:11
/// <param name="state">State object that will be passed as argument when calling the asyncCallback.</param>
/// <param name="dueTime">Due time for first timer tick.</param>
/// <param name="period">Period of subsequent timer ticks.</param>
/// <returns>Handle for this Timer.</returns>
/// <seealso cref="IDisposable"/>
protected IDisposable RegisterTimer(Func<object, Task> asyncCallback, object state, TimeSpan dueTime, TimeSpan period)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed parameter & obsoleted API, otherwise kept everything the same.

/// <returns>
/// An <see cref="IGrainTimer"/> instance which represents the timer.
/// </returns>
protected IGrainTimer RegisterGrainTimer<T>(Func<T, Task> callback, T state, GrainTimerCreationOptions options)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New RegisterGrainTimer API. Scheduling information is all in 'options' param.

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch 4 times, most recently from dafb518 to 75ac054 Compare May 16, 2024 01:26
var tmp = _waitingRequests.Select(m => m.Item1).ToList();
_waitingRequests.Clear();
return tmp;
if (message.IsLocalOnly)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never forward local-only requests. We could rename this to IsActivationSpecific or similar if "local-only" isn't clear enough

@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch 8 times, most recently from ee83996 to 385ad1a Compare May 22, 2024 00:40
@ReubenBond ReubenBond force-pushed the feature/non-reentant-grain-timers branch from 385ad1a to 07fff04 Compare May 22, 2024 01:20
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

Successfully merging this pull request may close these issues.

Make timer callbacks respect reentrancy of grains
5 participants