Skip to content

Replace Hangfire with custom CRON job runner #252

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

Merged
merged 7 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,8 @@ FodyWeavers.xsd
*.code-workspace

# Postgress data
postgres-data/
postgres-data/

# VS Code
.vscode

7 changes: 7 additions & 0 deletions HonzaBotner.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
CONTRIBUTING.md = CONTRIBUTING.md
Packages.props = Packages.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HonzaBotner", "src\HonzaBotner\HonzaBotner.csproj", "{04BEA8DF-6FDB-465A-BF6C-D28BADBD572F}"
Expand All @@ -23,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HonzaBotner.Database", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HonzaBotner.Services.Test", "src\HonzaBotner.Services.Test\HonzaBotner.Services.Test.csproj", "{AB6030AC-D422-4A52-A980-08D3836E4437}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HonzaBotner.Scheduler", "src\HonzaBotner.Scheduler\HonzaBotner.Scheduler.csproj", "{3C66CBA5-C1F4-4B3E-8D23-94B15652A452}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -57,6 +60,10 @@ Global
{AB6030AC-D422-4A52-A980-08D3836E4437}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB6030AC-D422-4A52-A980-08D3836E4437}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB6030AC-D422-4A52-A980-08D3836E4437}.Release|Any CPU.Build.0 = Release|Any CPU
{3C66CBA5-C1F4-4B3E-8D23-94B15652A452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3C66CBA5-C1F4-4B3E-8D23-94B15652A452}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C66CBA5-C1F4-4B3E-8D23-94B15652A452}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C66CBA5-C1F4-4B3E-8D23-94B15652A452}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
4 changes: 3 additions & 1 deletion Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<ItemGroup>
<PackageReference Update="Chronic.Core" Version="0.4.0" />
<PackageReference Update="Cronos" Version="0.7.1" />
<PackageReference Update="DSharpPlus" Version="4.1.0" />
<PackageReference Update="DSharpPlus.CommandsNext" Version="4.1.0" />
<PackageReference Update="DSharpPlus.Interactivity" Version="4.1.0" />
Expand All @@ -26,5 +27,6 @@
<PackageReference Update="coverlet.collector" Version="3.1.0" />
<PackageReference Update="xunit" Version="2.4.1" />
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Update="Shouldly" Version="4.0.3" />
</ItemGroup>
</Project>
</Project>
2 changes: 1 addition & 1 deletion src/HonzaBotner.Database/CountedEmoji.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ public class CountedEmoji
{
public ulong Id { get; set; }
public ulong Times { get; set; }
public DateTime FirstUsedAt { get; set; } = DateTime.Now;
public DateTime FirstUsedAt { get; set; } = DateTime.UtcNow;
}
}
4 changes: 2 additions & 2 deletions src/HonzaBotner.Discord.Services/Commands/ReminderCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async Task Create(
string? content
)
{
DateTime now = DateTime.Now;
DateTime now = DateTime.UtcNow;
DateTime? datetime = ParseDateTime(rawDatetime);

if (content == null)
Expand Down Expand Up @@ -89,7 +89,7 @@ await context.RespondErrorAsync(
context.User.Id,
message.Id,
message.ChannelId,
datetime.Value, // This is safe, as the nullability is validated above
datetime.Value.ToUniversalTime(), // This is safe, as the nullability is validated above
content
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static async Task RespondErrorAsync(this CommandContext context, string t
.WithTitle(title.RemoveDiscordMentions(context.Guild))
.WithDescription(content?.RemoveDiscordMentions(context.Guild) ?? "No additional content provided")
.WithColor(DiscordColor.Red)
.WithTimestamp(DateTime.Now)
.WithTimestamp(DateTime.UtcNow)
.Build();

await context.RespondAsync(embed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<ItemGroup>
<ProjectReference Include="..\HonzaBotner.Database\HonzaBotner.Database.csproj" />
<ProjectReference Include="..\HonzaBotner.Discord\HonzaBotner.Discord.csproj" />
<ProjectReference Include="..\HonzaBotner.Scheduler\HonzaBotner.Scheduler.csproj" />
<ProjectReference Include="..\HonzaBotner.Services.Contract\HonzaBotner.Services.Contract.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Chronic.Core" />
<PackageReference Include="Hangfire.Core" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DSharpPlus.Entities;
using Hangfire;
using HonzaBotner.Discord.Managers;
using HonzaBotner.Discord.Services.Options;
using HonzaBotner.Scheduler.Contract;
using HonzaBotner.Services.Contract;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using HonzaBotner.Services.Contract.Dto;

namespace HonzaBotner.Discord.Services.Jobs
{
public class TriggerRemindersJobProvider : IRecurringJobProvider
[Cron("*/5 * * * * *")]
public class TriggerRemindersJobProvider : IJob
{
private readonly IRemindersService _remindersService;

Expand All @@ -39,13 +41,11 @@ IReminderManager reminderManager
_reminderManager = reminderManager;
}

public const string Key = "reminders-trigger";
public string Name { get; } = "reminders-trigger";

public static string CronExpression => Cron.Minutely();

public async Task Run()
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var now = DateTime.Now; // Fix one point in time.
var now = DateTime.UtcNow; // Fix one point in time.
var reminders = await _remindersService.GetRemindersToExecuteAsync(now);
await _remindersService.DeleteExecutedRemindersAsync(now);

Expand Down
2 changes: 1 addition & 1 deletion src/HonzaBotner.Discord/Extensions/DiscordExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ await channel.SendMessageAsync(
.WithColor(DiscordColor.Red)
.AddField("Message:", exception.Message, true)
.AddField("Stack Trace:", Truncate(exception.StackTrace ?? "No stack trace", 500))
.WithTimestamp(DateTime.Now)
.WithTimestamp(DateTime.UtcNow)
.WithDescription(
"Please react to this message to indicate that it is already logged in isssue or solved"
)
Expand Down
15 changes: 15 additions & 0 deletions src/HonzaBotner.Scheduler/Contract/CronAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace HonzaBotner.Scheduler.Contract
{
[AttributeUsage(AttributeTargets.Class)]
public class CronAttribute : Attribute
{
public string Expression { get; }

public CronAttribute(string expression)
{
Expression = expression;
}
}
}
8 changes: 8 additions & 0 deletions src/HonzaBotner.Scheduler/Contract/ICronJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace HonzaBotner.Scheduler.Contract
{
public interface ICronJob : IJob
{
string CronExpression { get; }

}
}
12 changes: 12 additions & 0 deletions src/HonzaBotner.Scheduler/Contract/IJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;

namespace HonzaBotner.Scheduler.Contract
{
public interface IJob
{
string Name { get; }

Task ExecuteAsync(CancellationToken cancellationToken = default);
}
}
39 changes: 39 additions & 0 deletions src/HonzaBotner.Scheduler/CronJobWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using Cronos;
using HonzaBotner.Scheduler.Contract;

namespace HonzaBotner.Scheduler
{
internal class CronJobWrapper
{
private const string CetId = "Central Europe Standard Time";
private readonly TimeZoneInfo _centralEuropeTimezone =
TimeZoneInfo.FindSystemTimeZoneById(CetId);

public CronJobWrapper(ICronJob job, string cronExpression, DateTime nextRunTime)
{
Job = job;
Expression = CronExpression.Parse(cronExpression, CronFormat.IncludeSeconds);
NextRunTime = nextRunTime;
}

public ICronJob Job { get; }
private CronExpression Expression { get; }

public DateTime NextRunTime { get; private set; }
public DateTime LastRunTime { get; private set; }


public void Next()
{
LastRunTime = NextRunTime;
NextRunTime = Expression.GetNextOccurrence(NextRunTime, _centralEuropeTimezone) ?? NextRunTime;
}

public bool ShouldRun(DateTime currentTime)
{
return NextRunTime < currentTime && LastRunTime != NextRunTime;
}
}
}

9 changes: 9 additions & 0 deletions src/HonzaBotner.Scheduler/HonzaBotner.Scheduler.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Cronos" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions src/HonzaBotner.Scheduler/InvalidConfigurationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace HonzaBotner.Scheduler
{
public class InvalidConfigurationException : Exception
{
public Type Type { get; }

public InvalidConfigurationException(string message, Type type) : base(message)
{
Type = type;
}
}
}
68 changes: 68 additions & 0 deletions src/HonzaBotner.Scheduler/SchedulerHostedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HonzaBotner.Scheduler.Contract;

namespace HonzaBotner.Scheduler
{
public class SchedulerHostedService : BackgroundService
{
private readonly int _delay;
private readonly ILogger<SchedulerHostedService> _logger;
private readonly IList<CronJobWrapper> _cronJobs;

public SchedulerHostedService(int delay, IEnumerable<ICronJob> cronJobs, ILogger<SchedulerHostedService> logger)
{
DateTime now = DateTime.UtcNow;

_delay = delay;
_logger = logger;
_cronJobs = cronJobs
.Select(j => new CronJobWrapper(j, j.CronExpression, now))
.ToList();
}

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
DateTime currentTime = DateTime.UtcNow;

_logger.LogInformation("Scheduler running at: {Time}", currentTime.ToLocalTime());
await RunOnceAsync(currentTime, cancellationToken);

await Task.Delay(_delay, cancellationToken);
}
}

private async Task RunOnceAsync(DateTime currentTime, CancellationToken cancellationToken)
{
TaskFactory taskFactory = new(TaskScheduler.Current);

IList<CronJobWrapper> jobsToRun = _cronJobs.Where(job => job.ShouldRun(currentTime)).ToList();

foreach (CronJobWrapper cronJob in jobsToRun)
{
cronJob.Next();

await taskFactory.StartNew(async () =>
{
try
{
_logger.LogInformation("Starting job {JobType}", cronJob.Job.Name);
await cronJob.Job.ExecuteAsync(cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e,"Job {JobType} failed", cronJob.Job.Name);
// NOTE: Exception is not propagated, since we dont want crash scheduler.
}
}, cancellationToken);
}
}
}
}
36 changes: 36 additions & 0 deletions src/HonzaBotner.Scheduler/ScopedSchedulerJobWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using HonzaBotner.Scheduler.Contract;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace HonzaBotner.Scheduler
{
internal class ScopedSchedulerJobProvider<TJob> : ICronJob where TJob : class, IJob
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedSchedulerJobProvider<TJob>> _logger;
public string Name { get; } = typeof(TJob).Name;
public string CronExpression { get; }

public ScopedSchedulerJobProvider(string cronExpression, IServiceProvider serviceProvider,
ILogger<ScopedSchedulerJobProvider<TJob>> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;

CronExpression = cronExpression;
}

public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
_logger.LogDebug("Creating scope for {JobName}", Name);
await using AsyncServiceScope scope = _serviceProvider.CreateAsyncScope();

IJob cronJob = scope.ServiceProvider.GetRequiredService<TJob>();

await cronJob.ExecuteAsync(cancellationToken);
}
}
}
Loading