jamesmh / coravel

Near-zero config .NET library that makes advanced application features like Task Scheduling, Caching, Queuing, Event Broadcasting, and more a breeze!
https://docs.coravel.net/Installation/
MIT License
3.82k stars 252 forks source link

Adding new task to scheduler from another service #77

Closed soend closed 5 years ago

soend commented 5 years ago

I would like to add tasks to scheduler dynamically in a service what gets the execution times from configuration file. At the moment its done in the application Configure method and every time configuration is changed i would have to restart the whole program. Is this something what could be possible with current scheduler implementation?

soend commented 5 years ago

For testing i tried tried injecting IScheduler and adding new task to it and it actually worked. Code what i tried:

public ScheduleOrderService(ILogger<ScheduleOrderService> logger, IOptions<AppConfig> config, IScheduler scheduler)
{
    _logger = logger;
    _config = config.Value;
    scheduler.Schedule(() => _logger.LogInformation("foobar") ).EveryMinute();
}
jamesmh commented 5 years ago

If there's a way to hook into some event that's emited when the config files change, here's what you could do:

When scheduling, add an ID to each scheduled event (this is an internal method, which maybe I should make public via the interface...)

scheduler.EveryMinute().AssignUniqueIndentifier("SomeID");

Keep track of all the IDS that you have (statically).

Then, when you need to refresh - do this for each ID:

scheduler.TryUnschedule("SomeID");

Then re-schedule again.

Those are thread safe methods too.

Not necessarily recommended for production, but would work in dev for sure (assuming you can detect when the config files change - which Im looking into)

jamesmh commented 5 years ago

So you can detect when the IConfiguration is changed.

Take a look here https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getreloadtoken?view=aspnetcore-2.2

soend commented 5 years ago

There will be actually just one scheduled task ever created what i would like to change the execution time on. So if there would be a way to unschedule it and schedule new one that should do the business.

jamesmh commented 5 years ago

So try in Configure()

ChangeToken.OnChange(() => this.Configuration.GetReloadToken(), () => {
    using (var scope = app.ApplicationServices.CreateScope())
    {
        IScheduler scheduler = scope.ServiceProvider.GetRequiredService<IScheduler>();
        IConfiguration config = scope.ServiceProvider.GetRequiredService<IConfiguration>();

        // Remove your past scheduled event/task
        (scheduler as Scheduler).TryUnschedule("SomeID");

        // Add a new one.
        var scheduledEvent = scheduler
            .Schedule(() => Console.WriteLine("A new scheduled task!"))
            .Cron(config["MyCronConfigValue"]);

        // Assign a unique ID so you can remove it next time.
        (scheduledEvent as ScheduledEvent).AssignUniqueIndentifier("SomeID");
        //Or
        scheduledEvent.PreventOverlapping("SomeID");
    }
});
jhelmink commented 7 months ago

This can stay closed, but in case anyone is looking for a generic way to update scheduled tasks using reflection;

/// <summary>
/// Update the schedule for a specific service endpoint.
/// </summary>
/// <param name="fullServiceName">Fully qualified service name</param>
/// <param name="cron">string representation of a CRON schedule (5 values)</param>
/// <param name="scheduler">[FromServices] so not required</param>
/// <returns>string of result</returns>
public async Task<IResult> UpdateServiceSchedule(string fullServiceName, string cron, [FromServices] IScheduler scheduler)
{
    _log.LogInformation($"UpdateServiceSchedule called for {fullServiceName} setting CRON to: [{cron}]");

    if (string.IsNullOrEmpty(fullServiceName))
        return Results.BadRequest("Service Name is required");

    // Validate the cron
    if (string.IsNullOrEmpty(cron))
        return Results.BadRequest("CRON is required");
    else if (!CronSchedules.IsValidCRONSchedule(cron))
        return Results.BadRequest("CRON is not valid");

    // Try to reschedule as per;
    // https://github.com/jamesmh/coravel/issues/77#issuecomment-460785919
    try
    {
        // Remove your past scheduled event/task
        var unscheduleSuccess = (scheduler as Scheduler)!.TryUnschedule(fullServiceName);
        if (!unscheduleSuccess)
        {
            var errorMessage = $"Error unscheduling service: {fullServiceName}";
            _log.LogError(errorMessage);
            return Results.BadRequest(errorMessage);
        }

        // Check we have configured a class for this service
        var serviceType = Type.GetType("ServiceBroker.ScheduledInvocables." + fullServiceName);
        if (serviceType == null)
        {
            var errorMessage = $"Error finding service type for scheduling: {fullServiceName}";
            _log.LogError(errorMessage);
            return Results.BadRequest(errorMessage);
        }

        // Check if the serviceType implements the IInvocable interface
        Type interfaceType = typeof(IInvocable);
        bool implementsInterface = serviceType.GetInterfaces().Contains(interfaceType);
        if(!implementsInterface)
        {
            var errorMessage = $"Service type {fullServiceName} exists but is not invocable for scheduling";
            _log.LogError(errorMessage);
            return Results.BadRequest(errorMessage);
        }

        // Add a new cron schedule for the type we want to re-schedule
        var scheduledEvent = scheduler
            .ScheduleInvocableType(serviceType)
            .Cron(cron);

        // Assign a unique Id so you can remove and update it next time.
        var scheduleEventConfig = (scheduledEvent as ScheduledEvent)!.AssignUniqueIndentifier(fullServiceName);
        scheduleEventConfig = scheduledEvent.PreventOverlapping(fullServiceName);

        // We did it!
        var successMessage = $"UpdateServiceSchedule updated {fullServiceName} CRON setting to: [{cron}]";
        _log.LogInformation(successMessage);

        return Results.Ok(successMessage);
    }
    catch (Exception ex)
    {
        var exceptionMessage = $"Error updating service schedule: {fullServiceName} CRON setting to: [{cron}]";
        _log.LogError(ex, exceptionMessage);
        return Results.BadRequest(exceptionMessage);
    }
}

Many thanks to James for ScheduleInvocableType(serviceType) option!