How to create a global variable that can be unique per hosted service execution?
Complete Code: https://github.com/matvi/dotnet-hosted-services
The problem:
When running hosted services is difficult to keep track of the execution without logs. In order to keep track of the execution of the hosted services I implemented logs with a unique traceId (GUID)
The problem is that the TraceLogId is being created per Task using Static Memory and when 2 task runs at the same time the first TraceLogId gets overridden by the second task.
Is there any way to avoid the traceLogId being overridden?
public static class GlobalVariables
{
public static Guid TraceLogId { get; set; }
}
public class Task1Service : ITask1Service
{
public async Task StartAsync(CancellationToken cancellationToken)
{
GlobalVariables.TraceLogId = Guid.NewGuid();
Console.WriteLine($"Task1 executing with traceLogId = {GlobalVariables.TraceLogId}");
Console.WriteLine($"Task1 will wait 5 seconds = {GlobalVariables.TraceLogId}");
await Task.Delay(5000, cancellationToken);
Console.WriteLine($"Task1 ending = {GlobalVariables.TraceLogId}");
}
}
public class Task2Service : ITask2Service
{
public async Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Task2 executing");
GlobalVariables.TraceLogId = Guid.NewGuid();
Console.WriteLine($"Task2 executing with traceLogId = {GlobalVariables.TraceLogId}");
Console.WriteLine($"Task2 ending = {GlobalVariables.TraceLogId}");
}
}
When the code is executed Task1 gets a TraceLogId but when it finishes it has the traceLogId that was assigned in Task2.
using System;
using System.Threading;
using System.Threading.Tasks;
using Cronos;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace HostedServicesPoc.Tasks
{
public abstract class CronJobServiceBase : IHostedService, IDisposable
{
private readonly ILogger _log;
private readonly HostedServiceTaskSettingsBase _hostedServiceTaskSettingsBase;
private System.Timers.Timer _timer;
private readonly CronExpression _expression;
private readonly TimeZoneInfo _timeZoneInfo;
protected CronJobServiceBase(IOptions<HostedServiceTaskSettingsBase> hostedServiceSettings, ILogger<CronJobServiceBase> log)
{
_log = log;
_hostedServiceTaskSettingsBase = hostedServiceSettings?.Value;
_expression = CronExpression.Parse(_hostedServiceTaskSettingsBase.CronExpressionTimer, CronFormat.Standard);
_timeZoneInfo = TimeZoneInfo.Local;
}
public virtual async Task StartAsync(CancellationToken cancellationToken)
{
_log.LogInformation($"{GetType()} is Starting");
if (_hostedServiceTaskSettingsBase.Active)
{
await ScheduleJob(cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_log.LogInformation($"{GetType()} is Stopping");
return Task.CompletedTask;
}
private async Task ScheduleJob(CancellationToken cancellationToken)
{
var next = _expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo);
if (next.HasValue)
{
var delay = next.Value - DateTimeOffset.Now;
if (delay.TotalMilliseconds <= 0) // prevent non-positive values from being passed into Timer
{
await ScheduleJob(cancellationToken);
}
_timer = new System.Timers.Timer(delay.TotalMilliseconds);
_timer.Elapsed += async (sender, args) =>
{
_timer.Dispose(); // reset and dispose timer
_timer = null;
if (!cancellationToken.IsCancellationRequested)
{
await ExecuteTaskAsync(cancellationToken);
}
if (!cancellationToken.IsCancellationRequested)
{
await ScheduleJob(cancellationToken); // reschedule next
}
};
_timer.Start();
}
await Task.CompletedTask;
}
protected virtual async Task ExecuteTaskAsync(CancellationToken cancellationToken)
{
await Task.Delay(5000, cancellationToken);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool dispose)
{
try
{
if (dispose)
{
_timer?.Dispose();
}
}
finally
{
}
}
}
}
TaskServices:
public class Task1HostedService : CronJobServiceBase
{
private readonly IServiceProvider _serviceProvider;
public Task1HostedService(
IOptions<Task1HostedServiceSettings> hostedServiceSettings,
ILogger<CronJobServiceBase> log,
IServiceProvider serviceProvider) : base(hostedServiceSettings, log)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteTaskAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var task1Service = scope.ServiceProvider.GetRequiredService<ITask1Service>();
await task1Service.StartAsync(cancellationToken);
}
}
I recommend a scoped value for this; AsyncLocal<T>
fits the bill.
public static class GlobalVariables
{
private static AsyncLocal<Guid> _TraceLogId = new();
public static Guid TraceLogId => _TraceLogId.Value;
public static IDisposable SetTraceLogId(Guid value)
{
var oldValue = _TraceLogId.Value;
_TraceLogId.Value = value;
return Disposable.Create(() => _TraceLogId.Value = oldValue);
}
}
public class Task1Service : ITask1Service
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var traceIdScope = GlobalVariables.SetTraceLogId(Guid.NewGuid());
Console.WriteLine($"Task1 executing with traceLogId = {GlobalVariables.TraceLogId}");
Console.WriteLine($"Task1 will wait 5 seconds = {GlobalVariables.TraceLogId}");
await Task.Delay(5000, cancellationToken);
Console.WriteLine($"Task1 ending = {GlobalVariables.TraceLogId}");
}
}
This uses Disposable
from my Disposables library.