From ae984cb2fd0a69d405530e792ada7571b7b690ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 27 Nov 2025 16:12:57 +0000 Subject: [PATCH] Sync changes from main to net9 - Applied recent commits, updated versions & framework, preserved .csproj files --- .../Enums/TickerQStartMode.cs | 9 +++ .../Interfaces/ITickerQDispatcher.cs | 6 ++ .../Managers/TickerManager.cs | 6 +- .../Temps/NoOpTickerQDispatcher.cs | 21 +++++ .../Temps/NoOpTickerQHostScheduler.cs | 35 ++++++++ src/TickerQ.Utilities/TickerOptionsBuilder.cs | 17 ++++ .../TickerQServiceExtensions.cs | 81 ++++++++++++------- .../Src/Dispatcher/TickerQDispatcher.cs | 2 + .../TickerOptionsBuilderTests.cs | 24 ++++++ 9 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs create mode 100644 src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs diff --git a/src/TickerQ.Utilities/Enums/TickerQStartMode.cs b/src/TickerQ.Utilities/Enums/TickerQStartMode.cs index e650325a..16ead1fe 100644 --- a/src/TickerQ.Utilities/Enums/TickerQStartMode.cs +++ b/src/TickerQ.Utilities/Enums/TickerQStartMode.cs @@ -2,7 +2,16 @@ namespace TickerQ.Utilities.Enums { public enum TickerQStartMode { + /// + /// Start job processing immediately when UseTickerQ is called. + /// Background services are registered and start automatically. + /// Immediate, + + /// + /// Background services are registered but skip the first run. + /// Job processing needs to be started manually via ITickerQHostScheduler. + /// Manual } } \ No newline at end of file diff --git a/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs b/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs index c31df0e3..12ec1817 100644 --- a/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs +++ b/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs @@ -6,6 +6,12 @@ namespace TickerQ.Utilities.Interfaces { public interface ITickerQDispatcher { + /// + /// Indicates whether the dispatcher is functional (background services enabled). + /// When false, DispatchAsync will be a no-op. + /// + bool IsEnabled { get; } + Task DispatchAsync(InternalFunctionContext[] contexts, CancellationToken cancellationToken = default); } } diff --git a/src/TickerQ.Utilities/Managers/TickerManager.cs b/src/TickerQ.Utilities/Managers/TickerManager.cs index 99ccdfd0..cf6649d9 100644 --- a/src/TickerQ.Utilities/Managers/TickerManager.cs +++ b/src/TickerQ.Utilities/Managers/TickerManager.cs @@ -103,7 +103,8 @@ private async Task> AddTimeTickerAsync(TTimeTicker ent // Persist first await _persistenceProvider.AddTimeTickers([entity], cancellationToken: cancellationToken); - if (executionTime <= now.AddSeconds(1)) + // Only try to dispatch immediately if dispatcher is enabled (background services running) + if (_dispatcher.IsEnabled && executionTime <= now.AddSeconds(1)) { // Acquire and mark InProgress in one provider call var acquired = await _persistenceProvider @@ -347,7 +348,8 @@ private async Task>> AddTimeTickersBatchAsync(Lis await _notificationHubSender.AddTimeTickersBatchNotifyAsync().ConfigureAwait(false); - if (immediateTickers.Count > 0) + // Only try to dispatch immediately if dispatcher is enabled (background services running) + if (_dispatcher.IsEnabled && immediateTickers.Count > 0) { var acquired = await _persistenceProvider .AcquireImmediateTimeTickersAsync(immediateTickers.ToArray(), cancellationToken) diff --git a/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs b/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs new file mode 100644 index 00000000..68273f4f --- /dev/null +++ b/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using TickerQ.Utilities.Interfaces; +using TickerQ.Utilities.Models; + +namespace TickerQ.Utilities.Temps; + +/// +/// No-operation implementation of ITickerQDispatcher. +/// Used when background services are disabled (queue-only mode). +/// +internal class NoOpTickerQDispatcher : ITickerQDispatcher +{ + public bool IsEnabled => false; + + public Task DispatchAsync(InternalFunctionContext[] contexts, CancellationToken cancellationToken = default) + { + // No-op: dispatcher not available in queue-only mode + return Task.CompletedTask; + } +} diff --git a/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs b/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs new file mode 100644 index 00000000..0195a286 --- /dev/null +++ b/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using TickerQ.Utilities.Interfaces; + +namespace TickerQ.Utilities.Temps; + +/// +/// No-operation implementation of ITickerQHostScheduler. +/// Used when background services are disabled (queue-only mode). +/// +internal class NoOpTickerQHostScheduler : ITickerQHostScheduler +{ + public bool IsRunning => false; + + public Task StartAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void RestartIfNeeded(DateTime? dateTime) + { + // No-op: scheduler not running + } + + public void Restart() + { + // No-op: scheduler not running + } +} diff --git a/src/TickerQ.Utilities/TickerOptionsBuilder.cs b/src/TickerQ.Utilities/TickerOptionsBuilder.cs index fc782fb3..bf51fe32 100644 --- a/src/TickerQ.Utilities/TickerOptionsBuilder.cs +++ b/src/TickerQ.Utilities/TickerOptionsBuilder.cs @@ -34,6 +34,12 @@ internal TickerOptionsBuilder(TickerExecutionContext tickerExecutionContext, Sch /// Defaults to true. /// internal bool SeedDefinedCronTickers { get; set; } = true; + + /// + /// Controls whether background services (job processors) should be registered. + /// Defaults to true. Set to false to only register managers for queuing jobs. + /// + internal bool RegisterBackgroundServices { get; set; } = true; /// /// Seeding delegate for time tickers, executed with the application's service provider. @@ -98,6 +104,17 @@ public TickerOptionsBuilder IgnoreSeedDefinedCronTicke SeedDefinedCronTickers = false; return this; } + + /// + /// Disables background services registration. + /// Use this when you only want to queue jobs without processing them in this application. + /// Only the managers (ITimeTickerManager, ICronTickerManager) will be available for queuing jobs. + /// + public TickerOptionsBuilder DisableBackgroundServices() + { + RegisterBackgroundServices = false; + return this; + } /// /// Configure a custom seeder for time tickers, executed on application startup. diff --git a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs index cd57b22b..916c5fd7 100644 --- a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs +++ b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs @@ -49,22 +49,34 @@ public static IServiceCollection AddTickerQ(this IServ services.AddSingleton, TickerInMemoryPersistenceProvider>(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(provider => - provider.GetRequiredService()); - services.AddHostedService(provider => - provider.GetRequiredService()); - services.AddHostedService(provider => provider.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => + + // Only register background services if enabled (default is true) + if (optionInstance.RegisterBackgroundServices) + { + services.AddSingleton(); + services.AddSingleton(provider => + provider.GetRequiredService()); + services.AddHostedService(provider => + provider.GetRequiredService()); + services.AddHostedService(provider => provider.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + { + var notification = sp.GetRequiredService(); + var notifyDebounce = new SoftSchedulerNotifyDebounce((value) => notification.UpdateActiveThreads(value)); + return new TickerQTaskScheduler(schedulerOptionsBuilder.MaxConcurrency, schedulerOptionsBuilder.IdleWorkerTimeOut, notifyDebounce); + }); + } + else { - var notification = sp.GetRequiredService(); - var notifyDebounce = new SoftSchedulerNotifyDebounce((value) => notification.UpdateActiveThreads(value)); - return new TickerQTaskScheduler(schedulerOptionsBuilder.MaxConcurrency, schedulerOptionsBuilder.IdleWorkerTimeOut, notifyDebounce); - }); + // Register NoOp implementations when background services are disabled + services.AddSingleton(); + services.AddSingleton(); + } + + services.AddSingleton(); optionInstance.ExternalProviderConfigServiceAction?.Invoke(services); optionInstance.DashboardServiceAction?.Invoke(services); @@ -85,25 +97,32 @@ public static IApplicationBuilder UseTickerQ(this IApplicationBuilder app, Ticke var configuration = serviceProvider.GetService(); var notificationHubSender = serviceProvider.GetService(); var backgroundScheduler = serviceProvider.GetService(); - backgroundScheduler.SkipFirstRun = qStartMode == TickerQStartMode.Manual; - TickerFunctionProvider.UpdateCronExpressionsFromIConfiguration(configuration); - TickerFunctionProvider.Build(); - - tickerExecutionContext.NotifyCoreAction += (value, type) => + // If background services are registered, configure them + if (backgroundScheduler != null) { - if (type == CoreNotifyActionType.NotifyHostExceptionMessage) + backgroundScheduler.SkipFirstRun = qStartMode == TickerQStartMode.Manual; + + tickerExecutionContext.NotifyCoreAction += (value, type) => { - notificationHubSender.UpdateHostException(value); - tickerExecutionContext.LastHostExceptionMessage = (string)value; - } - else if (type == CoreNotifyActionType.NotifyNextOccurence) - notificationHubSender.UpdateNextOccurrence(value); - else if (type == CoreNotifyActionType.NotifyHostStatus) - notificationHubSender.UpdateHostStatus(value); - else if (type == CoreNotifyActionType.NotifyThreadCount) - notificationHubSender.UpdateActiveThreads(value); - }; + if (type == CoreNotifyActionType.NotifyHostExceptionMessage) + { + notificationHubSender.UpdateHostException(value); + tickerExecutionContext.LastHostExceptionMessage = (string)value; + } + else if (type == CoreNotifyActionType.NotifyNextOccurence) + notificationHubSender.UpdateNextOccurrence(value); + else if (type == CoreNotifyActionType.NotifyHostStatus) + notificationHubSender.UpdateHostStatus(value); + else if (type == CoreNotifyActionType.NotifyThreadCount) + notificationHubSender.UpdateActiveThreads(value); + }; + } + // If background services are not registered (due to DisableBackgroundServices()), + // silently skip background service configuration. This is expected behavior. + + TickerFunctionProvider.UpdateCronExpressionsFromIConfiguration(configuration); + TickerFunctionProvider.Build(); // Run core seeding pipeline based on main options (works for both in-memory and EF providers). var options = tickerExecutionContext.OptionsSeeding; diff --git a/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs b/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs index ff83f183..95b36f2f 100644 --- a/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs +++ b/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs @@ -12,6 +12,8 @@ internal class TickerQDispatcher : ITickerQDispatcher private readonly TickerQTaskScheduler _taskScheduler; private readonly TickerExecutionTaskHandler _taskHandler; + public bool IsEnabled => true; + public TickerQDispatcher(TickerQTaskScheduler taskScheduler, TickerExecutionTaskHandler taskHandler) { _taskScheduler = taskScheduler ?? throw new ArgumentNullException(nameof(taskScheduler)); diff --git a/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs b/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs index 195086c3..e8d3b781 100644 --- a/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs +++ b/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs @@ -151,4 +151,28 @@ public void ConfigureScheduler_Invokes_Delegate() schedulerOptions.MaxConcurrency.Should().Be(42); schedulerOptions.NodeIdentifier.Should().Be("test-node"); } + + [Fact] + public void DisableBackgroundServices_Sets_Flag_To_False() + { + var executionContext = new TickerExecutionContext(); + var schedulerOptions = new SchedulerOptionsBuilder(); + + var builder = new TickerOptionsBuilder(executionContext, schedulerOptions); + + // Default should be true + var defaultFlag = typeof(TickerOptionsBuilder) + .GetProperty("RegisterBackgroundServices", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(builder); + defaultFlag.Should().BeOfType().Which.Should().BeTrue(); + + // After calling DisableBackgroundServices, should be false + builder.DisableBackgroundServices(); + + var flag = typeof(TickerOptionsBuilder) + .GetProperty("RegisterBackgroundServices", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(builder); + + flag.Should().BeOfType().Which.Should().BeFalse(); + } }