We are running some BackgroundService workers using either PeriodicTimer or Sgbj.Cron.CronTimer. The problem occurs only when running one specific worker with Sgbj.Cron.CronTimer (no issue with PeriodicTimer).
Using net6.0 and Sgbj.Cron.CronTimer version 1.0.1.
An unhandled exception of type 'System.ArgumentNullException' occurred in System.Private.CoreLib.dllValue cannot be null.
at System.Threading.Monitor.ReliableEnter(Object obj, Boolean& lockTaken)
at System.Threading.Monitor.Enter(Object obj, Boolean& lockTaken)
at Sgbj.Cron.CronTimer.Dispose()
at Sgbj.Cron.CronTimer.Finalize()
It appears that the more frequent we run the worker, the sooner the exception is thrown.
When running every 10 seconds, it throws after ~90seconds.
When running every 20 seconds, it throws after ~180seconds.
At first we though there is some memory leak or similar issue, but when running the same intervals with PeriodicTimer, there is no issue even after much longer time period. Still, the issue is obviously specific to worker's work. While I cannot share the worker-specific code, it frequently uses Entity Framework and transactions.
using Cronos;
using dashboard_service.Configuration.Workers;
using Sgbj.Cron;
namespace dashboard_service.Workers;
public abstract class BackgroundWorker<TScopedService> : BackgroundService
{
private readonly IWorkerConfiguration _configuration;
private readonly ILogger _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
protected BackgroundWorker(IWorkerConfiguration configuration, ILogger logger,
IServiceScopeFactory serviceScopeFactory)
{
_configuration = configuration;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var workerName = GetType().Name;
if (!_configuration.Enabled)
{
_logger.LogInformation("{worker} won't be executed (disabled worker)", workerName);
return;
}
_logger.LogInformation("{worker} will be executed", workerName);
var isCronExpressionValid = _configuration.IsCronExpressionValid();
var isFixedDelayValid = _configuration.IsFixedDelayValid();
if (!(isCronExpressionValid ^ isFixedDelayValid))
{
_logger.LogError("Failed to execute {worker} (invalid configuration)", workerName);
return;
}
using var periodicTimer = isFixedDelayValid ? CreatePeriodicTimer() : null;
using var cronTimer = isCronExpressionValid ? CreateCronTimer() : null;
using var scope = _serviceScopeFactory.CreateScope();
while (!stoppingToken.IsCancellationRequested &&
await WaitForNextTickAsync(periodicTimer, cronTimer, stoppingToken))
{
if (periodicTimer != null)
{
_logger.LogInformation("Executing {worker} work with fixed delay {interval} seconds",
workerName, _configuration.FixedDelaySeconds);
}
else
{
_logger.LogInformation("Executing {worker} work with '{cron}' cron expression",
workerName, _configuration.CronExpression);
}
var service = scope.ServiceProvider.GetService<TScopedService>();
if (service == null)
{
_logger.LogError("Failed to execute {worker} (scoped service not found)", workerName);
return;
}
try
{
await ExecuteWorkAsync(service);
}
catch (Exception exception)
{
_logger.LogError("Failed to execute {worker} (caught exception: {exception})",
workerName, exception);
}
}
}
private static async ValueTask<bool> WaitForNextTickAsync(PeriodicTimer? periodicTimer, CronTimer? cronTimer,
CancellationToken stoppingToken)
{
return (periodicTimer != null && await periodicTimer.WaitForNextTickAsync(stoppingToken))
|| (cronTimer != null && await cronTimer.WaitForNextTickAsync(stoppingToken));
}
private CronTimer CreateCronTimer()
{
var timezone = TimeZoneInfo.Utc;
var cronExpressionValue = _configuration.CronExpression!;
try
{
return new CronTimer(cronExpressionValue, timezone);
}
catch (CronFormatException)
{
var cronExpression = CronExpression.Parse(cronExpressionValue, CronFormat.IncludeSeconds);
return new CronTimer(cronExpression, timezone);
}
}
private PeriodicTimer CreatePeriodicTimer()
{
return new PeriodicTimer(TimeSpan.FromSeconds(_configuration.FixedDelaySeconds!.Value));
}
protected abstract Task ExecuteWorkAsync(TScopedService service);
}