From 18f71e5a3cd82974ab69ffdea424bc4d12d44edf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Mar 2026 01:04:29 +0000 Subject: [PATCH 1/2] Sync changes from main to net9 --- .../Endpoints/DashboardEndpoints.cs | 21 +++++ .../Dashboard/TickerDashboardRepository.cs | 20 +++++ .../src/http/services/cronTickerService.ts | 11 +++ .../services/types/cronTickerService.types.ts | 2 + .../wwwroot/src/views/CronTicker.vue | 89 +++++++++++++++++++ .../CronTickerConfigurations.cs | 4 + .../Entities/CronTickerEntity.cs | 1 + .../Interfaces/ITickerDashboardRepository.cs | 1 + .../TickerInMemoryPersistenceProvider.cs | 3 +- tests/TickerQ.Tests/RetryBehaviorTests.cs | 9 +- tests/TickerQ.Tests/RetryExhaustionTests.cs | 8 +- .../TickerCancellationTokenManagerTests.cs | 1 + .../TickerExecutionTaskHandlerTests.cs | 8 +- 13 files changed, 174 insertions(+), 4 deletions(-) diff --git a/src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs b/src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs index 18c30a4d..5bcff086 100644 --- a/src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs +++ b/src/TickerQ.Dashboard/Endpoints/DashboardEndpoints.cs @@ -138,6 +138,10 @@ public static void MapDashboardEndpoints(this IEndpoin .WithName("UpdateCronTicker") .WithSummary("Update cron ticker"); + apiGroup.MapPut("/cron-ticker/toggle", ToggleCronTicker) + .WithName("ToggleCronTicker") + .WithSummary("Toggle cron ticker enabled/disabled"); + apiGroup.MapPost("/cron-ticker/run", RunCronTickerOnDemand) .WithName("RunCronTickerOnDemand") .WithSummary("Run cron ticker on demand"); @@ -551,6 +555,23 @@ private static async Task UpdateCronTicker( }, dashboardOptions.DashboardJsonOptions); } + private static async Task ToggleCronTicker( + Guid id, + bool isEnabled, + ITickerDashboardRepository repository, + DashboardOptionsBuilder dashboardOptions, + CancellationToken cancellationToken) + where TTimeTicker : TimeTickerEntity, new() + where TCronTicker : CronTickerEntity, new() + { + var success = await repository.ToggleCronTickerAsync(id, isEnabled, cancellationToken); + + return Results.Json(new { + success, + message = success ? $"Cron ticker {(isEnabled ? "enabled" : "disabled")} successfully" : "Failed to toggle cron ticker" + }, dashboardOptions.DashboardJsonOptions); + } + private static async Task RunCronTickerOnDemand( Guid id, ITickerDashboardRepository repository, diff --git a/src/TickerQ.Dashboard/Infrastructure/Dashboard/TickerDashboardRepository.cs b/src/TickerQ.Dashboard/Infrastructure/Dashboard/TickerDashboardRepository.cs index 0794defa..c04fd0a8 100644 --- a/src/TickerQ.Dashboard/Infrastructure/Dashboard/TickerDashboardRepository.cs +++ b/src/TickerQ.Dashboard/Infrastructure/Dashboard/TickerDashboardRepository.cs @@ -569,6 +569,26 @@ public async Task> GetCronTickersOccurrence return finalData; } + public async Task ToggleCronTickerAsync(Guid id, bool isEnabled, CancellationToken cancellationToken) + { + var cronTicker = await _persistenceProvider.GetCronTickerById(id, cancellationToken); + if (cronTicker == null) + return false; + + cronTicker.IsEnabled = isEnabled; + cronTicker.UpdatedAt = DateTime.UtcNow; + + var affectedRows = await _persistenceProvider.UpdateCronTickers([cronTicker], cancellationToken); + + if (affectedRows > 0) + { + _tickerQHostScheduler.Restart(); + await _notificationHubSender.UpdateCronTickerNotifyAsync(cronTicker); + } + + return affectedRows > 0; + } + public bool CancelTickerById(Guid tickerId) => TickerCancellationTokenManager.RequestTickerCancellationById(tickerId); diff --git a/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerService.ts b/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerService.ts index dca0a2ff..af2228d1 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerService.ts +++ b/src/TickerQ.Dashboard/wwwroot/src/http/services/cronTickerService.ts @@ -176,6 +176,16 @@ const getTimeTickersGraphData = () => { }; } +const toggleCronTicker = () => { + const baseHttp = useBaseHttpService('single') + const requestAsync = async (id: string, isEnabled: boolean) => (await baseHttp.sendAsync("PUT", "cron-ticker/toggle", { paramData: { id, isEnabled } })); + + return { + ...baseHttp, + requestAsync + }; +} + export const cronTickerService = { getCronTickers, getCronTickersPaginated, @@ -183,6 +193,7 @@ export const cronTickerService = { addCronTicker, deleteCronTicker, runCronTickerOnDemand, + toggleCronTicker, getTimeTickersGraphDataRange, getTimeTickersGraphDataRangeById, getTimeTickersGraphData diff --git a/src/TickerQ.Dashboard/wwwroot/src/http/services/types/cronTickerService.types.ts b/src/TickerQ.Dashboard/wwwroot/src/http/services/types/cronTickerService.types.ts index 3b3b5067..8d42d102 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/http/services/types/cronTickerService.types.ts +++ b/src/TickerQ.Dashboard/wwwroot/src/http/services/types/cronTickerService.types.ts @@ -12,6 +12,7 @@ export class GetCronTickerResponse { createdAt!:string; updatedAt!:string; retries!:number; + isEnabled!:boolean; actions:string|undefined = undefined; } @@ -22,6 +23,7 @@ export class UpdateCronTickerRequest { retries?: number; description?: string; intervals?:number[]; + isEnabled?: boolean; } export class GetCronTickerGraphDataRangeResponse{ diff --git a/src/TickerQ.Dashboard/wwwroot/src/views/CronTicker.vue b/src/TickerQ.Dashboard/wwwroot/src/views/CronTicker.vue index f6e95567..67d5d7a1 100644 --- a/src/TickerQ.Dashboard/wwwroot/src/views/CronTicker.vue +++ b/src/TickerQ.Dashboard/wwwroot/src/views/CronTicker.vue @@ -42,6 +42,7 @@ const getCronTickerRangeGraphDataById = cronTickerService.getTimeTickersGraphDat const getCronTickersGraphDataAndParseToGraph = cronTickerService.getTimeTickersGraphData() const deleteCronTicker = cronTickerService.deleteCronTicker() const runCronTickerOnDemand = cronTickerService.runCronTickerOnDemand() +const toggleCronTicker = cronTickerService.toggleCronTicker() // Pagination state const currentPage = ref(1) @@ -76,6 +77,9 @@ const handlePageSizeChange = async (size: number) => { const confirmDialog = useDialog().withComponent( () => import('@/components/common/ConfirmDialog.vue'), ) +const toggleConfirmDialog = useDialog().withComponent( + () => import('@/components/common/ConfirmDialog.vue'), +) const cronOccurrenceDialog = useDialog<{ id: string retries: number @@ -468,6 +472,36 @@ const RunCronTickerOnDemand = async (id: string) => { await loadPageData() } +const ToggleCronTickerEnabled = (id: string, isEnabled: boolean) => { + toggleConfirmDialog.open({ + ...new ConfirmDialogProps(), + id, + isEnabled, + icon: isEnabled ? 'mdi-power-off' : 'mdi-power', + iconColor: isEnabled ? '#F44336' : '#4CAF50', + title: isEnabled ? 'Disable Cron Ticker' : 'Enable Cron Ticker', + text: isEnabled + ? 'Are you sure you want to disable this cron ticker? It will stop generating new occurrences.' + : 'Are you sure you want to enable this cron ticker? It will start generating occurrences again.', + confirmText: isEnabled ? 'Disable' : 'Enable', + confirmColor: isEnabled ? '#F44336' : '#4CAF50', + }) +} + +const onSubmitToggleConfirmDialog = async () => { + try { + const id = toggleConfirmDialog.propData?.id! + const isEnabled = toggleConfirmDialog.propData?.isEnabled! + toggleConfirmDialog.close() + await toggleCronTicker.requestAsync(id, isEnabled) + await loadPageData() + } catch (error: any) { + if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED') { + return + } + } +} + onMounted(async () => { try { isMounted.value = true @@ -1221,6 +1255,23 @@ const refreshData = async () => { @@ -1872,6 +1931,28 @@ const refreshData = async () => { border-color: rgba(255, 255, 255, 0.15); } +.enable-btn { + color: #4caf50; + border-color: rgba(76, 175, 80, 0.2); +} + +.enable-btn:hover { + border-color: rgba(76, 175, 80, 0.5); + box-shadow: 0 8px 25px rgba(76, 175, 80, 0.4); + background: rgba(76, 175, 80, 0.15); +} + +.disable-btn { + color: #ffa726; + border-color: rgba(255, 167, 38, 0.2); +} + +.disable-btn:hover { + border-color: rgba(255, 167, 38, 0.5); + box-shadow: 0 8px 25px rgba(255, 167, 38, 0.4); + background: rgba(255, 167, 38, 0.15); +} + .table-container { background: linear-gradient(135deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.15) 100%); border-radius: 12px; @@ -2064,6 +2145,14 @@ const refreshData = async () => { background: rgba(229, 115, 115, 0.15); } +.btn-disabled { + color: #616161 !important; + border-color: rgba(97, 97, 97, 0.2) !important; + opacity: 0.5; + cursor: not-allowed !important; + pointer-events: none; +} + .expression-tooltip { cursor: help; text-decoration: underline; diff --git a/src/TickerQ.EntityFrameworkCore/Configurations/CronTickerConfigurations.cs b/src/TickerQ.EntityFrameworkCore/Configurations/CronTickerConfigurations.cs index 94bd9d32..ffcf2a5a 100644 --- a/src/TickerQ.EntityFrameworkCore/Configurations/CronTickerConfigurations.cs +++ b/src/TickerQ.EntityFrameworkCore/Configurations/CronTickerConfigurations.cs @@ -26,6 +26,10 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex("Function", "Expression") .HasDatabaseName("IX_Function_Expression"); + builder.Property(e => e.IsEnabled) + .IsRequired() + .HasDefaultValue(true); + builder.ToTable("CronTickers", _schema); } } diff --git a/src/TickerQ.Utilities/Entities/CronTickerEntity.cs b/src/TickerQ.Utilities/Entities/CronTickerEntity.cs index 98897ed8..13d0196b 100644 --- a/src/TickerQ.Utilities/Entities/CronTickerEntity.cs +++ b/src/TickerQ.Utilities/Entities/CronTickerEntity.cs @@ -9,5 +9,6 @@ public class CronTickerEntity : BaseTickerEntity public virtual byte[] Request { get; set; } public virtual int Retries { get; set; } public virtual int[] RetryIntervals { get; set; } + public virtual bool IsEnabled { get; set; } = true; } } \ No newline at end of file diff --git a/src/TickerQ.Utilities/Interfaces/ITickerDashboardRepository.cs b/src/TickerQ.Utilities/Interfaces/ITickerDashboardRepository.cs index c947c98a..a4538d3c 100644 --- a/src/TickerQ.Utilities/Interfaces/ITickerDashboardRepository.cs +++ b/src/TickerQ.Utilities/Interfaces/ITickerDashboardRepository.cs @@ -26,6 +26,7 @@ internal interface ITickerDashboardRepository Task[]> GetCronTickersOccurrencesAsync(Guid guid, CancellationToken cancellationToken = default); Task>> GetCronTickersOccurrencesPaginatedAsync(Guid guid, int pageNumber, int pageSize, CancellationToken cancellationToken = default); Task> GetCronTickersOccurrencesGraphDataAsync(Guid guid, CancellationToken cancellationToken = default); + Task ToggleCronTickerAsync(Guid id, bool isEnabled, CancellationToken cancellationToken = default); bool CancelTickerById(Guid tickerId); Task DeleteCronTickerOccurrenceByIdAsync(Guid id, CancellationToken cancellationToken = default); Task<(string, int)> GetTickerRequestByIdAsync(Guid tickerId, TickerType tickerType, CancellationToken cancellationToken = default); diff --git a/src/TickerQ/Src/Provider/TickerInMemoryPersistenceProvider.cs b/src/TickerQ/Src/Provider/TickerInMemoryPersistenceProvider.cs index 39544c40..45ff9dbd 100644 --- a/src/TickerQ/Src/Provider/TickerInMemoryPersistenceProvider.cs +++ b/src/TickerQ/Src/Provider/TickerInMemoryPersistenceProvider.cs @@ -563,9 +563,10 @@ public Task MigrateDefinedCronTickers((string Function, string Expression)[] cro public Task GetAllCronTickerExpressions(CancellationToken cancellationToken) { var result = CronTickers.Values + .Where(x => x.IsEnabled) .Cast() .ToArray(); - + return Task.FromResult(result); } diff --git a/tests/TickerQ.Tests/RetryBehaviorTests.cs b/tests/TickerQ.Tests/RetryBehaviorTests.cs index dd1870f5..14a3bac1 100644 --- a/tests/TickerQ.Tests/RetryBehaviorTests.cs +++ b/tests/TickerQ.Tests/RetryBehaviorTests.cs @@ -4,12 +4,19 @@ using TickerQ.Utilities.Interfaces.Managers; using Microsoft.Extensions.DependencyInjection; using TickerQ.Utilities.Instrumentation; +using TickerQ.Utilities; using TickerQ.Utilities.Models; namespace TickerQ.Tests; -public class RetryBehaviorTests +[Collection("TickerCancellationTokenState")] +public class RetryBehaviorTests : IDisposable { + public void Dispose() + { + TickerCancellationTokenManager.CleanUpTickerCancellationTokens(); + } + // End-to-end unit tests that call the public ExecuteTaskAsync with a CronTickerOccurrence // so RunContextFunctionAsync + retry logic is exercised. Tests use short intervals (1..3s). diff --git a/tests/TickerQ.Tests/RetryExhaustionTests.cs b/tests/TickerQ.Tests/RetryExhaustionTests.cs index da295551..5c3d6cc5 100644 --- a/tests/TickerQ.Tests/RetryExhaustionTests.cs +++ b/tests/TickerQ.Tests/RetryExhaustionTests.cs @@ -9,8 +9,14 @@ namespace TickerQ.Tests; -public class RetryExhaustionTests +[Collection("TickerCancellationTokenState")] +public class RetryExhaustionTests : IDisposable { + public void Dispose() + { + TickerCancellationTokenManager.CleanUpTickerCancellationTokens(); + } + private readonly ITickerClock _clock; private readonly IInternalTickerManager _internalManager; private readonly ITickerQInstrumentation _instrumentation; diff --git a/tests/TickerQ.Tests/TickerCancellationTokenManagerTests.cs b/tests/TickerQ.Tests/TickerCancellationTokenManagerTests.cs index 2310057a..80ceb2dd 100644 --- a/tests/TickerQ.Tests/TickerCancellationTokenManagerTests.cs +++ b/tests/TickerQ.Tests/TickerCancellationTokenManagerTests.cs @@ -7,6 +7,7 @@ namespace TickerQ.Tests; +[Collection("TickerCancellationTokenState")] public class TickerCancellationTokenManagerTests : IDisposable { public void Dispose() diff --git a/tests/TickerQ.Tests/TickerExecutionTaskHandlerTests.cs b/tests/TickerQ.Tests/TickerExecutionTaskHandlerTests.cs index 7f1139bf..d27d6510 100644 --- a/tests/TickerQ.Tests/TickerExecutionTaskHandlerTests.cs +++ b/tests/TickerQ.Tests/TickerExecutionTaskHandlerTests.cs @@ -10,8 +10,14 @@ namespace TickerQ.Tests; -public class TickerExecutionTaskHandlerTests +[Collection("TickerCancellationTokenState")] +public class TickerExecutionTaskHandlerTests : IDisposable { + public void Dispose() + { + TickerCancellationTokenManager.CleanUpTickerCancellationTokens(); + } + private readonly ITickerClock _clock; private readonly IInternalTickerManager _internalManager; private readonly ITickerQInstrumentation _instrumentation; From 0ccac6ffd5cd5db475d867d479d33d8077e5b13e Mon Sep 17 00:00:00 2001 From: Arcenox Date: Sat, 14 Mar 2026 02:14:03 +0100 Subject: [PATCH 2/2] Add IsEnabled support to sync-excluded EF Core files for net9 Add IsEnabled to ForCronTickerExpressions mapping and filter disabled cron tickers in GetAllCronTickerExpressions. These files are excluded from git sync and need manual updates per target framework. Co-Authored-By: Claude Opus 4.6 --- .../Infrastructure/BasePersistenceProvider.cs | 2 ++ .../Infrastructure/MappingExtensions.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs index 1e9fb743..01eb3b24 100644 --- a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs +++ b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs @@ -346,6 +346,7 @@ public async Task GetAllCronTickerExpressions(CancellationTo var dbContext = session.Context; return await dbContext.Set() .AsNoTracking() + .Where(x => x.IsEnabled) .Select(MappingExtensions.ForCronTickerExpressions()) .ToArrayAsync(ct) .ConfigureAwait(false); @@ -360,6 +361,7 @@ public async Task GetAllCronTickerExpressions(CancellationTo var dbContext = session.Context; return await dbContext.Set() .AsNoTracking() + .Where(x => x.IsEnabled) .Select(MappingExtensions.ForCronTickerExpressions()) .ToArrayAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/TickerQ.EntityFrameworkCore/Infrastructure/MappingExtensions.cs b/src/TickerQ.EntityFrameworkCore/Infrastructure/MappingExtensions.cs index de9206f4..caa19cbb 100644 --- a/src/TickerQ.EntityFrameworkCore/Infrastructure/MappingExtensions.cs +++ b/src/TickerQ.EntityFrameworkCore/Infrastructure/MappingExtensions.cs @@ -17,7 +17,8 @@ internal static class MappingExtensions Expression = e.Expression, Function = e.Function, RetryIntervals = e.RetryIntervals, - Retries = e.Retries + Retries = e.Retries, + IsEnabled = e.IsEnabled }; internal static Expression> ForQueueTimeTickers() where TTimeTicker : TimeTickerEntity, new()