Skip to content
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
75 changes: 29 additions & 46 deletions src/TickerQ.Utilities/TickerFunctionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ namespace TickerQ.Utilities
/// </summary>
public static class TickerFunctionProvider
{
private static readonly object _buildLock = new();

// Callback actions to collect registrations
private static Action<Dictionary<string, (string, Type)>> _requestTypeRegistrations;
private static Action<Dictionary<string, (string RequestType, string RequestExampleJson)>> _requestInfoRegistrations;
private static Action<Dictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)>> _functionRegistrations;

// Final frozen dictionaries
public static FrozenDictionary<string, (string, Type)> TickerFunctionRequestTypes;
public static FrozenDictionary<string, (string RequestType, string RequestExampleJson)> TickerFunctionRequestInfos;
public static FrozenDictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)> TickerFunctions;
public static FrozenDictionary<string, (string, Type)> TickerFunctionRequestTypes = FrozenDictionary<string, (string, Type)>.Empty;
public static FrozenDictionary<string, (string RequestType, string RequestExampleJson)> TickerFunctionRequestInfos = FrozenDictionary<string, (string RequestType, string RequestExampleJson)>.Empty;
public static FrozenDictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)> TickerFunctions = FrozenDictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)>.Empty;

public static bool IsBuilt { get; private set; }

/// <summary>
/// Registers ticker functions during application startup by adding to the callback chain.
Expand Down Expand Up @@ -158,57 +162,36 @@ internal static void UpdateCronExpressionsFromIConfiguration(IConfiguration conf
/// </summary>
public static void Build()
{
// Build functions dictionary
if (_functionRegistrations != null)
lock (_buildLock)
{
// Single pass: execute callbacks directly on final dictionary
var functionsDict = new Dictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)>();
_functionRegistrations(functionsDict);
TickerFunctions = functionsDict.ToFrozenDictionary();
_functionRegistrations = null; // Release callback chain
}
else
{
if (TickerFunctions == null)
// Build functions dictionary
if (_functionRegistrations != null)
{
TickerFunctions = new Dictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)>()
.ToFrozenDictionary();
var functionsDict = new Dictionary<string, (string cronExpression, TickerTaskPriority Priority, TickerFunctionDelegate Delegate, int MaxConcurrency)>();
_functionRegistrations(functionsDict);
TickerFunctions = functionsDict.ToFrozenDictionary();
_functionRegistrations = null;
}
}

// Build request types dictionary
if (_requestTypeRegistrations != null)
{
// Single pass: execute callbacks directly on final dictionary
var requestTypesDict = new Dictionary<string, (string, Type)>();
_requestTypeRegistrations(requestTypesDict);
TickerFunctionRequestTypes = requestTypesDict.ToFrozenDictionary();
_requestTypeRegistrations = null; // Release callback chain
}
else
{
if (TickerFunctionRequestTypes == null)
// Build request types dictionary
if (_requestTypeRegistrations != null)
{
TickerFunctionRequestTypes = new Dictionary<string, (string, Type)>()
.ToFrozenDictionary();
var requestTypesDict = new Dictionary<string, (string, Type)>();
_requestTypeRegistrations(requestTypesDict);
TickerFunctionRequestTypes = requestTypesDict.ToFrozenDictionary();
_requestTypeRegistrations = null;
}
}

// Build request info dictionary (string type + example JSON)
if (_requestInfoRegistrations != null)
{
var requestInfoDict = new Dictionary<string, (string RequestType, string RequestExampleJson)>();
_requestInfoRegistrations(requestInfoDict);
TickerFunctionRequestInfos = requestInfoDict.ToFrozenDictionary();
_requestInfoRegistrations = null;
}
else
{
if (TickerFunctionRequestInfos == null)
// Build request info dictionary (string type + example JSON)
if (_requestInfoRegistrations != null)
{
TickerFunctionRequestInfos = new Dictionary<string, (string RequestType, string RequestExampleJson)>()
.ToFrozenDictionary();
var requestInfoDict = new Dictionary<string, (string RequestType, string RequestExampleJson)>();
_requestInfoRegistrations(requestInfoDict);
TickerFunctionRequestInfos = requestInfoDict.ToFrozenDictionary();
_requestInfoRegistrations = null;
}

IsBuilt = true;
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public static IServiceCollection AddTickerQ<TTimeTicker, TCronTicker>(this IServ
services.AddSingleton(_ => optionInstance);
services.AddSingleton(_ => tickerExecutionContext);
services.AddSingleton(_ => schedulerOptionsBuilder);

// Register AFTER initializer and scheduler to ensure it runs last
services.AddHostedService<TickerQStartupValidator>();

return services;
}

Expand Down
45 changes: 45 additions & 0 deletions src/TickerQ/Src/BackgroundServices/TickerQStartupValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TickerQ.Utilities;
using TickerQ.Utilities.Enums;

namespace TickerQ.BackgroundServices;

internal class TickerQStartupValidator : IHostedService
{
private readonly TickerExecutionContext _executionContext;
private readonly TickerQInitializerHostedService _initializer;
private readonly ILogger<TickerQStartupValidator> _logger;

public TickerQStartupValidator(
TickerExecutionContext executionContext,
TickerQInitializerHostedService initializer,
ILogger<TickerQStartupValidator> logger)
{
_executionContext = executionContext;
_initializer = initializer;
_logger = logger;
}

public Task StartAsync(CancellationToken cancellationToken)
{
if (!_initializer.InitializationRequested)
{
const string message = "TickerQ — UseTickerQ() was not called. Call app.UseTickerQ() before app.Run() to initialize the scheduler.";
_logger.LogWarning(message);
_executionContext.NotifyCoreAction?.Invoke(message, CoreNotifyActionType.NotifyHostExceptionMessage);
}
else if (TickerFunctionProvider.TickerFunctions.Count == 0)
{
const string message = "TickerQ — No ticker functions registered. Ensure you have methods decorated with [TickerFunction].";
_logger.LogWarning(message);
_executionContext.NotifyCoreAction?.Invoke(message, CoreNotifyActionType.NotifyHostExceptionMessage);
}

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
10 changes: 7 additions & 3 deletions tests/TickerQ.Tests/TickerFunctionProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ private static void ResetProvider()
var type = typeof(TickerFunctionProvider);
const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;

type.GetField("TickerFunctions", flags)!.SetValue(null, null);
type.GetField("TickerFunctionRequestTypes", flags)!.SetValue(null, null);
type.GetField("TickerFunctionRequestInfos", flags)!.SetValue(null, null);
type.GetField("TickerFunctions", flags)!.SetValue(null,
System.Collections.Frozen.FrozenDictionary<string, (string, TickerTaskPriority, TickerFunctionDelegate, int)>.Empty);
type.GetField("TickerFunctionRequestTypes", flags)!.SetValue(null,
System.Collections.Frozen.FrozenDictionary<string, (string, Type)>.Empty);
type.GetField("TickerFunctionRequestInfos", flags)!.SetValue(null,
System.Collections.Frozen.FrozenDictionary<string, (string, string)>.Empty);
type.GetField("_functionRegistrations", flags)!.SetValue(null, null);
type.GetField("_requestTypeRegistrations", flags)!.SetValue(null, null);
type.GetField("_requestInfoRegistrations", flags)!.SetValue(null, null);
type.GetProperty("IsBuilt", flags)!.SetValue(null, false);
}

private static Task NoOpDelegate(CancellationToken ct, IServiceProvider sp, TickerQ.Utilities.Base.TickerFunctionContext ctx)
Expand Down
Loading