From dec3aee0aef9b05eba3a4f0c5b48bf45967239cc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:15:24 -0800 Subject: [PATCH 001/203] update sln --- Microsoft.DurableTask.sln | 71 ++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index a3a2af17..a86daa23 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,15 +71,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged", "src\Worker\AzureManaged\Worker.AzureManaged.csproj", "{6106872F-A730-4A75-9267-1B2E2C2DC18C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged", "src\Client\AzureManaged\Client.AzureManaged.csproj", "{E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged", "src\Client\AzureManaged\Client.AzureManaged.csproj", "{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged", "src\Worker\AzureManaged\Worker.AzureManaged.csproj", "{1110FD38-94C6-4374-BF9D-D3D43A129600}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{11357B31-9A63-4A5A-9BC5-091952B25BC0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", "test\Client\AzureManaged.Tests\Client.AzureManaged.Tests.csproj", "{43E540F8-39E6-484C-9F4C-5C745378741A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", "test\Client\AzureManaged.Tests\Client.AzureManaged.Tests.csproj", "{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{1E5C2E83-7B6B-425A-9C9B-0B887D273B12}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CECADDB5-E30A-4CE2-8604-9AC596D4A2DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{3272C041-F81D-4C85-A4FB-2A700B5A7A9D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -195,34 +197,26 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU - {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.Build.0 = Release|Any CPU - {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.Build.0 = Release|Any CPU - {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.Build.0 = Release|Any CPU - {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.Build.0 = Release|Any CPU - {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.Build.0 = Release|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU - {D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.Build.0 = Release|Any CPU + {E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2}.Release|Any CPU.Build.0 = Release|Any CPU + {1110FD38-94C6-4374-BF9D-D3D43A129600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1110FD38-94C6-4374-BF9D-D3D43A129600}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1110FD38-94C6-4374-BF9D-D3D43A129600}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1110FD38-94C6-4374-BF9D-D3D43A129600}.Release|Any CPU.Build.0 = Release|Any CPU + {43E540F8-39E6-484C-9F4C-5C745378741A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43E540F8-39E6-484C-9F4C-5C745378741A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43E540F8-39E6-484C-9F4C-5C745378741A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43E540F8-39E6-484C-9F4C-5C745378741A}.Release|Any CPU.Build.0 = Release|Any CPU + {1E5C2E83-7B6B-425A-9C9B-0B887D273B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E5C2E83-7B6B-425A-9C9B-0B887D273B12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E5C2E83-7B6B-425A-9C9B-0B887D273B12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E5C2E83-7B6B-425A-9C9B-0B887D273B12}.Release|Any CPU.Build.0 = Release|Any CPU + {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -258,11 +252,12 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} - {6106872F-A730-4A75-9267-1B2E2C2DC18C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {11357B31-9A63-4A5A-9BC5-091952B25BC0} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {A15BA625-DC6B-4C6D-8673-0CB08F1B9737} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {E2E47F34-AFDD-4FAD-B1AB-66B1C4911EF2} = {1C217BB2-CE16-41CC-9D47-0FC0DB60BDB3} + {1110FD38-94C6-4374-BF9D-D3D43A129600} = {5B448FF6-EC42-491D-A22E-1DC8B618E6D5} + {43E540F8-39E6-484C-9F4C-5C745378741A} = {5AD837BC-78F3-4543-8AA3-DF74D0DF94C0} + {1E5C2E83-7B6B-425A-9C9B-0B887D273B12} = {51DC98A3-0193-4C66-964B-C26C748E25B6} + {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} = {E5637F81-2FB9-4CD7-900D-455363B142A7} + {3272C041-F81D-4C85-A4FB-2A700B5A7A9D} = {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} From 72349334361064a4b5392d8fb1da5618fd124401 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:15:36 -0800 Subject: [PATCH 002/203] save --- schedulebasedonentity/CreateScheduler.cs | 36 ++++++++++ schedulebasedonentity/GenerateDailyReport.cs | 17 +++++ schedulebasedonentity/ScheduleEntity.cs | 73 ++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 schedulebasedonentity/CreateScheduler.cs create mode 100644 schedulebasedonentity/GenerateDailyReport.cs create mode 100644 schedulebasedonentity/ScheduleEntity.cs diff --git a/schedulebasedonentity/CreateScheduler.cs b/schedulebasedonentity/CreateScheduler.cs new file mode 100644 index 00000000..a5348ed0 --- /dev/null +++ b/schedulebasedonentity/CreateScheduler.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask.Client.Entities; + +public static class CreateSchedule +{ + [FunctionName("CreateSchedule")] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); + string cronExpression = req.Query["cron"]; + string orchestrationName = req.Query["orchestration"]; + + if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) + { + return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + + // Create the schedule + await client.SignalEntityAsync(schedulerId, "CreateSchedule", new + { + ScheduleName = scheduleName, + CronExpression = cronExpression, + OrchestrationName = orchestrationName + }); + + return new OkObjectResult($"Schedule '{scheduleName}' has been created."); + } +} diff --git a/schedulebasedonentity/GenerateDailyReport.cs b/schedulebasedonentity/GenerateDailyReport.cs new file mode 100644 index 00000000..2ff79019 --- /dev/null +++ b/schedulebasedonentity/GenerateDailyReport.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; + +public static class GenerateDailyReport +{ + [FunctionName("GenerateDailyReport")] + public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context) + { + string reportDate = DateTime.UtcNow.ToString("yyyy-MM-dd"); + Console.WriteLine($"Generating daily financial report for {reportDate}"); + + // Simulate work + await Task.Delay(1000); + } +} diff --git a/schedulebasedonentity/ScheduleEntity.cs b/schedulebasedonentity/ScheduleEntity.cs new file mode 100644 index 00000000..ec61763f --- /dev/null +++ b/schedulebasedonentity/ScheduleEntity.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Cronos; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Newtonsoft.Json; + +[JsonObject(MemberSerialization.OptIn)] +public class SchedulerEntity +{ + [JsonProperty("schedule")] + private ScheduleMetadata? schedule; + + public void CreateSchedule(string scheduleName, string cronExpression, string orchestrationName) + { + if (this.schedule != null) + { + throw new InvalidOperationException("Schedule already exists."); + } + + this.schedule = new ScheduleMetadata + { + Name = scheduleName, + CronExpression = cronExpression, + OrchestrationName = orchestrationName, + NextRun = DateTime.UtcNow, // Set next run to now to run immediately + }; + + // Signal the entity to run the schedule immediately + ctx.SignalEntity(ctx.EntityId, "RunSchedule"); + } + + public async Task RunSchedule(IDurableEntityContext ctx) + { + if (schedule == null) + { + throw new InvalidOperationException("Schedule not created."); + } + + // Wait until the next scheduled time + TimeSpan delay = schedule.NextRun - DateTime.UtcNow; + if (delay > TimeSpan.Zero) + { + await ctx.CreateTimer(schedule.NextRun, CancellationToken.None); + } + + // Trigger the target orchestration + var instanceId = Guid.NewGuid().ToString(); + ctx.SignalEntity(new EntityId(schedule.OrchestrationName, instanceId), "Run"); + + // Update the next run time + schedule.NextRun = CronExpressionParser.GetNextOccurrence(schedule.CronExpression, DateTime.UtcNow); + + // Reschedule by signaling itself + ctx.SignalEntity(ctx.EntityId, "RunSchedule"); + } + + [FunctionName(nameof(SchedulerEntity))] + public static Task Run([EntityTrigger] IDurableEntityContext context) + { + return context.DispatchAsync(); + } +} + +public class ScheduleMetadata +{ + public string Name { get; set; } = null!; + public string CronExpression { get; set; } = null!; + public string OrchestrationName { get; set; } = null!; + public DateTime NextRun { get; set; } +} From c95b539bda5d8c5139f0c6b0b337cbf3637d0e8c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 24 Jan 2025 03:55:16 -0800 Subject: [PATCH 003/203] save --- schedulebasedonentity/CreateScheduler.cs | 36 ---- schedulebasedonentity/ScheduleEntity.cs | 61 +++++- schedulebasedonentity/SchedulerEndpoints.cs | 125 +++++++++++++ schedulebasedonentity/make_req.http | 20 ++ .../Entities/Schedule/Schedule.cs | 175 ++++++++++++++++++ .../Entities/Schedule/ScheduleState.cs | 8 + 6 files changed, 387 insertions(+), 38 deletions(-) delete mode 100644 schedulebasedonentity/CreateScheduler.cs create mode 100644 schedulebasedonentity/SchedulerEndpoints.cs create mode 100644 schedulebasedonentity/make_req.http create mode 100644 src/Abstractions/Entities/Schedule/Schedule.cs create mode 100644 src/Abstractions/Entities/Schedule/ScheduleState.cs diff --git a/schedulebasedonentity/CreateScheduler.cs b/schedulebasedonentity/CreateScheduler.cs deleted file mode 100644 index a5348ed0..00000000 --- a/schedulebasedonentity/CreateScheduler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.DurableTask.Client.Entities; - -public static class CreateSchedule -{ - [FunctionName("CreateSchedule")] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); - string cronExpression = req.Query["cron"]; - string orchestrationName = req.Query["orchestration"]; - - if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) - { - return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - - // Create the schedule - await client.SignalEntityAsync(schedulerId, "CreateSchedule", new - { - ScheduleName = scheduleName, - CronExpression = cronExpression, - OrchestrationName = orchestrationName - }); - - return new OkObjectResult($"Schedule '{scheduleName}' has been created."); - } -} diff --git a/schedulebasedonentity/ScheduleEntity.cs b/schedulebasedonentity/ScheduleEntity.cs index ec61763f..64da0e27 100644 --- a/schedulebasedonentity/ScheduleEntity.cs +++ b/schedulebasedonentity/ScheduleEntity.cs @@ -26,12 +26,64 @@ public void CreateSchedule(string scheduleName, string cronExpression, string or CronExpression = cronExpression, OrchestrationName = orchestrationName, NextRun = DateTime.UtcNow, // Set next run to now to run immediately + IsEnabled = true }; // Signal the entity to run the schedule immediately ctx.SignalEntity(ctx.EntityId, "RunSchedule"); } + public void UpdateSchedule(string scheduleName, string cronExpression, string orchestrationName) + { + if (this.schedule == null) + { + throw new InvalidOperationException("Schedule does not exist."); + } + + this.schedule.Name = scheduleName; + this.schedule.CronExpression = cronExpression; + this.schedule.OrchestrationName = orchestrationName; + this.schedule.NextRun = DateTime.UtcNow; // Reset next run to now after update + } + + public ScheduleMetadata GetSchedule() + { + if (this.schedule == null) + { + throw new InvalidOperationException("Schedule does not exist."); + } + + return this.schedule; + } + + public void EnableSchedule() + { + if (this.schedule == null) + { + throw new InvalidOperationException("Schedule does not exist."); + } + + this.schedule.IsEnabled = true; + this.schedule.NextRun = DateTime.UtcNow; // Start immediately when enabled + ctx.SignalEntity(ctx.EntityId, "RunSchedule"); + } + + public void DisableSchedule() + { + if (this.schedule == null) + { + throw new InvalidOperationException("Schedule does not exist."); + } + + this.schedule.IsEnabled = false; + } + + async Task TriggerOrchestration(IDurableEntityContext ctx) + { + string instanceId = await ctx.CallOrchestratorAsync("GenerateDailyReport"); + ctx.SetState(this); // Save entity state + } + public async Task RunSchedule(IDurableEntityContext ctx) { if (schedule == null) @@ -39,6 +91,11 @@ public async Task RunSchedule(IDurableEntityContext ctx) throw new InvalidOperationException("Schedule not created."); } + if (!schedule.IsEnabled) + { + return; // Don't run if schedule is disabled + } + // Wait until the next scheduled time TimeSpan delay = schedule.NextRun - DateTime.UtcNow; if (delay > TimeSpan.Zero) @@ -47,8 +104,7 @@ public async Task RunSchedule(IDurableEntityContext ctx) } // Trigger the target orchestration - var instanceId = Guid.NewGuid().ToString(); - ctx.SignalEntity(new EntityId(schedule.OrchestrationName, instanceId), "Run"); + await TriggerOrchestration(ctx); // Update the next run time schedule.NextRun = CronExpressionParser.GetNextOccurrence(schedule.CronExpression, DateTime.UtcNow); @@ -70,4 +126,5 @@ public class ScheduleMetadata public string CronExpression { get; set; } = null!; public string OrchestrationName { get; set; } = null!; public DateTime NextRun { get; set; } + public bool IsEnabled { get; set; } } diff --git a/schedulebasedonentity/SchedulerEndpoints.cs b/schedulebasedonentity/SchedulerEndpoints.cs new file mode 100644 index 00000000..adbdcec9 --- /dev/null +++ b/schedulebasedonentity/SchedulerEndpoints.cs @@ -0,0 +1,125 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.DurableTask; +using Microsoft.DurableTask.Client.Entities; +using System.IO; +using System; + +public static class SchedulerEndpoints +{ + [FunctionName("CreateSchedule")] + public static async Task CreateSchedule( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); + string cronExpression = req.Query["cron"]; + string orchestrationName = req.Query["orchestration"]; + + if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) + { + return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + + // Create the schedule + await client.SignalEntityAsync(schedulerId, "CreateSchedule", new + { + ScheduleName = scheduleName, + CronExpression = cronExpression, + OrchestrationName = orchestrationName + }); + + return new OkObjectResult($"Schedule '{scheduleName}' has been created."); + } + + [FunctionName("UpdateSchedule")] + public static async Task UpdateSchedule( + [HttpTrigger(AuthorizationLevel.Function, "put")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); + string cronExpression = req.Query["cron"]; + string orchestrationName = req.Query["orchestration"]; + + if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) + { + return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + + // Update the schedule + await client.SignalEntityAsync(schedulerId, "UpdateSchedule", new + { + ScheduleName = scheduleName, + CronExpression = cronExpression, + OrchestrationName = orchestrationName + }); + + return new OkObjectResult($"Schedule '{scheduleName}' has been updated."); + } + + [FunctionName("GetSchedule")] + public static async Task GetSchedule( + [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = req.Query["name"]; + + if (string.IsNullOrEmpty(scheduleName)) + { + return new BadRequestObjectResult("Schedule name is required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + var state = await client.ReadEntityStateAsync(schedulerId); + + if (!state.EntityExists) + { + return new NotFoundObjectResult($"Schedule '{scheduleName}' not found."); + } + + var schedule = state.EntityState.GetSchedule(); + return new OkObjectResult(schedule); + } + + [FunctionName("EnableSchedule")] + public static async Task EnableSchedule( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = req.Query["name"]; + + if (string.IsNullOrEmpty(scheduleName)) + { + return new BadRequestObjectResult("Schedule name is required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + await client.SignalEntityAsync(schedulerId, "EnableSchedule"); + + return new OkObjectResult($"Schedule '{scheduleName}' has been enabled."); + } + + [FunctionName("DisableSchedule")] + public static async Task DisableSchedule( + [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, + [DurableClient] IDurableEntityClient client) + { + string scheduleName = req.Query["name"]; + + if (string.IsNullOrEmpty(scheduleName)) + { + return new BadRequestObjectResult("Schedule name is required"); + } + + var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); + await client.SignalEntityAsync(schedulerId, "DisableSchedule"); + + return new OkObjectResult($"Schedule '{scheduleName}' has been disabled."); + } +} diff --git a/schedulebasedonentity/make_req.http b/schedulebasedonentity/make_req.http new file mode 100644 index 00000000..2c0d1c34 --- /dev/null +++ b/schedulebasedonentity/make_req.http @@ -0,0 +1,20 @@ +# Create a schedule +POST /api/CreateSchedule?cron=0 0 * * *&orchestration=MyOrchestration +Content-Type: text/plain + +MySchedule + +# Update a schedule +PUT /api/UpdateSchedule?cron=0 0 * * *&orchestration=MyNewOrchestration +Content-Type: text/plain + +MySchedule + +# Get schedule details +GET /api/GetSchedule?name=MySchedule + +# Enable schedule +POST /api/EnableSchedule?name=MySchedule + +# Disable schedule +POST /api/DisableSchedule?name=MySchedule \ No newline at end of file diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs new file mode 100644 index 00000000..42d6c385 --- /dev/null +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -0,0 +1,175 @@ +using DurableTask.Core.Entities; +using DurableTask.Core.Entities.OperationFormat; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +public class ScheduleEntity : TaskEntity +{ + private readonly ILogger logger; + private ScheduleState currentState; + private int runScheduleCount = 0; // Track how many times RunSchedule has been called + + public ScheduleEntity(ILogger logger) + { + this.logger = logger; + this.currentState = ScheduleState.Provisioning; // Initial state + } + + /// + /// Creates a new schedule. + /// + public async Task CreateSchedule(ScheduleCreationDetails details) + { + if (this.currentState != ScheduleState.Provisioning) + { + throw new InvalidOperationException("Schedule is already created."); + } + + // Validate input + if (details == null) + { + throw new ArgumentNullException(nameof(details)); + } + + // Simulate schedule creation (e.g., save to database) + logger.LogInformation($"Creating schedule with details: {details}"); + + // Transition to Active state + this.currentState = ScheduleState.Active; + + // Call RunSchedule at the end of CreateSchedule + await RunSchedule(); + } + + /// + /// Updates an existing schedule. + /// + public async Task UpdateSchedule(ScheduleUpdateDetails details) + { + if (this.currentState == ScheduleState.Provisioning) + { + throw new InvalidOperationException("Cannot update a schedule that is still provisioning."); + } + + if (this.currentState != ScheduleState.Active) + { + throw new InvalidOperationException("Schedule must be in Active state to update."); + } + + // Validate input + if (details == null) + { + throw new ArgumentNullException(nameof(details)); + } + + // Transition to Updating state + this.currentState = ScheduleState.Updating; + + try + { + // Simulate schedule update (e.g., save to database) + logger.LogInformation($"Updating schedule with details: {details}"); + + // Transition back to Active state + this.currentState = ScheduleState.Active; + } + catch (Exception ex) + { + // Transition to Failed state if update fails + this.currentState = ScheduleState.Failed; + logger.LogError(ex, "Failed to update schedule."); + throw; + } + } + + /// + /// Pauses the schedule. + /// + public void PauseSchedule() + { + if (this.currentState != ScheduleState.Active) + { + throw new InvalidOperationException("Schedule must be in Active state to pause."); + } + + // Transition to Paused state + this.currentState = ScheduleState.Paused; + logger.LogInformation("Schedule paused."); + } + + /// + /// Resumes the schedule. + /// + public void ResumeSchedule() + { + if (this.currentState != ScheduleState.Paused) + { + throw new InvalidOperationException("Schedule must be in Paused state to resume."); + } + + // Transition to Active state + this.currentState = ScheduleState.Active; + logger.LogInformation("Schedule resumed."); + } + + /// + /// Deletes the schedule. + /// + public void DeleteSchedule() + { + // Simulate schedule deletion (e.g., remove from database) + logger.LogInformation("Schedule deleted."); + + // Reset state + this.currentState = ScheduleState.Provisioning; + this.runScheduleCount = 0; + } + + /// + /// Runs the schedule. + /// + private async Task RunSchedule() + { + if (this.runScheduleCount > 0) + { + throw new InvalidOperationException("RunSchedule can only be called once."); + } + + if (this.currentState != ScheduleState.Active) + { + throw new InvalidOperationException("Schedule must be in Active state to run."); + } + + // Simulate schedule execution + logger.LogInformation("Running schedule."); + this.runScheduleCount++; + + // Simulate a long-running task + await Task.Delay(1000); // Replace with actual schedule logic + } + + /// + /// Gets the current state of the schedule. + /// + public ScheduleState GetCurrentState() + { + return this.currentState; + } +} + + +public class ScheduleCreationDetails +{ + public string Name { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string CronExpression { get; set; } +} + +public class ScheduleUpdateDetails +{ + public DateTime? NewStartTime { get; set; } + public DateTime? NewEndTime { get; set; } + public string NewCronExpression { get; set; } +} \ No newline at end of file diff --git a/src/Abstractions/Entities/Schedule/ScheduleState.cs b/src/Abstractions/Entities/Schedule/ScheduleState.cs new file mode 100644 index 00000000..2bbf8cd0 --- /dev/null +++ b/src/Abstractions/Entities/Schedule/ScheduleState.cs @@ -0,0 +1,8 @@ +internal enum ScheduleState +{ + Provisioning, // Schedule is being created + Active, // Schedule is active and running + Paused, // Schedule is paused + Failed, // Schedule has failed + Updating // Schedule is being updated +} \ No newline at end of file From 538581b8a7f60001f021a4e56d32b9844347b834 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:16:06 -0800 Subject: [PATCH 004/203] save --- .../Entities/Schedule/Schedule.cs | 285 +++++++++++------- .../Entities/Schedule/ScheduleState.cs | 8 - 2 files changed, 176 insertions(+), 117 deletions(-) delete mode 100644 src/Abstractions/Entities/Schedule/ScheduleState.cs diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index 42d6c385..40b13856 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -1,86 +1,178 @@ -using DurableTask.Core.Entities; -using DurableTask.Core.Entities.OperationFormat; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -public class ScheduleEntity : TaskEntity +namespace DurableTask.Abstractions.Entities.Schedule; + +class ScheduleState { - private readonly ILogger logger; - private ScheduleState currentState; - private int runScheduleCount = 0; // Track how many times RunSchedule has been called + internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; - public ScheduleEntity(ILogger logger) - { - this.logger = logger; - this.currentState = ScheduleState.Provisioning; // Initial state - } + internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); - /// - /// Creates a new schedule. - /// - public async Task CreateSchedule(ScheduleCreationDetails details) + internal ScheduleConfiguration? ScheduleConfiguration { get; set; } + + public void UpdateConfig(ScheduleConfiguration scheduleUpdateConfig) { - if (this.currentState != ScheduleState.Provisioning) + Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); + Check.NotNull(scheduleUpdateConfig, nameof(scheduleUpdateConfig)); + + this.ScheduleConfiguration.Version++; + + if (!string.IsNullOrEmpty(scheduleUpdateConfig.OrchestrationName)) { - throw new InvalidOperationException("Schedule is already created."); + this.ScheduleConfiguration.OrchestrationName = scheduleUpdateConfig.OrchestrationName; } - // Validate input - if (details == null) + if (!string.IsNullOrEmpty(scheduleUpdateConfig.ScheduleId)) { - throw new ArgumentNullException(nameof(details)); + this.ScheduleConfiguration.ScheduleId = scheduleUpdateConfig.ScheduleId; } - // Simulate schedule creation (e.g., save to database) - logger.LogInformation($"Creating schedule with details: {details}"); + if (scheduleUpdateConfig.OrchestrationInput == null) + { + this.ScheduleConfiguration.OrchestrationInput = scheduleUpdateConfig.OrchestrationInput; + } - // Transition to Active state - this.currentState = ScheduleState.Active; + if (scheduleUpdateConfig.StartAt.HasValue) + { + this.ScheduleConfiguration.StartAt = scheduleUpdateConfig.StartAt; + } - // Call RunSchedule at the end of CreateSchedule - await RunSchedule(); - } + if (scheduleUpdateConfig.EndAt.HasValue) + { + this.ScheduleConfiguration.EndAt = scheduleUpdateConfig.EndAt; + } - /// - /// Updates an existing schedule. - /// - public async Task UpdateSchedule(ScheduleUpdateDetails details) - { - if (this.currentState == ScheduleState.Provisioning) + if (scheduleUpdateConfig.Interval.HasValue) { - throw new InvalidOperationException("Cannot update a schedule that is still provisioning."); + this.ScheduleConfiguration.Interval = scheduleUpdateConfig.Interval; } - if (this.currentState != ScheduleState.Active) + if (!string.IsNullOrEmpty(scheduleUpdateConfig.CronExpression)) { - throw new InvalidOperationException("Schedule must be in Active state to update."); + this.ScheduleConfiguration.CronExpression = scheduleUpdateConfig.CronExpression; } - // Validate input - if (details == null) + if (scheduleUpdateConfig.MaxOccurrence != 0) { - throw new ArgumentNullException(nameof(details)); + this.ScheduleConfiguration.MaxOccurrence = scheduleUpdateConfig.MaxOccurrence; } - // Transition to Updating state - this.currentState = ScheduleState.Updating; + // Only update if the customer explicitly set a value + if (scheduleUpdateConfig.StartImmediatelyIfLate.HasValue) + { + this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleUpdateConfig.StartImmediatelyIfLate.Value; + } + } + + public void RefreshScheduleRunExecutionToken() + { + this.ExecutionToken = Guid.NewGuid().ToString("N"); + } +} + +class ScheduleConfiguration +{ + public ScheduleConfiguration(string orchestrationName, string scheduleId) + { + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + this.Version++; + } - try + string orchestrationName; + + public string OrchestrationName + { + get => this.orchestrationName; + set { - // Simulate schedule update (e.g., save to database) - logger.LogInformation($"Updating schedule with details: {details}"); + this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + string scheduleId; - // Transition back to Active state - this.currentState = ScheduleState.Active; + public string ScheduleId + { + get => this.scheduleId; + set + { + this.scheduleId = Check.NotNullOrEmpty(value, nameof(value)); } - catch (Exception ex) + } + + public string? OrchestrationInput { get; set; } + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + public TimeSpan? Interval { get; set; } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } + + internal int Version { get; set; } // Tracking schedule config version +} + +enum ScheduleStatus +{ + Uninitialized, // Schedule has not been created + Active, // Schedule is active and running + Paused, // Schedule is paused + Failed, // Schedule has failed +} + +class Schedule : TaskEntity +{ + readonly ILogger logger; + + public Schedule(ILogger logger) + { + this.logger = logger; + } + + public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration scheduleCreationConfig) + { + Verify.NotNull(scheduleCreationConfig, nameof(scheduleCreationConfig)); + + if (this.State.Status != ScheduleStatus.Uninitialized) { - // Transition to Failed state if update fails - this.currentState = ScheduleState.Failed; - logger.LogError(ex, "Failed to update schedule."); - throw; + throw new InvalidOperationException("Schedule is already created."); } + + this.logger.LogInformation($"Creating schedule with options: {scheduleCreationConfig}"); + + this.State.ScheduleConfiguration = scheduleCreationConfig; + this.State.Status = ScheduleStatus.Active; + + // Run schedule after creation + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); + } + + /// + /// Updates an existing schedule. + /// + public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration scheduleUpdateConfig) + { + Verify.NotNull(scheduleUpdateConfig, nameof(scheduleUpdateConfig)); + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + + this.logger.LogInformation($"Updating schedule with details: {scheduleUpdateConfig}"); + + this.State.UpdateConfig(scheduleUpdateConfig); + this.State.RefreshScheduleRunExecutionToken(); + + // Run schedule after update + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); } /// @@ -88,88 +180,63 @@ public async Task UpdateSchedule(ScheduleUpdateDetails details) /// public void PauseSchedule() { - if (this.currentState != ScheduleState.Active) + if (this.State.Status != ScheduleStatus.Active) { - throw new InvalidOperationException("Schedule must be in Active state to pause."); + throw new InvalidOperationException("Schedule must be in Active status to pause."); } // Transition to Paused state - this.currentState = ScheduleState.Paused; - logger.LogInformation("Schedule paused."); + this.State.Status = ScheduleStatus.Paused; + this.State.RefreshScheduleRunExecutionToken(); + this.logger.LogInformation("Schedule paused."); } /// /// Resumes the schedule. /// - public void ResumeSchedule() + public void ResumeSchedule(TaskEntityContext context) { - if (this.currentState != ScheduleState.Paused) + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + if (this.State.Status != ScheduleStatus.Paused) { throw new InvalidOperationException("Schedule must be in Paused state to resume."); } - // Transition to Active state - this.currentState = ScheduleState.Active; - logger.LogInformation("Schedule resumed."); + this.State.Status = ScheduleStatus.Active; + this.logger.LogInformation("Schedule resumed."); + + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); } - /// - /// Deletes the schedule. - /// + // TODO: Only implement this there is any cleanup shall be performed within entity before purging the instance. public void DeleteSchedule() { - // Simulate schedule deletion (e.g., remove from database) - logger.LogInformation("Schedule deleted."); - - // Reset state - this.currentState = ScheduleState.Provisioning; - this.runScheduleCount = 0; + throw new NotImplementedException(); } - /// - /// Runs the schedule. - /// - private async Task RunSchedule() + public void RunSchedule(TaskEntityContext context, string executionToken) { - if (this.runScheduleCount > 0) + if (executionToken != this.State.ExecutionToken) { - throw new InvalidOperationException("RunSchedule can only be called once."); + // Execution token has expired, log and return + this.logger.LogInformation( + "Skipping schedule run - execution token {token} has expired", + executionToken); + return; } - if (this.currentState != ScheduleState.Active) + if (this.State.Status != ScheduleStatus.Active) { - throw new InvalidOperationException("Schedule must be in Active state to run."); + throw new InvalidOperationException("Schedule must be in Active status to run."); } - // Simulate schedule execution - logger.LogInformation("Running schedule."); - this.runScheduleCount++; - - // Simulate a long-running task - await Task.Delay(1000); // Replace with actual schedule logic - } - - /// - /// Gets the current state of the schedule. - /// - public ScheduleState GetCurrentState() - { - return this.currentState; + // TODO: Implement all schedule config properties + // if startat is null, then start immediately + // first check startat, compute gap with current time, if gap is negative, then start immediately + // if gap is positive, then wait for gap seconds and then signal runschedule with delay of gap time + // first check if there is already existing orchestration instance with same orchestration name + // if there is no existing orchestration instance, then create a new one + // if there is existing orchestration instance, then check if it is done, if it is done, then create a new one + // if there is existing orchestration instance, then check if it is not done, then skip } } - - -public class ScheduleCreationDetails -{ - public string Name { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string CronExpression { get; set; } -} - -public class ScheduleUpdateDetails -{ - public DateTime? NewStartTime { get; set; } - public DateTime? NewEndTime { get; set; } - public string NewCronExpression { get; set; } -} \ No newline at end of file diff --git a/src/Abstractions/Entities/Schedule/ScheduleState.cs b/src/Abstractions/Entities/Schedule/ScheduleState.cs deleted file mode 100644 index 2bbf8cd0..00000000 --- a/src/Abstractions/Entities/Schedule/ScheduleState.cs +++ /dev/null @@ -1,8 +0,0 @@ -internal enum ScheduleState -{ - Provisioning, // Schedule is being created - Active, // Schedule is active and running - Paused, // Schedule is paused - Failed, // Schedule has failed - Updating // Schedule is being updated -} \ No newline at end of file From a65c5e8e52717a04a516e3978541a78c56868469 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:38:50 -0800 Subject: [PATCH 005/203] save --- .../Entities/Schedule/Schedule.cs | 88 ++++++++++++++++--- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index 40b13856..160443dd 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -13,6 +13,10 @@ class ScheduleState internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); + internal DateTimeOffset? LastRunAt { get; set; } + + internal DateTimeOffset? NextRunAt { get; set; } + internal ScheduleConfiguration? ScheduleConfiguration { get; set; } public void UpdateConfig(ScheduleConfiguration scheduleUpdateConfig) @@ -73,6 +77,17 @@ public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } + + public void ResetNextRunAt() + { + this.NextRunAt = null; + } + + public void ResetScheduleRunState() + { + this.ResetNextRunAt(); + this.RefreshScheduleRunExecutionToken(); + } } class ScheduleConfiguration @@ -170,7 +185,6 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration sche this.State.UpdateConfig(scheduleUpdateConfig); this.State.RefreshScheduleRunExecutionToken(); - // Run schedule after update context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); } @@ -214,13 +228,12 @@ public void DeleteSchedule() throw new NotImplementedException(); } - public void RunSchedule(TaskEntityContext context, string executionToken) + public async void RunSchedule(TaskEntityContext context, string executionToken) { if (executionToken != this.State.ExecutionToken) { - // Execution token has expired, log and return this.logger.LogInformation( - "Skipping schedule run - execution token {token} has expired", + "Cancel schedule run - execution token {token} has expired", executionToken); return; } @@ -230,13 +243,64 @@ public void RunSchedule(TaskEntityContext context, string executionToken) throw new InvalidOperationException("Schedule must be in Active status to run."); } - // TODO: Implement all schedule config properties - // if startat is null, then start immediately - // first check startat, compute gap with current time, if gap is negative, then start immediately - // if gap is positive, then wait for gap seconds and then signal runschedule with delay of gap time - // first check if there is already existing orchestration instance with same orchestration name - // if there is no existing orchestration instance, then create a new one - // if there is existing orchestration instance, then check if it is done, if it is done, then create a new one - // if there is existing orchestration instance, then check if it is not done, then skip + // run schedule based on next run at + // if next run at is null, it is not scheduled yet, we compute the next run at based on startat and update + // else if next run at is now, schedule the orchestration, and update next run at = next runat + interval + // if next run at is in the future, signal to run schedule later + if (!this.State.NextRunAt.HasValue) + { + this.State.NextRunAt = this.State.ScheduleConfiguration.StartAt; + } + + if (this.State.NextRunAt.Value <= DateTimeOffset.UtcNow) { + await this.StartOrchestrationIfNotRunning(context); + this.State.NextRunAt = this.State.NextRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; + } + + context.SignalEntity( + new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), + "RunSchedule", + this.State.ExecutionToken, new SignalEntityOptions + { + SignalTime = this.State.NextRunAt.Value, + }); + } + + // implement a func to check startat internal func + void CheckStartAt(TaskEntityContext context) + { + ScheduleConfiguration? config = this.State.ScheduleConfiguration; + DateTime now = DateTime.UtcNow; + TimeSpan startDelay = config.StartAt.HasValue ? config.StartAt.Value - now : TimeSpan.Zero; + + if (startDelay <= TimeSpan.Zero) + { + // Start immediately if no delay or start time has passed + this.StartOrchestrationIfNotRunning(context); + } + else + { + // Schedule future run + context.SignalEntity( + new EntityInstanceId(nameof(Schedule), config.ScheduleId), + "RunSchedule", + this.State.ExecutionToken, new SignalEntityOptions + { + SignalTime = config.StartAt.Value, + }); + } + } + + void StartOrchestrationIfNotRunning(TaskEntityContext context) + { + var config = this.State.ScheduleConfiguration; + var instance = context.GetOrchestrationInstance(config.OrchestrationName); + + if (instance == null || instance.IsComplete) + { + context.StartNewOrchestration( + config.OrchestrationName, + config.OrchestrationInput); + } } } From ea5e73317d44b3638bbb44db369f0f8ec848e03b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 25 Jan 2025 19:16:43 -0800 Subject: [PATCH 006/203] save --- .../Entities/Schedule/Schedule.cs | 88 ++++++++++++++----- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index 160443dd..a1b361ca 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -14,80 +14,84 @@ class ScheduleState internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); internal DateTimeOffset? LastRunAt { get; set; } - internal DateTimeOffset? NextRunAt { get; set; } internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - public void UpdateConfig(ScheduleConfiguration scheduleUpdateConfig) + public HashSet UpdateConfig(ScheduleConfiguration scheduleUpdateConfig) { Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); Check.NotNull(scheduleUpdateConfig, nameof(scheduleUpdateConfig)); + HashSet updatedFields = new HashSet(); + this.ScheduleConfiguration.Version++; if (!string.IsNullOrEmpty(scheduleUpdateConfig.OrchestrationName)) { this.ScheduleConfiguration.OrchestrationName = scheduleUpdateConfig.OrchestrationName; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); } if (!string.IsNullOrEmpty(scheduleUpdateConfig.ScheduleId)) { this.ScheduleConfiguration.ScheduleId = scheduleUpdateConfig.ScheduleId; + updatedFields.Add(nameof(this.ScheduleConfiguration.ScheduleId)); } if (scheduleUpdateConfig.OrchestrationInput == null) { this.ScheduleConfiguration.OrchestrationInput = scheduleUpdateConfig.OrchestrationInput; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); } if (scheduleUpdateConfig.StartAt.HasValue) { this.ScheduleConfiguration.StartAt = scheduleUpdateConfig.StartAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); } if (scheduleUpdateConfig.EndAt.HasValue) { this.ScheduleConfiguration.EndAt = scheduleUpdateConfig.EndAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); } if (scheduleUpdateConfig.Interval.HasValue) { this.ScheduleConfiguration.Interval = scheduleUpdateConfig.Interval; + updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); } if (!string.IsNullOrEmpty(scheduleUpdateConfig.CronExpression)) { this.ScheduleConfiguration.CronExpression = scheduleUpdateConfig.CronExpression; + updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); } if (scheduleUpdateConfig.MaxOccurrence != 0) { this.ScheduleConfiguration.MaxOccurrence = scheduleUpdateConfig.MaxOccurrence; + updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); } // Only update if the customer explicitly set a value if (scheduleUpdateConfig.StartImmediatelyIfLate.HasValue) { this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleUpdateConfig.StartImmediatelyIfLate.Value; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); } + + return updatedFields; } + // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully + // This could incur little overhead as ideally the runSchedule with old token should be killed immediately + // but there is no support to cancel pending entity operations currently, can be a todo item public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } - - public void ResetNextRunAt() - { - this.NextRunAt = null; - } - - public void ResetScheduleRunState() - { - this.ResetNextRunAt(); - this.RefreshScheduleRunExecutionToken(); - } } class ScheduleConfiguration @@ -169,8 +173,9 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration sche this.State.ScheduleConfiguration = scheduleCreationConfig; this.State.Status = ScheduleStatus.Active; - // Run schedule after creation - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); + // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately + // or later to separate response from schedule creation and schedule responsibilities + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); } /// @@ -183,10 +188,34 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration sche this.logger.LogInformation($"Updating schedule with details: {scheduleUpdateConfig}"); - this.State.UpdateConfig(scheduleUpdateConfig); + HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleUpdateConfig); + if (updatedScheduleConfigFields.Count == 0) + { + // no need to interrupt and update current schedule run as there is no change in the schedule config + this.logger.LogInformation("Schedule configuration is up to date."); + return; + } + + // after schedule config is updated, perform post-config-update logic separately + foreach (string updatedScheduleConfigField in updatedScheduleConfigFields) + { + switch (updatedScheduleConfigField) + { + case nameof(this.State.ScheduleConfiguration.StartAt): + case nameof(this.State.ScheduleConfiguration.Interval): + this.State.NextRunAt = null; + break; + // TODO: add other fields's callback logic after config update if any + default: + break; + } + } + this.State.RefreshScheduleRunExecutionToken(); - // Run schedule after update - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); + + // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately + // or later to separate response from schedule creation and schedule responsibilities + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); } /// @@ -201,6 +230,7 @@ public void PauseSchedule() // Transition to Paused state this.State.Status = ScheduleStatus.Paused; + this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); this.logger.LogInformation("Schedule paused."); } @@ -217,9 +247,10 @@ public void ResumeSchedule(TaskEntityContext context) } this.State.Status = ScheduleStatus.Active; + this.State.NextRunAt = null; this.logger.LogInformation("Schedule resumed."); - - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), "RunSchedule", this.State.ExecutionToken); + // compute next run based on startat and interval + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); } // TODO: Only implement this there is any cleanup shall be performed within entity before purging the instance. @@ -244,12 +275,21 @@ public async void RunSchedule(TaskEntityContext context, string executionToken) } // run schedule based on next run at - // if next run at is null, it is not scheduled yet, we compute the next run at based on startat and update - // else if next run at is now, schedule the orchestration, and update next run at = next runat + interval - // if next run at is in the future, signal to run schedule later + // need to enforce the constraint here NextRunAt truly represents the next run at + // if next run at is null, this means schedule is changed, we compute the next run at based on startat and update + // else if next run at is set, then we run at next run at if (!this.State.NextRunAt.HasValue) { - this.State.NextRunAt = this.State.ScheduleConfiguration.StartAt; + // check whats last run at time, if not set, meaning it has not run once, we run at startat + // else, it has run before, we cant run at startat, need to compute next run at based on last run at + num of intervals between last runtime and now plus 1 + if (!this.State.LastRunAt.HasValue) + { + this.State.NextRunAt = this.State.ScheduleConfiguration.StartAt; + } + else + { + this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; + } } if (this.State.NextRunAt.Value <= DateTimeOffset.UtcNow) { From 4be057e3ef965aaf9f47b7b24e116e604c38c8a8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 25 Jan 2025 20:47:29 -0800 Subject: [PATCH 007/203] save --- .../Entities/Schedule/Schedule.cs | 114 ++++++++++-------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index a1b361ca..f2f01cad 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -14,6 +14,7 @@ class ScheduleState internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); internal DateTimeOffset? LastRunAt { get; set; } + internal DateTimeOffset? NextRunAt { get; set; } internal ScheduleConfiguration? ScheduleConfiguration { get; set; } @@ -127,11 +128,37 @@ public string ScheduleId public string? OrchestrationInput { get; set; } + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + public DateTimeOffset? StartAt { get; set; } public DateTimeOffset? EndAt { get; set; } - public TimeSpan? Interval { get; set; } + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } public string? CronExpression { get; set; } @@ -147,7 +174,6 @@ enum ScheduleStatus Uninitialized, // Schedule has not been created Active, // Schedule is active and running Paused, // Schedule is paused - Failed, // Schedule has failed } class Schedule : TaskEntity @@ -175,7 +201,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration sche // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } /// @@ -205,6 +231,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration sche case nameof(this.State.ScheduleConfiguration.Interval): this.State.NextRunAt = null; break; + // TODO: add other fields's callback logic after config update if any default: break; @@ -215,7 +242,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration sche // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } /// @@ -249,8 +276,9 @@ public void ResumeSchedule(TaskEntityContext context) this.State.Status = ScheduleStatus.Active; this.State.NextRunAt = null; this.logger.LogInformation("Schedule resumed."); + // compute next run based on startat and interval - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(RunSchedule), this.State.ExecutionToken); + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } // TODO: Only implement this there is any cleanup shall be performed within entity before purging the instance. @@ -259,13 +287,18 @@ public void DeleteSchedule() throw new NotImplementedException(); } - public async void RunSchedule(TaskEntityContext context, string executionToken) + // TODO: Support other schedule option properties like cron expression, max occurrence, etc. + public void RunSchedule(TaskEntityContext context, string executionToken) { + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + if (this.State.ScheduleConfiguration.Interval == null) + { + throw new ArgumentNullException(nameof(this.State.ScheduleConfiguration.Interval)); + } + if (executionToken != this.State.ExecutionToken) { - this.logger.LogInformation( - "Cancel schedule run - execution token {token} has expired", - executionToken); + this.logger.LogInformation("Cancel schedule run - execution token {token} has expired", executionToken); return; } @@ -288,59 +321,44 @@ public async void RunSchedule(TaskEntityContext context, string executionToken) } else { - this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; + // Calculate number of intervals between last run and now + TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - this.State.LastRunAt.Value; + int intervalsElapsed = (int)(timeSinceLastRun.Ticks / this.State.ScheduleConfiguration.Interval.Value.Ticks); + + // Compute the next run time + this.State.NextRunAt = this.State.LastRunAt.Value + TimeSpan.FromTicks(this.State.ScheduleConfiguration.Interval.Value.Ticks * (intervalsElapsed + 1)); } } - if (this.State.NextRunAt.Value <= DateTimeOffset.UtcNow) { - await this.StartOrchestrationIfNotRunning(context); - this.State.NextRunAt = this.State.NextRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; + DateTimeOffset currentTime = DateTimeOffset.UtcNow; + + if (!this.State.NextRunAt.HasValue || this.State.NextRunAt!.Value <= currentTime) + { + this.State.NextRunAt = currentTime; + this.StartOrchestrationIfNotRunning(context); + this.State.LastRunAt = this.State.NextRunAt; + this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; } context.SignalEntity( - new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), - "RunSchedule", - this.State.ExecutionToken, new SignalEntityOptions - { - SignalTime = this.State.NextRunAt.Value, - }); + new EntityInstanceId( + nameof(Schedule), + this.State.ScheduleConfiguration.ScheduleId), + nameof(this.RunSchedule), + this.State.ExecutionToken, new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); } - // implement a func to check startat internal func - void CheckStartAt(TaskEntityContext context) + void StartOrchestrationIfNotRunning(TaskEntityContext context) { ScheduleConfiguration? config = this.State.ScheduleConfiguration; - DateTime now = DateTime.UtcNow; - TimeSpan startDelay = config.StartAt.HasValue ? config.StartAt.Value - now : TimeSpan.Zero; - - if (startDelay <= TimeSpan.Zero) - { - // Start immediately if no delay or start time has passed - this.StartOrchestrationIfNotRunning(context); - } - else - { - // Schedule future run - context.SignalEntity( - new EntityInstanceId(nameof(Schedule), config.ScheduleId), - "RunSchedule", - this.State.ExecutionToken, new SignalEntityOptions - { - SignalTime = config.StartAt.Value, - }); - } + context.ScheduleNewOrchestration(new TaskName(config!.OrchestrationName), config!.OrchestrationInput, new StartOrchestrationOptions(config!.OrchestrationInstanceId)); } - void StartOrchestrationIfNotRunning(TaskEntityContext context) + void ValidateStateTransition(ScheduleStatus from, ScheduleStatus to) { - var config = this.State.ScheduleConfiguration; - var instance = context.GetOrchestrationInstance(config.OrchestrationName); - - if (instance == null || instance.IsComplete) + if (this.State.Status != from) { - context.StartNewOrchestration( - config.OrchestrationName, - config.OrchestrationInput); + throw new InvalidOperationException($"Cannot transition from {this.State.Status} to {to}"); } } } From 92923bff252bd54b7b30b1cc8c914d808f857433 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:11:45 -0800 Subject: [PATCH 008/203] save --- .../Entities/Schedule/Schedule.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index f2f01cad..8dc7ac4a 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -180,6 +180,23 @@ class Schedule : TaskEntity { readonly ILogger logger; + // Valid schedule status transitions + readonly Dictionary> validTransitions = new Dictionary> + { + { + ScheduleStatus.Uninitialized, + new HashSet { ScheduleStatus.Active } + }, + { + ScheduleStatus.Active, + new HashSet { ScheduleStatus.Paused } + }, + { + ScheduleStatus.Paused, + new HashSet { ScheduleStatus.Active } + }, + }; + public Schedule(ILogger logger) { this.logger = logger; @@ -197,7 +214,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration sche this.logger.LogInformation($"Creating schedule with options: {scheduleCreationConfig}"); this.State.ScheduleConfiguration = scheduleCreationConfig; - this.State.Status = ScheduleStatus.Active; + this.TryStatusTransition(ScheduleStatus.Active); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities @@ -256,9 +273,10 @@ public void PauseSchedule() } // Transition to Paused state - this.State.Status = ScheduleStatus.Paused; + this.TryStatusTransition(ScheduleStatus.Paused); this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); + this.logger.LogInformation("Schedule paused."); } @@ -273,7 +291,7 @@ public void ResumeSchedule(TaskEntityContext context) throw new InvalidOperationException("Schedule must be in Paused state to resume."); } - this.State.Status = ScheduleStatus.Active; + this.TryStatusTransition(ScheduleStatus.Active); this.State.NextRunAt = null; this.logger.LogInformation("Schedule resumed."); @@ -354,11 +372,17 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) context.ScheduleNewOrchestration(new TaskName(config!.OrchestrationName), config!.OrchestrationInput, new StartOrchestrationOptions(config!.OrchestrationInstanceId)); } - void ValidateStateTransition(ScheduleStatus from, ScheduleStatus to) + void TryStatusTransition(ScheduleStatus to) { - if (this.State.Status != from) + // Check if transition is valid + HashSet validTargetStates; + ScheduleStatus from = this.State.Status; + + if (!this.validTransitions.TryGetValue(from, out validTargetStates) || !validTargetStates.Contains(to)) { - throw new InvalidOperationException($"Cannot transition from {this.State.Status} to {to}"); + throw new InvalidOperationException($"Invalid state transition: Cannot transition from {from} to {to}"); } + + this.State.Status = to; } } From 29235d77a1151d20f355eebad3ab12b197a8ae9c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:43:30 -0800 Subject: [PATCH 009/203] save --- samples/ScheduleDemo/Program.cs | 131 ++++++++++++++++++ samples/ScheduleDemo/ScheduleConfiguration.cs | 73 ++++++++++ samples/ScheduleDemo/ScheduleDemo.csproj | 20 +++ 3 files changed, 224 insertions(+) create mode 100644 samples/ScheduleDemo/Program.cs create mode 100644 samples/ScheduleDemo/ScheduleConfiguration.cs create mode 100644 samples/ScheduleDemo/ScheduleDemo.csproj diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs new file mode 100644 index 00000000..18e38f7b --- /dev/null +++ b/samples/ScheduleDemo/Program.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.DurableTask.Client.AzureManaged; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using DurableTask.Abstractions.Entities.Schedule; + +internal class Program +{ + private static async Task Main(string[] args) + { + // Create the host builder + IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + + // Configure the worker + services.AddDurableTaskWorker(builder => + { + // Add the Schedule entity and demo orchestration + builder.AddTasks(r => + { + r.AddAllGeneratedTasks(); + r.AddEntity(); + + // Add a demo orchestration that will be triggered by the schedule + r.AddOrchestratorFunc("DemoOrchestration", async TaskOrchestrationContext context => + { + string input = context.GetInput(); + await context.CallActivityAsync("ProcessMessage", input); + return $"Completed processing at {DateTime.UtcNow}"; + }); + + // Add a demo activity + r.AddActivityFunc("ProcessMessage", (TaskActivityContext context, string message) => + { + context.GetLogger().LogInformation($"Processing scheduled message: {message}"); + return Task.CompletedTask; + }); + }); + builder.UseDurableTaskScheduler(connectionString); + }); + + // Configure the client + services.AddDurableTaskClient(builder => + { + builder.UseDurableTaskScheduler(connectionString); + }); + + // Configure console logging + services.AddLogging(logging => + { + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); + }); + }) + .Build(); + + await host.StartAsync(); + await using DurableTaskClient client = host.Services.GetRequiredService(); + + try + { + // Create a schedule that runs every 30 seconds + ScheduleConfiguration scheduleConfig = new ScheduleConfiguration( + orchestrationName: "DemoOrchestration", + scheduleId: "demo-schedule") + { + Interval = TimeSpan.FromSeconds(30), + StartAt = DateTimeOffset.UtcNow, + OrchestrationInput = "This is a scheduled message!" + }; + + // Create the schedule + Console.WriteLine("Creating schedule..."); + await client.CreateScheduleAsync(scheduleConfig); + Console.WriteLine($"Created schedule with ID: {scheduleConfig.ScheduleId}"); + + // Monitor the schedule for a while + Console.WriteLine("\nMonitoring schedule for 2 minutes..."); + for (int i = 0; i < 4; i++) + { + await Task.Delay(TimeSpan.FromSeconds(30)); + Schedule schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + Console.WriteLine($"\nSchedule status: {schedule.Status}"); + Console.WriteLine($"Last run at: {schedule.LastRunAt}"); + Console.WriteLine($"Next run at: {schedule.NextRunAt}"); + } + + // Pause the schedule + Console.WriteLine("\nPausing schedule..."); + await client.PauseScheduleAsync(scheduleConfig.ScheduleId); + + Schedule pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); + + // Resume the schedule + Console.WriteLine("\nResuming schedule..."); + await client.ResumeScheduleAsync(scheduleConfig.ScheduleId); + + Schedule resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); + Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); + + Console.WriteLine("\nPress any key to delete the schedule and exit..."); + Console.ReadKey(); + + // Delete the schedule + await client.DeleteScheduleAsync(scheduleConfig.ScheduleId); + Console.WriteLine("Schedule deleted."); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + + await host.StopAsync(); + } +} diff --git a/samples/ScheduleDemo/ScheduleConfiguration.cs b/samples/ScheduleDemo/ScheduleConfiguration.cs new file mode 100644 index 00000000..169dbbb9 --- /dev/null +++ b/samples/ScheduleDemo/ScheduleConfiguration.cs @@ -0,0 +1,73 @@ +class ScheduleConfiguration +{ + public ScheduleConfiguration(string orchestrationName, string scheduleId) + { + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + this.Version++; + } + + string orchestrationName; + + public string OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + string scheduleId; + + public string ScheduleId + { + get => this.scheduleId; + set + { + this.scheduleId = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } + + internal int Version { get; set; } // Tracking schedule config version +} \ No newline at end of file diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj new file mode 100644 index 00000000..10f7c9c7 --- /dev/null +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + + + + + + + + + + + + + + From e4759f2fc7fe8b9d8ece69470745bd4b370f1fc1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:47:11 -0800 Subject: [PATCH 010/203] save --- .../AzureManaged/DurableTaskSchedulerWorkerExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 2eec714c..84774990 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using DurableTask.Abstractions.Entities.Schedule; namespace Microsoft.DurableTask.Worker.AzureManaged; @@ -80,6 +81,9 @@ static void ConfigureSchedulerOptions( Action initialConfig, Action? additionalConfig) { + // Add the Schedule entity by default + builder.AddTasks(r => r.AddEntity()); + builder.Services.AddOptions(builder.Name) .Configure(initialConfig) .Configure(additionalConfig ?? (_ => { })) From c5e1be3102454e8ac7ba8c175ea3df8d4f8d2587 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:48:36 -0800 Subject: [PATCH 011/203] save --- src/Abstractions/Entities/Schedule/Schedule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index 8dc7ac4a..52432e16 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -176,7 +176,7 @@ enum ScheduleStatus Paused, // Schedule is paused } -class Schedule : TaskEntity +public class Schedule : TaskEntity { readonly ILogger logger; From e90687e8d8317109e29abdd0ab0a0ac6da47a7dc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:02:30 -0800 Subject: [PATCH 012/203] save --- Microsoft.DurableTask.sln | 7 ++++ samples/ScheduleDemo/Program.cs | 33 ++++++---------- samples/ScheduleDemo/ScheduleConfiguration.cs | 27 +++---------- samples/ScheduleDemo/ScheduleDemo.csproj | 6 ++- .../Entities/Schedule/Schedule.cs | 4 +- src/Client/Core/DurableTaskClient.cs | 32 +++++++++++++++ src/Client/Grpc/GrpcDurableTaskClient.cs | 39 +++++++++++++++++++ .../DurableTaskSchedulerWorkerExtensions.cs | 2 +- .../AzureManaged/Worker.AzureManaged.csproj | 1 - 9 files changed, 103 insertions(+), 48 deletions(-) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index a86daa23..412ba062 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -83,6 +83,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CECADD EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{3272C041-F81D-4C85-A4FB-2A700B5A7A9D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleDemo", "samples\ScheduleDemo\ScheduleDemo.csproj", "{FF37BC53-8EC1-4673-915B-E59B38E286DF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -217,6 +219,10 @@ Global {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.Build.0 = Release|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -258,6 +264,7 @@ Global {1E5C2E83-7B6B-425A-9C9B-0B887D273B12} = {51DC98A3-0193-4C66-964B-C26C748E25B6} {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {3272C041-F81D-4C85-A4FB-2A700B5A7A9D} = {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} + {FF37BC53-8EC1-4673-915B-E59B38E286DF} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 18e38f7b..e5c210f3 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -1,19 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask; using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.AzureManaged; using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.AzureManaged; -using Microsoft.DurableTask.Client.AzureManaged; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using DurableTask.Abstractions.Entities.Schedule; -internal class Program +class Program { - private static async Task Main(string[] args) + static async Task Main(string[] args) { // Create the host builder IHost host = Host.CreateDefaultBuilder(args) @@ -26,26 +24,19 @@ private static async Task Main(string[] args) services.AddDurableTaskWorker(builder => { // Add the Schedule entity and demo orchestration - builder.AddTasks(r => + _ = builder.AddTasks(r => { - r.AddAllGeneratedTasks(); - r.AddEntity(); - // Add a demo orchestration that will be triggered by the schedule - r.AddOrchestratorFunc("DemoOrchestration", async TaskOrchestrationContext context => + r.AddOrchestratorFunc("DemoOrchestration", async context => { string input = context.GetInput(); await context.CallActivityAsync("ProcessMessage", input); return $"Completed processing at {DateTime.UtcNow}"; }); - // Add a demo activity - r.AddActivityFunc("ProcessMessage", (TaskActivityContext context, string message) => - { - context.GetLogger().LogInformation($"Processing scheduled message: {message}"); - return Task.CompletedTask; - }); + r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); }); + builder.UseDurableTaskScheduler(connectionString); }); @@ -93,7 +84,7 @@ private static async Task Main(string[] args) for (int i = 0; i < 4; i++) { await Task.Delay(TimeSpan.FromSeconds(30)); - Schedule schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + var schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); Console.WriteLine($"\nSchedule status: {schedule.Status}"); Console.WriteLine($"Last run at: {schedule.LastRunAt}"); Console.WriteLine($"Next run at: {schedule.NextRunAt}"); @@ -102,15 +93,15 @@ private static async Task Main(string[] args) // Pause the schedule Console.WriteLine("\nPausing schedule..."); await client.PauseScheduleAsync(scheduleConfig.ScheduleId); - - Schedule pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + + var pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); // Resume the schedule Console.WriteLine("\nResuming schedule..."); await client.ResumeScheduleAsync(scheduleConfig.ScheduleId); - - Schedule resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); + + var resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); diff --git a/samples/ScheduleDemo/ScheduleConfiguration.cs b/samples/ScheduleDemo/ScheduleConfiguration.cs index 169dbbb9..a39c93e7 100644 --- a/samples/ScheduleDemo/ScheduleConfiguration.cs +++ b/samples/ScheduleDemo/ScheduleConfiguration.cs @@ -1,33 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + class ScheduleConfiguration { public ScheduleConfiguration(string orchestrationName, string scheduleId) { - this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.OrchestrationName = orchestrationName; this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); this.Version++; } - string orchestrationName; - - public string OrchestrationName - { - get => this.orchestrationName; - set - { - this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } - } + public string OrchestrationName { get; set; } - string scheduleId; - - public string ScheduleId - { - get => this.scheduleId; - set - { - this.scheduleId = Check.NotNullOrEmpty(value, nameof(value)); - } - } + public string ScheduleId { get; set; } public string? OrchestrationInput { get; set; } diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index 10f7c9c7..f4a1fbb0 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -8,8 +8,6 @@ - - @@ -17,4 +15,8 @@ + + + + diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/Abstractions/Entities/Schedule/Schedule.cs index 52432e16..01902ba7 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/Abstractions/Entities/Schedule/Schedule.cs @@ -7,7 +7,7 @@ namespace DurableTask.Abstractions.Entities.Schedule; -class ScheduleState +public class ScheduleState { internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; @@ -95,7 +95,7 @@ public void RefreshScheduleRunExecutionToken() } } -class ScheduleConfiguration +public class ScheduleConfiguration { public ScheduleConfiguration(string orchestrationName, string scheduleId) { diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index 16d6d842..cd4c6ea1 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.ComponentModel; +using DurableTask.Abstractions.Entities.Schedule; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Internal; @@ -408,4 +409,35 @@ public virtual Task PurgeAllInstancesAsync( /// /// A that completes when the disposal completes. public abstract ValueTask DisposeAsync(); + + public virtual Task GetScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + public virtual Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + public virtual Task DeleteScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + public virtual Task PauseScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + public virtual Task ResumeScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + // update schedule + public virtual Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index f0d6e6e8..faecf251 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text; +using DurableTask.Abstractions.Entities.Schedule; using Google.Protobuf.WellKnownTypes; using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.DependencyInjection; @@ -358,6 +359,44 @@ public override Task PurgeAllInstancesAsync( return this.PurgeInstancesCoreAsync(request, cancellation); } + // SCHEDULE SUPPORT + + /// + public override Task GetScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + /// + public override Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + /// + public override Task DeleteScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + /// + public override Task PauseScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + /// + public override Task ResumeScheduleAsync(string scheduleId) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + + /// + public override Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) + { + throw new NotSupportedException($"{this.GetType()} does not support schedules."); + } + static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker) { if (options.Channel is GrpcChannel c) diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 84774990..4f6d34e0 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -82,7 +82,7 @@ static void ConfigureSchedulerOptions( Action? additionalConfig) { // Add the Schedule entity by default - builder.AddTasks(r => r.AddEntity()); + builder.AddTasks(r => r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp))); builder.Services.AddOptions(builder.Name) .Configure(initialConfig) diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index 9f3b877d..159f5bcd 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -10,7 +10,6 @@ - From 2ee50e1c2b2082f0a6b6dd8bdcdc426963192d9a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 26 Jan 2025 02:04:56 -0800 Subject: [PATCH 013/203] save --- schedulebasedonentity/GenerateDailyReport.cs | 17 --- schedulebasedonentity/ScheduleEntity.cs | 130 ------------------- schedulebasedonentity/SchedulerEndpoints.cs | 125 ------------------ schedulebasedonentity/make_req.http | 20 --- 4 files changed, 292 deletions(-) delete mode 100644 schedulebasedonentity/GenerateDailyReport.cs delete mode 100644 schedulebasedonentity/ScheduleEntity.cs delete mode 100644 schedulebasedonentity/SchedulerEndpoints.cs delete mode 100644 schedulebasedonentity/make_req.http diff --git a/schedulebasedonentity/GenerateDailyReport.cs b/schedulebasedonentity/GenerateDailyReport.cs deleted file mode 100644 index 2ff79019..00000000 --- a/schedulebasedonentity/GenerateDailyReport.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; - -public static class GenerateDailyReport -{ - [FunctionName("GenerateDailyReport")] - public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context) - { - string reportDate = DateTime.UtcNow.ToString("yyyy-MM-dd"); - Console.WriteLine($"Generating daily financial report for {reportDate}"); - - // Simulate work - await Task.Delay(1000); - } -} diff --git a/schedulebasedonentity/ScheduleEntity.cs b/schedulebasedonentity/ScheduleEntity.cs deleted file mode 100644 index 64da0e27..00000000 --- a/schedulebasedonentity/ScheduleEntity.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Cronos; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Newtonsoft.Json; - -[JsonObject(MemberSerialization.OptIn)] -public class SchedulerEntity -{ - [JsonProperty("schedule")] - private ScheduleMetadata? schedule; - - public void CreateSchedule(string scheduleName, string cronExpression, string orchestrationName) - { - if (this.schedule != null) - { - throw new InvalidOperationException("Schedule already exists."); - } - - this.schedule = new ScheduleMetadata - { - Name = scheduleName, - CronExpression = cronExpression, - OrchestrationName = orchestrationName, - NextRun = DateTime.UtcNow, // Set next run to now to run immediately - IsEnabled = true - }; - - // Signal the entity to run the schedule immediately - ctx.SignalEntity(ctx.EntityId, "RunSchedule"); - } - - public void UpdateSchedule(string scheduleName, string cronExpression, string orchestrationName) - { - if (this.schedule == null) - { - throw new InvalidOperationException("Schedule does not exist."); - } - - this.schedule.Name = scheduleName; - this.schedule.CronExpression = cronExpression; - this.schedule.OrchestrationName = orchestrationName; - this.schedule.NextRun = DateTime.UtcNow; // Reset next run to now after update - } - - public ScheduleMetadata GetSchedule() - { - if (this.schedule == null) - { - throw new InvalidOperationException("Schedule does not exist."); - } - - return this.schedule; - } - - public void EnableSchedule() - { - if (this.schedule == null) - { - throw new InvalidOperationException("Schedule does not exist."); - } - - this.schedule.IsEnabled = true; - this.schedule.NextRun = DateTime.UtcNow; // Start immediately when enabled - ctx.SignalEntity(ctx.EntityId, "RunSchedule"); - } - - public void DisableSchedule() - { - if (this.schedule == null) - { - throw new InvalidOperationException("Schedule does not exist."); - } - - this.schedule.IsEnabled = false; - } - - async Task TriggerOrchestration(IDurableEntityContext ctx) - { - string instanceId = await ctx.CallOrchestratorAsync("GenerateDailyReport"); - ctx.SetState(this); // Save entity state - } - - public async Task RunSchedule(IDurableEntityContext ctx) - { - if (schedule == null) - { - throw new InvalidOperationException("Schedule not created."); - } - - if (!schedule.IsEnabled) - { - return; // Don't run if schedule is disabled - } - - // Wait until the next scheduled time - TimeSpan delay = schedule.NextRun - DateTime.UtcNow; - if (delay > TimeSpan.Zero) - { - await ctx.CreateTimer(schedule.NextRun, CancellationToken.None); - } - - // Trigger the target orchestration - await TriggerOrchestration(ctx); - - // Update the next run time - schedule.NextRun = CronExpressionParser.GetNextOccurrence(schedule.CronExpression, DateTime.UtcNow); - - // Reschedule by signaling itself - ctx.SignalEntity(ctx.EntityId, "RunSchedule"); - } - - [FunctionName(nameof(SchedulerEntity))] - public static Task Run([EntityTrigger] IDurableEntityContext context) - { - return context.DispatchAsync(); - } -} - -public class ScheduleMetadata -{ - public string Name { get; set; } = null!; - public string CronExpression { get; set; } = null!; - public string OrchestrationName { get; set; } = null!; - public DateTime NextRun { get; set; } - public bool IsEnabled { get; set; } -} diff --git a/schedulebasedonentity/SchedulerEndpoints.cs b/schedulebasedonentity/SchedulerEndpoints.cs deleted file mode 100644 index adbdcec9..00000000 --- a/schedulebasedonentity/SchedulerEndpoints.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.DurableTask.Client.Entities; -using System.IO; -using System; - -public static class SchedulerEndpoints -{ - [FunctionName("CreateSchedule")] - public static async Task CreateSchedule( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); - string cronExpression = req.Query["cron"]; - string orchestrationName = req.Query["orchestration"]; - - if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) - { - return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - - // Create the schedule - await client.SignalEntityAsync(schedulerId, "CreateSchedule", new - { - ScheduleName = scheduleName, - CronExpression = cronExpression, - OrchestrationName = orchestrationName - }); - - return new OkObjectResult($"Schedule '{scheduleName}' has been created."); - } - - [FunctionName("UpdateSchedule")] - public static async Task UpdateSchedule( - [HttpTrigger(AuthorizationLevel.Function, "put")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = await new StreamReader(req.Body).ReadToEndAsync(); - string cronExpression = req.Query["cron"]; - string orchestrationName = req.Query["orchestration"]; - - if (string.IsNullOrEmpty(scheduleName) || string.IsNullOrEmpty(cronExpression) || string.IsNullOrEmpty(orchestrationName)) - { - return new BadRequestObjectResult("Schedule name, cron expression and orchestration name are required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - - // Update the schedule - await client.SignalEntityAsync(schedulerId, "UpdateSchedule", new - { - ScheduleName = scheduleName, - CronExpression = cronExpression, - OrchestrationName = orchestrationName - }); - - return new OkObjectResult($"Schedule '{scheduleName}' has been updated."); - } - - [FunctionName("GetSchedule")] - public static async Task GetSchedule( - [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = req.Query["name"]; - - if (string.IsNullOrEmpty(scheduleName)) - { - return new BadRequestObjectResult("Schedule name is required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - var state = await client.ReadEntityStateAsync(schedulerId); - - if (!state.EntityExists) - { - return new NotFoundObjectResult($"Schedule '{scheduleName}' not found."); - } - - var schedule = state.EntityState.GetSchedule(); - return new OkObjectResult(schedule); - } - - [FunctionName("EnableSchedule")] - public static async Task EnableSchedule( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = req.Query["name"]; - - if (string.IsNullOrEmpty(scheduleName)) - { - return new BadRequestObjectResult("Schedule name is required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - await client.SignalEntityAsync(schedulerId, "EnableSchedule"); - - return new OkObjectResult($"Schedule '{scheduleName}' has been enabled."); - } - - [FunctionName("DisableSchedule")] - public static async Task DisableSchedule( - [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, - [DurableClient] IDurableEntityClient client) - { - string scheduleName = req.Query["name"]; - - if (string.IsNullOrEmpty(scheduleName)) - { - return new BadRequestObjectResult("Schedule name is required"); - } - - var schedulerId = new EntityInstanceId("SchedulerEntity", scheduleName); - await client.SignalEntityAsync(schedulerId, "DisableSchedule"); - - return new OkObjectResult($"Schedule '{scheduleName}' has been disabled."); - } -} diff --git a/schedulebasedonentity/make_req.http b/schedulebasedonentity/make_req.http deleted file mode 100644 index 2c0d1c34..00000000 --- a/schedulebasedonentity/make_req.http +++ /dev/null @@ -1,20 +0,0 @@ -# Create a schedule -POST /api/CreateSchedule?cron=0 0 * * *&orchestration=MyOrchestration -Content-Type: text/plain - -MySchedule - -# Update a schedule -PUT /api/UpdateSchedule?cron=0 0 * * *&orchestration=MyNewOrchestration -Content-Type: text/plain - -MySchedule - -# Get schedule details -GET /api/GetSchedule?name=MySchedule - -# Enable schedule -POST /api/EnableSchedule?name=MySchedule - -# Disable schedule -POST /api/DisableSchedule?name=MySchedule \ No newline at end of file From a1c9be8b0ea8fcd7df2fd58a19c7e5bcb1ee1d6e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:49:06 -0800 Subject: [PATCH 014/203] save --- .../Schedule => ScheduledTasks}/Schedule.cs | 164 ++++++++++-------- src/ScheduledTasks/ScheduledTasks.csproj | 10 ++ 2 files changed, 104 insertions(+), 70 deletions(-) rename src/{Abstractions/Entities/Schedule => ScheduledTasks}/Schedule.cs (74%) create mode 100644 src/ScheduledTasks/ScheduledTasks.csproj diff --git a/src/Abstractions/Entities/Schedule/Schedule.cs b/src/ScheduledTasks/Schedule.cs similarity index 74% rename from src/Abstractions/Entities/Schedule/Schedule.cs rename to src/ScheduledTasks/Schedule.cs index 01902ba7..8f7fb3c0 100644 --- a/src/Abstractions/Entities/Schedule/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -1,13 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask; -using Microsoft.DurableTask.Entities; -using Microsoft.Extensions.Logging; - namespace DurableTask.Abstractions.Entities.Schedule; -public class ScheduleState +class ScheduleState { internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; @@ -19,67 +15,59 @@ public class ScheduleState internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - public HashSet UpdateConfig(ScheduleConfiguration scheduleUpdateConfig) + public HashSet UpdateConfig(ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) { Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); - Check.NotNull(scheduleUpdateConfig, nameof(scheduleUpdateConfig)); + Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); HashSet updatedFields = new HashSet(); - this.ScheduleConfiguration.Version++; - - if (!string.IsNullOrEmpty(scheduleUpdateConfig.OrchestrationName)) + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) { - this.ScheduleConfiguration.OrchestrationName = scheduleUpdateConfig.OrchestrationName; + this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); } - if (!string.IsNullOrEmpty(scheduleUpdateConfig.ScheduleId)) - { - this.ScheduleConfiguration.ScheduleId = scheduleUpdateConfig.ScheduleId; - updatedFields.Add(nameof(this.ScheduleConfiguration.ScheduleId)); - } - - if (scheduleUpdateConfig.OrchestrationInput == null) + if (scheduleConfigUpdateOptions.OrchestrationInput == null) { - this.ScheduleConfiguration.OrchestrationInput = scheduleUpdateConfig.OrchestrationInput; + this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); } - if (scheduleUpdateConfig.StartAt.HasValue) + if (scheduleConfigUpdateOptions.StartAt.HasValue) { - this.ScheduleConfiguration.StartAt = scheduleUpdateConfig.StartAt; + this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); } - if (scheduleUpdateConfig.EndAt.HasValue) + if (scheduleConfigUpdateOptions.EndAt.HasValue) { - this.ScheduleConfiguration.EndAt = scheduleUpdateConfig.EndAt; + this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); } - if (scheduleUpdateConfig.Interval.HasValue) + if (scheduleConfigUpdateOptions.Interval.HasValue) { - this.ScheduleConfiguration.Interval = scheduleUpdateConfig.Interval; + this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); } - if (!string.IsNullOrEmpty(scheduleUpdateConfig.CronExpression)) + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) { - this.ScheduleConfiguration.CronExpression = scheduleUpdateConfig.CronExpression; + this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); } - if (scheduleUpdateConfig.MaxOccurrence != 0) + if (scheduleConfigUpdateOptions.MaxOccurrence != 0) { - this.ScheduleConfiguration.MaxOccurrence = scheduleUpdateConfig.MaxOccurrence; + this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); } // Only update if the customer explicitly set a value - if (scheduleUpdateConfig.StartImmediatelyIfLate.HasValue) + if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) { - this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleUpdateConfig.StartImmediatelyIfLate.Value; + this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); } @@ -95,13 +83,14 @@ public void RefreshScheduleRunExecutionToken() } } -public class ScheduleConfiguration +// TODO: Create a updatescheduleconfig schema for customer to contain the fields they can update only, and add validations in client side +// TODO: NJsonSchema +class ScheduleConfiguration { public ScheduleConfiguration(string orchestrationName, string scheduleId) { this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); - this.Version++; } string orchestrationName; @@ -115,20 +104,65 @@ public string OrchestrationName } } - string scheduleId; + public string ScheduleId { get; init; } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } +} + +public class ScheduleConfigurationUpdateOptions +{ + string? orchestrationName; - public string ScheduleId + public string? OrchestrationName { - get => this.scheduleId; + get => this.orchestrationName; set { - this.scheduleId = Check.NotNullOrEmpty(value, nameof(value)); + this.orchestrationName = value; } } public string? OrchestrationInput { get; set; } - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + public string? OrchestrationInstanceId { get; set; } public DateTimeOffset? StartAt { get; set; } @@ -165,8 +199,6 @@ public TimeSpan? Interval public int MaxOccurrence { get; set; } public bool? StartImmediatelyIfLate { get; set; } - - internal int Version { get; set; } // Tracking schedule config version } enum ScheduleStatus @@ -176,31 +208,27 @@ enum ScheduleStatus Paused, // Schedule is paused } -public class Schedule : TaskEntity +// Valid schedule status transitions +static class ScheduleTransitions { - readonly ILogger logger; - - // Valid schedule status transitions - readonly Dictionary> validTransitions = new Dictionary> - { + static readonly Dictionary> ValidTransitions = + new Dictionary> { - ScheduleStatus.Uninitialized, - new HashSet { ScheduleStatus.Active } - }, - { - ScheduleStatus.Active, - new HashSet { ScheduleStatus.Paused } - }, - { - ScheduleStatus.Paused, - new HashSet { ScheduleStatus.Active } - }, - }; + { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, + { ScheduleStatus.Active, new HashSet { ScheduleStatus.Paused } }, + { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, + }; - public Schedule(ILogger logger) + public static bool TryGetValidTransitions(ScheduleStatus from, out HashSet validTargetStates) { - this.logger = logger; + return ValidTransitions.TryGetValue(from, out validTargetStates); } +} + +// TODO: Separate client request objects from entity state objects +class Schedule(ILogger logger) : TaskEntity +{ + readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration scheduleCreationConfig) { @@ -224,14 +252,14 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration sche /// /// Updates an existing schedule. /// - public void UpdateSchedule(TaskEntityContext context, ScheduleConfiguration scheduleUpdateConfig) + public void UpdateSchedule(TaskEntityContext context, ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) { - Verify.NotNull(scheduleUpdateConfig, nameof(scheduleUpdateConfig)); + Verify.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.logger.LogInformation($"Updating schedule with details: {scheduleUpdateConfig}"); + this.logger.LogInformation($"Updating schedule with details: {scheduleConfigUpdateOptions}"); - HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleUpdateConfig); + HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleConfigUpdateOptions); if (updatedScheduleConfigFields.Count == 0) { // no need to interrupt and update current schedule run as there is no change in the schedule config @@ -299,11 +327,7 @@ public void ResumeSchedule(TaskEntityContext context) context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - // TODO: Only implement this there is any cleanup shall be performed within entity before purging the instance. - public void DeleteSchedule() - { - throw new NotImplementedException(); - } + // TODO: Verify use built int entity delete operation to delete schedule // TODO: Support other schedule option properties like cron expression, max occurrence, etc. public void RunSchedule(TaskEntityContext context, string executionToken) @@ -378,7 +402,7 @@ void TryStatusTransition(ScheduleStatus to) HashSet validTargetStates; ScheduleStatus from = this.State.Status; - if (!this.validTransitions.TryGetValue(from, out validTargetStates) || !validTargetStates.Contains(to)) + if (!ScheduleTransitions.TryGetValidTransitions(from, out validTargetStates) || !validTargetStates.Contains(to)) { throw new InvalidOperationException($"Invalid state transition: Cannot transition from {from} to {to}"); } diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj new file mode 100644 index 00000000..2150e379 --- /dev/null +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + From 208f35d33124447723b0ce51dc4bdf56bb865993 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:49:17 -0800 Subject: [PATCH 015/203] save --- Microsoft.DurableTask.sln | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 9a6f48fe..58dca808 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -84,8 +84,11 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{3272C041-F81D-4C85-A4FB-2A700B5A7A9D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleDemo", "samples\ScheduleDemo\ScheduleDemo.csproj", "{FF37BC53-8EC1-4673-915B-E59B38E286DF}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppMinimal", "samples\ConsoleAppMinimal\ConsoleAppMinimal.csproj", "{B48FACA9-A328-452A-BFAE-C4F60F9C7024}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduledTasks", "src\ScheduledTasks\ScheduledTasks.csproj", "{69ED743C-D616-4530-87E2-391D249D7368}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -220,10 +223,18 @@ Global {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.Build.0 = Release|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.Build.0 = Release|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.Build.0 = Debug|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Release|Any CPU.ActiveCfg = Release|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Release|Any CPU.Build.0 = Release|Any CPU + {69ED743C-D616-4530-87E2-391D249D7368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69ED743C-D616-4530-87E2-391D249D7368}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69ED743C-D616-4530-87E2-391D249D7368}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69ED743C-D616-4530-87E2-391D249D7368}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -266,6 +277,7 @@ Global {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {3272C041-F81D-4C85-A4FB-2A700B5A7A9D} = {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} {B48FACA9-A328-452A-BFAE-C4F60F9C7024} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {69ED743C-D616-4530-87E2-391D249D7368} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} From 2b190375cac6a25cc28c21fcdb6ed0a05aab95cb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:32:28 -0800 Subject: [PATCH 016/203] save --- src/ScheduledTasks/Schedule.cs | 225 +----------------- src/ScheduledTasks/ScheduleConfiguration.cs | 66 +++++ .../ScheduleConfigurationUpdateOptions.cs | 58 +++++ src/ScheduledTasks/ScheduleState.cs | 84 +++++++ src/ScheduledTasks/ScheduleStatus.cs | 11 + src/ScheduledTasks/ScheduleTransitions.cs | 20 ++ src/ScheduledTasks/ScheduledTasks.csproj | 17 +- 7 files changed, 255 insertions(+), 226 deletions(-) create mode 100644 src/ScheduledTasks/ScheduleConfiguration.cs create mode 100644 src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs create mode 100644 src/ScheduledTasks/ScheduleState.cs create mode 100644 src/ScheduledTasks/ScheduleStatus.cs create mode 100644 src/ScheduledTasks/ScheduleTransitions.cs diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index 8f7fb3c0..8712892e 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -1,229 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace DurableTask.Abstractions.Entities.Schedule; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; -class ScheduleState -{ - internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; - - internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); - - internal DateTimeOffset? LastRunAt { get; set; } - - internal DateTimeOffset? NextRunAt { get; set; } - - internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - - public HashSet UpdateConfig(ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) - { - Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); - Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); - - HashSet updatedFields = new HashSet(); - - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) - { - this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); - } - - if (scheduleConfigUpdateOptions.OrchestrationInput == null) - { - this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); - } - - if (scheduleConfigUpdateOptions.StartAt.HasValue) - { - this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); - } - - if (scheduleConfigUpdateOptions.EndAt.HasValue) - { - this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); - } - - if (scheduleConfigUpdateOptions.Interval.HasValue) - { - this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; - updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); - } - - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) - { - this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; - updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); - } - - if (scheduleConfigUpdateOptions.MaxOccurrence != 0) - { - this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; - updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); - } - - // Only update if the customer explicitly set a value - if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) - { - this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); - } - - return updatedFields; - } - - // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully - // This could incur little overhead as ideally the runSchedule with old token should be killed immediately - // but there is no support to cancel pending entity operations currently, can be a todo item - public void RefreshScheduleRunExecutionToken() - { - this.ExecutionToken = Guid.NewGuid().ToString("N"); - } -} - -// TODO: Create a updatescheduleconfig schema for customer to contain the fields they can update only, and add validations in client side -// TODO: NJsonSchema -class ScheduleConfiguration -{ - public ScheduleConfiguration(string orchestrationName, string scheduleId) - { - this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); - this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); - } - - string orchestrationName; - - public string OrchestrationName - { - get => this.orchestrationName; - set - { - this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } - } - - public string ScheduleId { get; init; } - - public string? OrchestrationInput { get; set; } - - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); - - public DateTimeOffset? StartAt { get; set; } - - public DateTimeOffset? EndAt { get; set; } - - TimeSpan? interval; - - public TimeSpan? Interval - { - get => this.interval; - set - { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } - - this.interval = value; - } - } - - public string? CronExpression { get; set; } - - public int MaxOccurrence { get; set; } - - public bool? StartImmediatelyIfLate { get; set; } -} - -public class ScheduleConfigurationUpdateOptions -{ - string? orchestrationName; - - public string? OrchestrationName - { - get => this.orchestrationName; - set - { - this.orchestrationName = value; - } - } - - public string? OrchestrationInput { get; set; } - - public string? OrchestrationInstanceId { get; set; } - - public DateTimeOffset? StartAt { get; set; } - - public DateTimeOffset? EndAt { get; set; } - - TimeSpan? interval; - - public TimeSpan? Interval - { - get => this.interval; - set - { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } - - this.interval = value; - } - } - - public string? CronExpression { get; set; } - - public int MaxOccurrence { get; set; } - - public bool? StartImmediatelyIfLate { get; set; } -} - -enum ScheduleStatus -{ - Uninitialized, // Schedule has not been created - Active, // Schedule is active and running - Paused, // Schedule is paused -} - -// Valid schedule status transitions -static class ScheduleTransitions -{ - static readonly Dictionary> ValidTransitions = - new Dictionary> - { - { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, - { ScheduleStatus.Active, new HashSet { ScheduleStatus.Paused } }, - { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, - }; - - public static bool TryGetValidTransitions(ScheduleStatus from, out HashSet validTargetStates) - { - return ValidTransitions.TryGetValue(from, out validTargetStates); - } -} +namespace Microsoft.DurableTask; // TODO: Separate client request objects from entity state objects class Schedule(ILogger logger) : TaskEntity diff --git a/src/ScheduledTasks/ScheduleConfiguration.cs b/src/ScheduledTasks/ScheduleConfiguration.cs new file mode 100644 index 00000000..49b2017d --- /dev/null +++ b/src/ScheduledTasks/ScheduleConfiguration.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask; + +class ScheduleConfiguration +{ + public ScheduleConfiguration(string orchestrationName, string scheduleId) + { + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + } + + string orchestrationName; + + public string OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + public string ScheduleId { get; init; } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } +} diff --git a/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs new file mode 100644 index 00000000..19321f30 --- /dev/null +++ b/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask; + +public class ScheduleConfigurationUpdateOptions +{ + string? orchestrationName; + + public string? OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = value; + } + } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } +} diff --git a/src/ScheduledTasks/ScheduleState.cs b/src/ScheduledTasks/ScheduleState.cs new file mode 100644 index 00000000..bb9623f0 --- /dev/null +++ b/src/ScheduledTasks/ScheduleState.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask; + +class ScheduleState +{ + internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; + + internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); + + internal DateTimeOffset? LastRunAt { get; set; } + + internal DateTimeOffset? NextRunAt { get; set; } + + internal ScheduleConfiguration? ScheduleConfiguration { get; set; } + + public HashSet UpdateConfig(ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) + { + Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); + Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); + + HashSet updatedFields = new HashSet(); + + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) + { + this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); + } + + if (scheduleConfigUpdateOptions.OrchestrationInput == null) + { + this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); + } + + if (scheduleConfigUpdateOptions.StartAt.HasValue) + { + this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); + } + + if (scheduleConfigUpdateOptions.EndAt.HasValue) + { + this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); + } + + if (scheduleConfigUpdateOptions.Interval.HasValue) + { + this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; + updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); + } + + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) + { + this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; + updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); + } + + if (scheduleConfigUpdateOptions.MaxOccurrence != 0) + { + this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; + updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); + } + + // Only update if the customer explicitly set a value + if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) + { + this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); + } + + return updatedFields; + } + + // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully + // This could incur little overhead as ideally the runSchedule with old token should be killed immediately + // but there is no support to cancel pending entity operations currently, can be a todo item + public void RefreshScheduleRunExecutionToken() + { + this.ExecutionToken = Guid.NewGuid().ToString("N"); + } +} diff --git a/src/ScheduledTasks/ScheduleStatus.cs b/src/ScheduledTasks/ScheduleStatus.cs new file mode 100644 index 00000000..76f90cf3 --- /dev/null +++ b/src/ScheduledTasks/ScheduleStatus.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask; + +enum ScheduleStatus +{ + Uninitialized, // Schedule has not been created + Active, // Schedule is active and running + Paused, // Schedule is paused +} diff --git a/src/ScheduledTasks/ScheduleTransitions.cs b/src/ScheduledTasks/ScheduleTransitions.cs new file mode 100644 index 00000000..6138a316 --- /dev/null +++ b/src/ScheduledTasks/ScheduleTransitions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask; + +static class ScheduleTransitions +{ + static readonly Dictionary> ValidTransitions = + new Dictionary> + { + { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, + { ScheduleStatus.Active, new HashSet { ScheduleStatus.Paused } }, + { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, + }; + + public static bool TryGetValidTransitions(ScheduleStatus from, out HashSet validTargetStates) + { + return ValidTransitions.TryGetValue(from, out validTargetStates); + } +} diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj index 2150e379..bb372bb8 100644 --- a/src/ScheduledTasks/ScheduledTasks.csproj +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -1,10 +1,19 @@  - Exe - net8.0 - enable - enable + net6.0 + Durable Task Scheduled Tasks Client + true + preview.1 + + + + + + + + + From 2e126b353e6916375a0a91235515e27a242a11eb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:26:56 -0800 Subject: [PATCH 017/203] save --- src/Client/Grpc/GrpcDurableTaskClient.cs | 38 ------------ .../DurableTaskClientExtensions.cs | 20 +++++++ src/ScheduledTasks/IScheduledTaskClient.cs | 59 +++++++++++++++++++ src/ScheduledTasks/Schedule.cs | 2 +- src/ScheduledTasks/ScheduleConfiguration.cs | 2 +- .../ScheduleConfigurationUpdateOptions.cs | 2 +- src/ScheduledTasks/ScheduleState.cs | 2 +- src/ScheduledTasks/ScheduleStatus.cs | 2 +- src/ScheduledTasks/ScheduleTransitions.cs | 2 +- src/ScheduledTasks/ScheduledTaskClient.cs | 0 10 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 src/ScheduledTasks/DurableTaskClientExtensions.cs create mode 100644 src/ScheduledTasks/IScheduledTaskClient.cs create mode 100644 src/ScheduledTasks/ScheduledTaskClient.cs diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index faecf251..8f0b7fe5 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -359,44 +359,6 @@ public override Task PurgeAllInstancesAsync( return this.PurgeInstancesCoreAsync(request, cancellation); } - // SCHEDULE SUPPORT - - /// - public override Task GetScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - /// - public override Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - /// - public override Task DeleteScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - /// - public override Task PauseScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - /// - public override Task ResumeScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - /// - public override Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker) { if (options.Channel is GrpcChannel c) diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs new file mode 100644 index 00000000..c736721d --- /dev/null +++ b/src/ScheduledTasks/DurableTaskClientExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Extension methods for working with . +/// +public static class DurableTaskClientExtensions +{ + /// + /// Gets a client for working with scheduled tasks. + /// + /// The DurableTaskClient instance. + /// A client for managing scheduled tasks. + public static ScheduledTasksClient ScheduledTasks(this DurableTaskClient client) + { + return new ScheduledTasksClient(client); + } +} diff --git a/src/ScheduledTasks/IScheduledTaskClient.cs b/src/ScheduledTasks/IScheduledTaskClient.cs new file mode 100644 index 00000000..e79cfb74 --- /dev/null +++ b/src/ScheduledTasks/IScheduledTaskClient.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Interface for managing scheduled tasks in a Durable Task application. +/// +public interface IScheduledTaskClient +{ + /// + /// Gets the current state of a schedule. + /// + /// The ID of the schedule to retrieve. + /// The current state of the schedule. + Task GetScheduleAsync(string scheduleId); + + /// + /// Gets a list of all schedule IDs. + /// + /// A list of schedule IDs. + Task> ListSchedulesAsync(); + + /// + /// Creates a new schedule with the specified configuration. + /// + /// The configuration for the new schedule. + /// The ID of the newly created schedule. + Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig); + + /// + /// Deletes an existing schedule. + /// + /// The ID of the schedule to delete. + /// A task that completes when the schedule is deleted. + Task DeleteScheduleAsync(string scheduleId); + + /// + /// Pauses an active schedule. + /// + /// The ID of the schedule to pause. + /// A task that completes when the schedule is paused. + Task PauseScheduleAsync(string scheduleId); + + /// + /// Resumes a paused schedule. + /// + /// The ID of the schedule to resume. + /// A task that completes when the schedule is resumed. + Task ResumeScheduleAsync(string scheduleId); + + /// + /// Updates an existing schedule with new configuration. + /// + /// The ID of the schedule to update. + /// The new configuration to apply to the schedule. + /// A task that completes when the schedule is updated. + Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig); +} diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index 8712892e..ad0261f1 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -4,7 +4,7 @@ using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; // TODO: Separate client request objects from entity state objects class Schedule(ILogger logger) : TaskEntity diff --git a/src/ScheduledTasks/ScheduleConfiguration.cs b/src/ScheduledTasks/ScheduleConfiguration.cs index 49b2017d..24c4ad78 100644 --- a/src/ScheduledTasks/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/ScheduleConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; class ScheduleConfiguration { diff --git a/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs index 19321f30..4af5527c 100644 --- a/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs +++ b/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; public class ScheduleConfigurationUpdateOptions { diff --git a/src/ScheduledTasks/ScheduleState.cs b/src/ScheduledTasks/ScheduleState.cs index bb9623f0..97b71808 100644 --- a/src/ScheduledTasks/ScheduleState.cs +++ b/src/ScheduledTasks/ScheduleState.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; class ScheduleState { diff --git a/src/ScheduledTasks/ScheduleStatus.cs b/src/ScheduledTasks/ScheduleStatus.cs index 76f90cf3..b2e667f0 100644 --- a/src/ScheduledTasks/ScheduleStatus.cs +++ b/src/ScheduledTasks/ScheduleStatus.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; enum ScheduleStatus { diff --git a/src/ScheduledTasks/ScheduleTransitions.cs b/src/ScheduledTasks/ScheduleTransitions.cs index 6138a316..0cc39f7c 100644 --- a/src/ScheduledTasks/ScheduleTransitions.cs +++ b/src/ScheduledTasks/ScheduleTransitions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.DurableTask; +namespace Microsoft.DurableTask.ScheduledTasks; static class ScheduleTransitions { diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs new file mode 100644 index 00000000..e69de29b From 6cab35c6c73ee011a5d137c13e71f03a952b2dfb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:31:10 -0800 Subject: [PATCH 018/203] save --- src/ScheduledTasks/ScheduleConfiguration.cs | 5 +- src/ScheduledTasks/ScheduleState.cs | 5 +- src/ScheduledTasks/ScheduledTaskClient.cs | 159 ++++++++++++++++++++ src/ScheduledTasks/ScheduledTasks.csproj | 1 + 4 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/ScheduleConfiguration.cs b/src/ScheduledTasks/ScheduleConfiguration.cs index 24c4ad78..51cd69a1 100644 --- a/src/ScheduledTasks/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/ScheduleConfiguration.cs @@ -3,7 +3,10 @@ namespace Microsoft.DurableTask.ScheduledTasks; -class ScheduleConfiguration +/// +/// Configuration for a scheduled task. +/// +public class ScheduleConfiguration { public ScheduleConfiguration(string orchestrationName, string scheduleId) { diff --git a/src/ScheduledTasks/ScheduleState.cs b/src/ScheduledTasks/ScheduleState.cs index 97b71808..d4102f8f 100644 --- a/src/ScheduledTasks/ScheduleState.cs +++ b/src/ScheduledTasks/ScheduleState.cs @@ -3,7 +3,10 @@ namespace Microsoft.DurableTask.ScheduledTasks; -class ScheduleState +/// +/// Represents the current state of a schedule. +/// +public class ScheduleState { internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs index e69de29b..2b1e340f 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/ScheduledTaskClient.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Client for managing scheduled tasks in a Durable Task application. +/// +public class ScheduledTaskClient : IScheduledTaskClient +{ + private readonly DurableTaskClient durableTaskClient; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Durable Task client to use for orchestration operations. + /// The logger to use for logging operations. + public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) + { + this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); + this.logger = Check.NotNull(logger, nameof(logger)); + } + + /// + public async Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) + { + Check.NotNull(scheduleConfig, nameof(scheduleConfig)); + this.logger.LogInformation("Creating new schedule with ID {ScheduleId}", scheduleConfig.ScheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfig.ScheduleId); + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfig); + + return scheduleConfig.ScheduleId; + } + + /// + public async Task DeleteScheduleAsync(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.logger.LogInformation("Deleting schedule with ID {ScheduleId}", scheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); + } + + /// + public async Task GetScheduleAsync(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.logger.LogInformation("Getting schedule with ID {ScheduleId}", scheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + + if (metadata == null || !metadata.IncludesState) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + return metadata.State; + } + + /// + public async Task> ListSchedulesAsync() + { + this.logger.LogInformation("Listing all schedules"); + + var query = new EntityQuery + { + EntityName = nameof(Schedule), + }; + + var scheduleIds = new List(); + await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + { + scheduleIds.Add(metadata.Id.Key); + } + + return scheduleIds; + } + + /// + public async Task PauseScheduleAsync(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.logger.LogInformation("Pausing schedule with ID {ScheduleId}", scheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.PauseSchedule)); + } + + /// + public async Task ResumeScheduleAsync(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.logger.LogInformation("Resuming schedule with ID {ScheduleId}", scheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.ResumeSchedule)); + } + + /// + public async Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + Check.NotNull(scheduleConfig, nameof(scheduleConfig)); + this.logger.LogInformation("Updating schedule with ID {ScheduleId}", scheduleId); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + // Convert ScheduleConfiguration to ScheduleConfigurationUpdateOptions + var updateOptions = new ScheduleConfigurationUpdateOptions + { + OrchestrationName = scheduleConfig.OrchestrationName, + OrchestrationInput = scheduleConfig.OrchestrationInput, + OrchestrationInstanceId = scheduleConfig.OrchestrationInstanceId, + StartAt = scheduleConfig.StartAt, + EndAt = scheduleConfig.EndAt, + Interval = scheduleConfig.Interval, + CronExpression = scheduleConfig.CronExpression, + MaxOccurrence = scheduleConfig.MaxOccurrence, + StartImmediatelyIfLate = scheduleConfig.StartImmediatelyIfLate + }; + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); + } +} diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj index bb372bb8..875e10ba 100644 --- a/src/ScheduledTasks/ScheduledTasks.csproj +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -10,6 +10,7 @@ + From 2b983b34789a46de11760bdcad2f8914153d30c2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:38:33 -0800 Subject: [PATCH 019/203] save --- src/ScheduledTasks/DurableTaskClientExtensions.cs | 4 ++-- src/ScheduledTasks/ScheduledTaskClient.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs index c736721d..602fc022 100644 --- a/src/ScheduledTasks/DurableTaskClientExtensions.cs +++ b/src/ScheduledTasks/DurableTaskClientExtensions.cs @@ -13,8 +13,8 @@ public static class DurableTaskClientExtensions /// /// The DurableTaskClient instance. /// A client for managing scheduled tasks. - public static ScheduledTasksClient ScheduledTasks(this DurableTaskClient client) + public static ScheduledTaskClient ScheduledTasks(this DurableTaskClient client) { - return new ScheduledTasksClient(client); + return new ScheduledTaskClient(client); } } diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs index 2b1e340f..98f83cd8 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/ScheduledTaskClient.cs @@ -82,7 +82,8 @@ public async Task> ListSchedulesAsync() var query = new EntityQuery { - EntityName = nameof(Schedule), + InstanceIdStartsWith = $"@{nameof(Schedule)}@", + IncludeState = false }; var scheduleIds = new List(); From 4ffd60a96bba268f231b348b8fa198b6589c941b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:16:54 -0800 Subject: [PATCH 020/203] save --- src/Client/Core/DurableTaskClient.cs | 31 ------------------- src/Client/Grpc/GrpcDurableTaskClient.cs | 1 - .../DurableTaskClientExtensions.cs | 2 ++ .../DurableTaskSchedulerWorkerExtensions.cs | 21 +++++++++++++ src/ScheduledTasks/ScheduledTaskClient.cs | 26 ++++------------ src/ScheduledTasks/ScheduledTasks.csproj | 1 + .../DurableTaskSchedulerWorkerExtensions.cs | 4 --- 7 files changed, 30 insertions(+), 56 deletions(-) create mode 100644 src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index cd4c6ea1..ea2f0a58 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -409,35 +409,4 @@ public virtual Task PurgeAllInstancesAsync( /// /// A that completes when the disposal completes. public abstract ValueTask DisposeAsync(); - - public virtual Task GetScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - public virtual Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - public virtual Task DeleteScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - public virtual Task PauseScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - public virtual Task ResumeScheduleAsync(string scheduleId) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } - - // update schedule - public virtual Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) - { - throw new NotSupportedException($"{this.GetType()} does not support schedules."); - } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index 8f0b7fe5..f0d6e6e8 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Text; -using DurableTask.Abstractions.Entities.Schedule; using Google.Protobuf.WellKnownTypes; using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs index 602fc022..e09a8bdf 100644 --- a/src/ScheduledTasks/DurableTaskClientExtensions.cs +++ b/src/ScheduledTasks/DurableTaskClientExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.DurableTask.Client; + namespace Microsoft.DurableTask.ScheduledTasks; /// diff --git a/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs new file mode 100644 index 00000000..c1f64a2d --- /dev/null +++ b/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Extension methods for configuring Durable Task workers to use the Azure Durable Task Scheduler service. +/// +public static class DurableTaskSchedulerWorkerExtensions +{ + /// + /// Adds scheduled task support to the worker builder. + /// + /// The worker builder to add scheduled task support to. + public static void EnableScheduleSupport(this IDurableTaskWorkerBuilder builder) + { + builder.AddTasks(r => r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp))); + } +} diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs index 98f83cd8..38b2db84 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/ScheduledTaskClient.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; @@ -16,29 +13,25 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduledTaskClient : IScheduledTaskClient { - private readonly DurableTaskClient durableTaskClient; - private readonly ILogger logger; + readonly DurableTaskClient durableTaskClient; /// /// Initializes a new instance of the class. /// /// The Durable Task client to use for orchestration operations. - /// The logger to use for logging operations. - public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) + public ScheduledTaskClient(DurableTaskClient durableTaskClient) { this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); - this.logger = Check.NotNull(logger, nameof(logger)); } /// public async Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) { Check.NotNull(scheduleConfig, nameof(scheduleConfig)); - this.logger.LogInformation("Creating new schedule with ID {ScheduleId}", scheduleConfig.ScheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfig.ScheduleId); await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfig); - + return scheduleConfig.ScheduleId; } @@ -46,7 +39,6 @@ public async Task CreateScheduleAsync(ScheduleConfiguration scheduleConf public async Task DeleteScheduleAsync(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - this.logger.LogInformation("Deleting schedule with ID {ScheduleId}", scheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -62,11 +54,10 @@ public async Task DeleteScheduleAsync(string scheduleId) public async Task GetScheduleAsync(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - this.logger.LogInformation("Getting schedule with ID {ScheduleId}", scheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - + if (metadata == null || !metadata.IncludesState) { throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); @@ -78,12 +69,10 @@ public async Task GetScheduleAsync(string scheduleId) /// public async Task> ListSchedulesAsync() { - this.logger.LogInformation("Listing all schedules"); - var query = new EntityQuery { InstanceIdStartsWith = $"@{nameof(Schedule)}@", - IncludeState = false + IncludeState = false, }; var scheduleIds = new List(); @@ -99,7 +88,6 @@ public async Task> ListSchedulesAsync() public async Task PauseScheduleAsync(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - this.logger.LogInformation("Pausing schedule with ID {ScheduleId}", scheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -115,7 +103,6 @@ public async Task PauseScheduleAsync(string scheduleId) public async Task ResumeScheduleAsync(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - this.logger.LogInformation("Resuming schedule with ID {ScheduleId}", scheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -132,7 +119,6 @@ public async Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration s { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); Check.NotNull(scheduleConfig, nameof(scheduleConfig)); - this.logger.LogInformation("Updating schedule with ID {ScheduleId}", scheduleId); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -152,7 +138,7 @@ public async Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration s Interval = scheduleConfig.Interval, CronExpression = scheduleConfig.CronExpression, MaxOccurrence = scheduleConfig.MaxOccurrence, - StartImmediatelyIfLate = scheduleConfig.StartImmediatelyIfLate + StartImmediatelyIfLate = scheduleConfig.StartImmediatelyIfLate, }; await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj index 875e10ba..dd4008c0 100644 --- a/src/ScheduledTasks/ScheduledTasks.csproj +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index ff375e15..63d19fa0 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using DurableTask.Abstractions.Entities.Schedule; namespace Microsoft.DurableTask.Worker.AzureManaged; @@ -82,9 +81,6 @@ static void ConfigureSchedulerOptions( Action initialConfig, Action? additionalConfig) { - // Add the Schedule entity by default - builder.AddTasks(r => r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp))); - builder.Services.AddOptions(builder.Name) .Configure(initialConfig) .Configure(additionalConfig ?? (_ => { })) From 6741e6d74ec9fe61f9cbd57be7badd9bcfca5679 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:26:56 -0800 Subject: [PATCH 021/203] save --- src/Grpc/versions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index ad90d91f..ca514f29 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2025-02-11 19:40:58 UTC +# The following files were downloaded from branch main at 2025-02-19 06:25:02 UTC https://raw.githubusercontent.com/microsoft/durabletask-protobuf/589cb5ecd9dd4b1fe463750defa3e2c84276b079/protos/orchestrator_service.proto From 93ee63213b568778f34b71ba1aac7fab946b34e5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:33:46 -0800 Subject: [PATCH 022/203] save --- samples/ScheduleDemo/Program.cs | 238 +++++++++++++-------------- src/Client/Core/DurableTaskClient.cs | 1 - 2 files changed, 119 insertions(+), 120 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index e5c210f3..64ff4628 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -1,122 +1,122 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.AzureManaged; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Worker.AzureManaged; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -class Program -{ - static async Task Main(string[] args) - { - // Create the host builder - IHost host = Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") - ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); - - // Configure the worker - services.AddDurableTaskWorker(builder => - { - // Add the Schedule entity and demo orchestration - _ = builder.AddTasks(r => - { - // Add a demo orchestration that will be triggered by the schedule - r.AddOrchestratorFunc("DemoOrchestration", async context => - { - string input = context.GetInput(); - await context.CallActivityAsync("ProcessMessage", input); - return $"Completed processing at {DateTime.UtcNow}"; - }); - // Add a demo activity - r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); - }); - - builder.UseDurableTaskScheduler(connectionString); - }); - - // Configure the client - services.AddDurableTaskClient(builder => - { - builder.UseDurableTaskScheduler(connectionString); - }); - - // Configure console logging - services.AddLogging(logging => - { - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); - }); - }) - .Build(); - - await host.StartAsync(); - await using DurableTaskClient client = host.Services.GetRequiredService(); - - try - { - // Create a schedule that runs every 30 seconds - ScheduleConfiguration scheduleConfig = new ScheduleConfiguration( - orchestrationName: "DemoOrchestration", - scheduleId: "demo-schedule") - { - Interval = TimeSpan.FromSeconds(30), - StartAt = DateTimeOffset.UtcNow, - OrchestrationInput = "This is a scheduled message!" - }; - - // Create the schedule - Console.WriteLine("Creating schedule..."); - await client.CreateScheduleAsync(scheduleConfig); - Console.WriteLine($"Created schedule with ID: {scheduleConfig.ScheduleId}"); - - // Monitor the schedule for a while - Console.WriteLine("\nMonitoring schedule for 2 minutes..."); - for (int i = 0; i < 4; i++) - { - await Task.Delay(TimeSpan.FromSeconds(30)); - var schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); - Console.WriteLine($"\nSchedule status: {schedule.Status}"); - Console.WriteLine($"Last run at: {schedule.LastRunAt}"); - Console.WriteLine($"Next run at: {schedule.NextRunAt}"); - } - - // Pause the schedule - Console.WriteLine("\nPausing schedule..."); - await client.PauseScheduleAsync(scheduleConfig.ScheduleId); - - var pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); - Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); - - // Resume the schedule - Console.WriteLine("\nResuming schedule..."); - await client.ResumeScheduleAsync(scheduleConfig.ScheduleId); - - var resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); - Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); - Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); - - Console.WriteLine("\nPress any key to delete the schedule and exit..."); - Console.ReadKey(); - - // Delete the schedule - await client.DeleteScheduleAsync(scheduleConfig.ScheduleId); - Console.WriteLine("Schedule deleted."); - } - catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - } - - await host.StopAsync(); - } -} +//using Microsoft.DurableTask.Client; +//using Microsoft.DurableTask.Client.AzureManaged; +//using Microsoft.DurableTask.Worker; +//using Microsoft.DurableTask.Worker.AzureManaged; +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Hosting; +//using Microsoft.Extensions.Logging; + +//class Program +//{ +// static async Task Main(string[] args) +// { +// // Create the host builder +// IHost host = Host.CreateDefaultBuilder(args) +// .ConfigureServices(services => +// { +// string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") +// ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + +// // Configure the worker +// services.AddDurableTaskWorker(builder => +// { +// // Add the Schedule entity and demo orchestration +// _ = builder.AddTasks(r => +// { +// // Add a demo orchestration that will be triggered by the schedule +// r.AddOrchestratorFunc("DemoOrchestration", async context => +// { +// string input = context.GetInput(); +// await context.CallActivityAsync("ProcessMessage", input); +// return $"Completed processing at {DateTime.UtcNow}"; +// }); +// // Add a demo activity +// r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); +// }); + +// builder.UseDurableTaskScheduler(connectionString); +// }); + +// // Configure the client +// services.AddDurableTaskClient(builder => +// { +// builder.UseDurableTaskScheduler(connectionString); +// }); + +// // Configure console logging +// services.AddLogging(logging => +// { +// logging.AddSimpleConsole(options => +// { +// options.SingleLine = true; +// options.UseUtcTimestamp = true; +// options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; +// }); +// }); +// }) +// .Build(); + +// await host.StartAsync(); +// await using DurableTaskClient client = host.Services.GetRequiredService(); + +// try +// { +// // Create a schedule that runs every 30 seconds +// ScheduleConfiguration scheduleConfig = new ScheduleConfiguration( +// orchestrationName: "DemoOrchestration", +// scheduleId: "demo-schedule") +// { +// Interval = TimeSpan.FromSeconds(30), +// StartAt = DateTimeOffset.UtcNow, +// OrchestrationInput = "This is a scheduled message!" +// }; + +// // Create the schedule +// Console.WriteLine("Creating schedule..."); +// await client.CreateScheduleAsync(scheduleConfig); +// Console.WriteLine($"Created schedule with ID: {scheduleConfig.ScheduleId}"); + +// // Monitor the schedule for a while +// Console.WriteLine("\nMonitoring schedule for 2 minutes..."); +// for (int i = 0; i < 4; i++) +// { +// await Task.Delay(TimeSpan.FromSeconds(30)); +// var schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); +// Console.WriteLine($"\nSchedule status: {schedule.Status}"); +// Console.WriteLine($"Last run at: {schedule.LastRunAt}"); +// Console.WriteLine($"Next run at: {schedule.NextRunAt}"); +// } + +// // Pause the schedule +// Console.WriteLine("\nPausing schedule..."); +// await client.PauseScheduleAsync(scheduleConfig.ScheduleId); + +// var pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); +// Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); + +// // Resume the schedule +// Console.WriteLine("\nResuming schedule..."); +// await client.ResumeScheduleAsync(scheduleConfig.ScheduleId); + +// var resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); +// Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); +// Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); + +// Console.WriteLine("\nPress any key to delete the schedule and exit..."); +// Console.ReadKey(); + +// // Delete the schedule +// await client.DeleteScheduleAsync(scheduleConfig.ScheduleId); +// Console.WriteLine("Schedule deleted."); +// } +// catch (Exception ex) +// { +// Console.WriteLine($"Error: {ex.Message}"); +// } + +// await host.StopAsync(); +// } +//} diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index ea2f0a58..16d6d842 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.ComponentModel; -using DurableTask.Abstractions.Entities.Schedule; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Internal; From 9987b15ffbbb51faa33d33dc02ea4b59ad40e86a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:54:06 -0800 Subject: [PATCH 023/203] save --- samples/ScheduleDemo/ScheduleConfiguration.cs | 84 +++++++++---------- samples/ScheduleDemo/ScheduleDemo.csproj | 4 +- src/Client/Core/DurableTaskClient.cs | 2 +- src/Client/Grpc/GrpcDurableTaskClient.cs | 2 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/samples/ScheduleDemo/ScheduleConfiguration.cs b/samples/ScheduleDemo/ScheduleConfiguration.cs index a39c93e7..142b0f57 100644 --- a/samples/ScheduleDemo/ScheduleConfiguration.cs +++ b/samples/ScheduleDemo/ScheduleConfiguration.cs @@ -1,58 +1,58 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. -class ScheduleConfiguration -{ - public ScheduleConfiguration(string orchestrationName, string scheduleId) - { - this.OrchestrationName = orchestrationName; - this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); - this.Version++; - } +// class ScheduleConfiguration +// { +// public ScheduleConfiguration(string orchestrationName, string scheduleId) +// { +// this.OrchestrationName = orchestrationName; +// this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); +// this.Version++; +// } - public string OrchestrationName { get; set; } +// public string OrchestrationName { get; set; } - public string ScheduleId { get; set; } +// public string ScheduleId { get; set; } - public string? OrchestrationInput { get; set; } +// public string? OrchestrationInput { get; set; } - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); +// public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); - public DateTimeOffset? StartAt { get; set; } +// public DateTimeOffset? StartAt { get; set; } - public DateTimeOffset? EndAt { get; set; } +// public DateTimeOffset? EndAt { get; set; } - TimeSpan? interval; +// TimeSpan? interval; - public TimeSpan? Interval - { - get => this.interval; - set - { - if (!value.HasValue) - { - return; - } +// public TimeSpan? Interval +// { +// get => this.interval; +// set +// { +// if (!value.HasValue) +// { +// return; +// } - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } +// if (value.Value <= TimeSpan.Zero) +// { +// throw new ArgumentException("Interval must be positive", nameof(value)); +// } - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } +// if (value.Value.TotalSeconds < 1) +// { +// throw new ArgumentException("Interval must be at least 1 second", nameof(value)); +// } - this.interval = value; - } - } +// this.interval = value; +// } +// } - public string? CronExpression { get; set; } +// public string? CronExpression { get; set; } - public int MaxOccurrence { get; set; } +// public int MaxOccurrence { get; set; } - public bool? StartImmediatelyIfLate { get; set; } +// public bool? StartImmediatelyIfLate { get; set; } - internal int Version { get; set; } // Tracking schedule config version -} \ No newline at end of file +// internal int Version { get; set; } // Tracking schedule config version +// } \ No newline at end of file diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index f4a1fbb0..45dd1ae3 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index 16d6d842..dcf33f93 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -408,4 +408,4 @@ public virtual Task PurgeAllInstancesAsync( /// /// A that completes when the disposal completes. public abstract ValueTask DisposeAsync(); -} +} \ No newline at end of file diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index f0d6e6e8..0fa87d95 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -431,4 +431,4 @@ OrchestrationMetadata CreateMetadata(P.OrchestrationState state, bool includeInp DataConverter = includeInputsAndOutputs ? this.DataConverter : null, }; } -} +} \ No newline at end of file From 6a75677e881d4fef69b65e0f1a40e445024477c9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:14:07 -0800 Subject: [PATCH 024/203] save --- src/ScheduledTasks/Logs.cs | 33 ++++++++ .../Models/ScheduleConfiguration.cs | 84 +++++++++++++++++++ .../ScheduleConfigurationCreateOptions.cs} | 9 +- .../ScheduleConfigurationUpdateOptions.cs | 0 .../{ => Models}/ScheduleState.cs | 0 .../{ => Models}/ScheduleStatus.cs | 0 src/ScheduledTasks/Schedule.cs | 12 ++- 7 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 src/ScheduledTasks/Logs.cs create mode 100644 src/ScheduledTasks/Models/ScheduleConfiguration.cs rename src/ScheduledTasks/{ScheduleConfiguration.cs => Models/ScheduleConfigurationCreateOptions.cs} (81%) rename src/ScheduledTasks/{ => Models}/ScheduleConfigurationUpdateOptions.cs (100%) rename src/ScheduledTasks/{ => Models}/ScheduleState.cs (100%) rename src/ScheduledTasks/{ => Models}/ScheduleStatus.cs (100%) diff --git a/src/ScheduledTasks/Logs.cs b/src/ScheduledTasks/Logs.cs new file mode 100644 index 00000000..d59d57f7 --- /dev/null +++ b/src/ScheduledTasks/Logs.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; +/// +/// Log messages. +/// +/// +/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. +/// +static partial class Logs +{ + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigurationCreateOptions}")] + public static partial void CreatingSchedule(this ILogger logger, ScheduleConfigurationCreateOptions scheduleConfigurationCreateOptions); + + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}' with options: {scheduleConfigurationUpdateOptions}")] + public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleConfigurationUpdateOptions scheduleConfigurationUpdateOptions); + + [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] + public static partial void PausingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] + public static partial void ResumingSchedule(this ILogger logger, string scheduleId); + + // run schedule logging + [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Running schedule '{scheduleId}'")] + public static partial void RunningSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] + public static partial void DeletingSchedule(this ILogger logger, string scheduleId); +} \ No newline at end of file diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs new file mode 100644 index 00000000..37d1d1f1 --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Configuration for a scheduled task. +/// +public class ScheduleConfiguration +{ + public ScheduleConfiguration(string orchestrationName, string scheduleId) + { + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + } + + string orchestrationName; + + public string OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + public string ScheduleId { get; init; } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public string? CronExpression { get; set; } + + public int MaxOccurrence { get; set; } + + public bool? StartImmediatelyIfLate { get; set; } + + public static ScheduleConfiguration FromCreateOptions(ScheduleConfigurationCreateOptions createOptions) + { + return new ScheduleConfiguration(createOptions.OrchestrationName, createOptions.ScheduleId) + { + OrchestrationInput = createOptions.OrchestrationInput, + OrchestrationInstanceId = createOptions.OrchestrationInstanceId, + StartAt = createOptions.StartAt, + EndAt = createOptions.EndAt, + Interval = createOptions.Interval, + CronExpression = createOptions.CronExpression, + MaxOccurrence = createOptions.MaxOccurrence, + StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate + }; + } +} diff --git a/src/ScheduledTasks/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs similarity index 81% rename from src/ScheduledTasks/ScheduleConfiguration.cs rename to src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs index 51cd69a1..29532086 100644 --- a/src/ScheduledTasks/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs @@ -6,9 +6,14 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Configuration for a scheduled task. /// -public class ScheduleConfiguration +public class ScheduleConfigurationCreateOptions { - public ScheduleConfiguration(string orchestrationName, string scheduleId) + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ScheduleConfigurationCreateOptions(string orchestrationName, string scheduleId) { this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); diff --git a/src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs similarity index 100% rename from src/ScheduledTasks/ScheduleConfigurationUpdateOptions.cs rename to src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs diff --git a/src/ScheduledTasks/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs similarity index 100% rename from src/ScheduledTasks/ScheduleState.cs rename to src/ScheduledTasks/Models/ScheduleState.cs diff --git a/src/ScheduledTasks/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs similarity index 100% rename from src/ScheduledTasks/ScheduleStatus.cs rename to src/ScheduledTasks/Models/ScheduleStatus.cs diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index ad0261f1..a576277e 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -6,23 +6,21 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: Separate client request objects from entity state objects +// TODO: logging class Schedule(ILogger logger) : TaskEntity { readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); - public void CreateSchedule(TaskEntityContext context, ScheduleConfiguration scheduleCreationConfig) + public void CreateSchedule(TaskEntityContext context, ScheduleConfigurationCreateOptions scheduleConfigurationCreateOptions) { - Verify.NotNull(scheduleCreationConfig, nameof(scheduleCreationConfig)); + Verify.NotNull(scheduleConfigurationCreateOptions, nameof(scheduleConfigurationCreateOptions)); if (this.State.Status != ScheduleStatus.Uninitialized) { throw new InvalidOperationException("Schedule is already created."); } - this.logger.LogInformation($"Creating schedule with options: {scheduleCreationConfig}"); - - this.State.ScheduleConfiguration = scheduleCreationConfig; + this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleConfigurationCreateOptions); this.TryStatusTransition(ScheduleStatus.Active); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately @@ -38,7 +36,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleConfigurationUpdat Verify.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.logger.LogInformation($"Updating schedule with details: {scheduleConfigUpdateOptions}"); + this.logger.UpdatingSchedule(this.State.ScheduleConfiguration.ScheduleId, scheduleConfigUpdateOptions); HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleConfigUpdateOptions); if (updatedScheduleConfigFields.Count == 0) From 7024f00ea2c99f76559c79ed782666fd1b0a02a3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:16:07 -0800 Subject: [PATCH 025/203] add --- src/ScheduledTasks/Schedule.cs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index a576277e..6858053a 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -7,10 +7,26 @@ namespace Microsoft.DurableTask.ScheduledTasks; // TODO: logging +/// +/// Entity that manages the state and execution of a scheduled task. +/// +/// +/// The Schedule entity maintains the configuration and state of a scheduled task, +/// handling operations like creation, updates, pausing/resuming, and executing the task +/// according to the defined schedule. +/// +/// Logger for recording schedule operations and events. class Schedule(ILogger logger) : TaskEntity { readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + /// + /// Creates a new schedule with the specified configuration. + /// + /// The task entity context. + /// The configuration options for creating the schedule. + /// Thrown when scheduleConfigurationCreateOptions is null. + /// Thrown when the schedule is already created. public void CreateSchedule(TaskEntityContext context, ScheduleConfigurationCreateOptions scheduleConfigurationCreateOptions) { Verify.NotNull(scheduleConfigurationCreateOptions, nameof(scheduleConfigurationCreateOptions)); @@ -31,6 +47,10 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfigurationCreat /// /// Updates an existing schedule. /// + /// The task entity context. + /// The options for updating the schedule configuration. + /// Thrown when scheduleConfigUpdateOptions is null. + /// Thrown when the schedule is not created. public void UpdateSchedule(TaskEntityContext context, ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) { Verify.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); @@ -90,6 +110,8 @@ public void PauseSchedule() /// /// Resumes the schedule. /// + /// The task entity context. + /// Thrown when the schedule is not paused. public void ResumeSchedule(TaskEntityContext context) { Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); @@ -109,6 +131,13 @@ public void ResumeSchedule(TaskEntityContext context) // TODO: Verify use built int entity delete operation to delete schedule // TODO: Support other schedule option properties like cron expression, max occurrence, etc. + /// + /// Runs the schedule based on the defined configuration. + /// + /// The task entity context. + /// The execution token for the schedule. + /// Thrown when the schedule configuration is null. + /// Thrown when the schedule is not active. public void RunSchedule(TaskEntityContext context, string executionToken) { Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); From 4c1036cdd6d941c9aca3dea77400e96873f714cb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:49:16 -0800 Subject: [PATCH 026/203] save --- src/ScheduledTasks/Schedule.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index 6858053a..ed54f3e4 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -7,6 +7,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; // TODO: logging + /// /// Entity that manages the state and execution of a scheduled task. /// @@ -131,19 +132,19 @@ public void ResumeSchedule(TaskEntityContext context) // TODO: Verify use built int entity delete operation to delete schedule // TODO: Support other schedule option properties like cron expression, max occurrence, etc. + /// /// Runs the schedule based on the defined configuration. /// /// The task entity context. /// The execution token for the schedule. - /// Thrown when the schedule configuration is null. - /// Thrown when the schedule is not active. + /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); if (this.State.ScheduleConfiguration.Interval == null) { - throw new ArgumentNullException(nameof(this.State.ScheduleConfiguration.Interval)); + throw new InvalidOperationException("Schedule interval must be specified."); } if (executionToken != this.State.ExecutionToken) @@ -195,7 +196,8 @@ public void RunSchedule(TaskEntityContext context, string executionToken) nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), - this.State.ExecutionToken, new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); + this.State.ExecutionToken, + new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); } void StartOrchestrationIfNotRunning(TaskEntityContext context) From 08c0f5c626c6acca5791d13dbee7f84ff00d5566 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:23:40 -0800 Subject: [PATCH 027/203] save --- src/ScheduledTasks/IScheduledTaskClient.cs | 9 +- src/ScheduledTasks/Logs.cs | 4 +- .../Models/ScheduleConfiguration.cs | 4 +- ...eOptions.cs => ScheduleCreationOptions.cs} | 6 +- .../Models/ScheduleDescription.cs | 87 +++++++++++++++++++ src/ScheduledTasks/Models/ScheduleState.cs | 4 +- ...ateOptions.cs => ScheduleUpdateOptions.cs} | 2 +- src/ScheduledTasks/Schedule.cs | 4 +- src/ScheduledTasks/ScheduledTaskClient.cs | 35 ++++---- 9 files changed, 120 insertions(+), 35 deletions(-) rename src/ScheduledTasks/Models/{ScheduleConfigurationCreateOptions.cs => ScheduleCreationOptions.cs} (88%) create mode 100644 src/ScheduledTasks/Models/ScheduleDescription.cs rename src/ScheduledTasks/Models/{ScheduleConfigurationUpdateOptions.cs => ScheduleUpdateOptions.cs} (96%) diff --git a/src/ScheduledTasks/IScheduledTaskClient.cs b/src/ScheduledTasks/IScheduledTaskClient.cs index e79cfb74..c5206384 100644 --- a/src/ScheduledTasks/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/IScheduledTaskClient.cs @@ -20,13 +20,12 @@ public interface IScheduledTaskClient /// /// A list of schedule IDs. Task> ListSchedulesAsync(); - + /// /// Creates a new schedule with the specified configuration. /// - /// The configuration for the new schedule. /// The ID of the newly created schedule. - Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig); + Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); /// /// Deletes an existing schedule. @@ -53,7 +52,7 @@ public interface IScheduledTaskClient /// Updates an existing schedule with new configuration. /// /// The ID of the schedule to update. - /// The new configuration to apply to the schedule. + /// The options for updating the schedule configuration. /// A task that completes when the schedule is updated. - Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig); + Task UpdateScheduleAsync(string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); } diff --git a/src/ScheduledTasks/Logs.cs b/src/ScheduledTasks/Logs.cs index d59d57f7..163b8b59 100644 --- a/src/ScheduledTasks/Logs.cs +++ b/src/ScheduledTasks/Logs.cs @@ -13,10 +13,10 @@ namespace Microsoft.DurableTask.ScheduledTasks; static partial class Logs { [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigurationCreateOptions}")] - public static partial void CreatingSchedule(this ILogger logger, ScheduleConfigurationCreateOptions scheduleConfigurationCreateOptions); + public static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}' with options: {scheduleConfigurationUpdateOptions}")] - public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleConfigurationUpdateOptions scheduleConfigurationUpdateOptions); + public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] public static partial void PausingSchedule(this ILogger logger, string scheduleId); diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 37d1d1f1..199aea6c 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Configuration for a scheduled task. /// -public class ScheduleConfiguration +class ScheduleConfiguration { public ScheduleConfiguration(string orchestrationName, string scheduleId) { @@ -67,7 +67,7 @@ public TimeSpan? Interval public bool? StartImmediatelyIfLate { get; set; } - public static ScheduleConfiguration FromCreateOptions(ScheduleConfigurationCreateOptions createOptions) + public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions createOptions) { return new ScheduleConfiguration(createOptions.OrchestrationName, createOptions.ScheduleId) { diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs similarity index 88% rename from src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs rename to src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 29532086..adc2ec64 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -6,14 +6,14 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Configuration for a scheduled task. /// -public class ScheduleConfigurationCreateOptions +public class ScheduleCreationOptions { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// - public ScheduleConfigurationCreateOptions(string orchestrationName, string scheduleId) + public ScheduleCreationOptions(string orchestrationName, string scheduleId) { this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs new file mode 100644 index 00000000..6ea343e1 --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents the current state of a schedule. +/// +class ScheduleState +{ + internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; + + internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); + + internal DateTimeOffset? LastRunAt { get; set; } + + internal DateTimeOffset? NextRunAt { get; set; } + + internal ScheduleConfiguration? ScheduleConfiguration { get; set; } + + public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOptions) + { + Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); + Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); + + HashSet updatedFields = new HashSet(); + + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) + { + this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); + } + + if (scheduleConfigUpdateOptions.OrchestrationInput == null) + { + this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; + updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); + } + + if (scheduleConfigUpdateOptions.StartAt.HasValue) + { + this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); + } + + if (scheduleConfigUpdateOptions.EndAt.HasValue) + { + this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; + updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); + } + + if (scheduleConfigUpdateOptions.Interval.HasValue) + { + this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; + updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); + } + + if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) + { + this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; + updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); + } + + if (scheduleConfigUpdateOptions.MaxOccurrence != 0) + { + this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; + updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); + } + + // Only update if the customer explicitly set a value + if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) + { + this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; + updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); + } + + return updatedFields; + } + + // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully + // This could incur little overhead as ideally the runSchedule with old token should be killed immediately + // but there is no support to cancel pending entity operations currently, can be a todo item + public void RefreshScheduleRunExecutionToken() + { + this.ExecutionToken = Guid.NewGuid().ToString("N"); + } +} diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index d4102f8f..6ea343e1 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents the current state of a schedule. /// -public class ScheduleState +class ScheduleState { internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; @@ -18,7 +18,7 @@ public class ScheduleState internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - public HashSet UpdateConfig(ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) + public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOptions) { Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs similarity index 96% rename from src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs rename to src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 4af5527c..9aee8c2d 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -3,7 +3,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; -public class ScheduleConfigurationUpdateOptions +public class ScheduleUpdateOptions { string? orchestrationName; diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index ed54f3e4..c8f2d079 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -28,7 +28,7 @@ class Schedule(ILogger logger) : TaskEntity /// The configuration options for creating the schedule. /// Thrown when scheduleConfigurationCreateOptions is null. /// Thrown when the schedule is already created. - public void CreateSchedule(TaskEntityContext context, ScheduleConfigurationCreateOptions scheduleConfigurationCreateOptions) + public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions scheduleConfigurationCreateOptions) { Verify.NotNull(scheduleConfigurationCreateOptions, nameof(scheduleConfigurationCreateOptions)); @@ -52,7 +52,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleConfigurationCreat /// The options for updating the schedule configuration. /// Thrown when scheduleConfigUpdateOptions is null. /// Thrown when the schedule is not created. - public void UpdateSchedule(TaskEntityContext context, ScheduleConfigurationUpdateOptions scheduleConfigUpdateOptions) + public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions scheduleConfigUpdateOptions) { Verify.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs index 38b2db84..71ff06e4 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/ScheduledTaskClient.cs @@ -4,7 +4,6 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; -using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -25,14 +24,14 @@ public ScheduledTaskClient(DurableTaskClient durableTaskClient) } /// - public async Task CreateScheduleAsync(ScheduleConfiguration scheduleConfig) + public async Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions) { - Check.NotNull(scheduleConfig, nameof(scheduleConfig)); + Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfig.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfig); + var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); - return scheduleConfig.ScheduleId; + return scheduleConfigCreateOptions.ScheduleId; } /// @@ -115,10 +114,10 @@ public async Task ResumeScheduleAsync(string scheduleId) } /// - public async Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration scheduleConfig) + public async Task UpdateScheduleAsync(string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - Check.NotNull(scheduleConfig, nameof(scheduleConfig)); + Check.NotNull(scheduleConfigurationUpdateOptions, nameof(scheduleConfigurationUpdateOptions)); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -128,17 +127,17 @@ public async Task UpdateScheduleAsync(string scheduleId, ScheduleConfiguration s } // Convert ScheduleConfiguration to ScheduleConfigurationUpdateOptions - var updateOptions = new ScheduleConfigurationUpdateOptions + var updateOptions = new ScheduleUpdateOptions { - OrchestrationName = scheduleConfig.OrchestrationName, - OrchestrationInput = scheduleConfig.OrchestrationInput, - OrchestrationInstanceId = scheduleConfig.OrchestrationInstanceId, - StartAt = scheduleConfig.StartAt, - EndAt = scheduleConfig.EndAt, - Interval = scheduleConfig.Interval, - CronExpression = scheduleConfig.CronExpression, - MaxOccurrence = scheduleConfig.MaxOccurrence, - StartImmediatelyIfLate = scheduleConfig.StartImmediatelyIfLate, + OrchestrationName = scheduleConfigurationUpdateOptions.OrchestrationName, + OrchestrationInput = scheduleConfigurationUpdateOptions.OrchestrationInput, + OrchestrationInstanceId = scheduleConfigurationUpdateOptions.OrchestrationInstanceId, + StartAt = scheduleConfigurationUpdateOptions.StartAt, + EndAt = scheduleConfigurationUpdateOptions.EndAt, + Interval = scheduleConfigurationUpdateOptions.Interval, + CronExpression = scheduleConfigurationUpdateOptions.CronExpression, + MaxOccurrence = scheduleConfigurationUpdateOptions.MaxOccurrence, + StartImmediatelyIfLate = scheduleConfigurationUpdateOptions.StartImmediatelyIfLate, }; await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); From a6951bf25401a8a6c92f5db630f1b6a304325802 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:30:31 -0800 Subject: [PATCH 028/203] save --- src/ScheduledTasks/IScheduledTaskClient.cs | 2 +- .../Models/ScheduleConfiguration.cs | 2 +- .../Models/ScheduleDescription.cs | 97 ++++--------------- src/ScheduledTasks/Models/ScheduleStatus.cs | 2 +- src/ScheduledTasks/ScheduledTaskClient.cs | 14 +-- 5 files changed, 22 insertions(+), 95 deletions(-) diff --git a/src/ScheduledTasks/IScheduledTaskClient.cs b/src/ScheduledTasks/IScheduledTaskClient.cs index c5206384..a20a8bad 100644 --- a/src/ScheduledTasks/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/IScheduledTaskClient.cs @@ -13,7 +13,7 @@ public interface IScheduledTaskClient /// /// The ID of the schedule to retrieve. /// The current state of the schedule. - Task GetScheduleAsync(string scheduleId); + Task GetScheduleAsync(string scheduleId); /// /// Gets a list of all schedule IDs. diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 199aea6c..01c0c441 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -78,7 +78,7 @@ public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions cr Interval = createOptions.Interval, CronExpression = createOptions.CronExpression, MaxOccurrence = createOptions.MaxOccurrence, - StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate + StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate, }; } } diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 6ea343e1..2ce27b9a 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -4,84 +4,21 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Represents the current state of a schedule. +/// Represents the comprehensive details of a schedule. /// -class ScheduleState -{ - internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; - - internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); - - internal DateTimeOffset? LastRunAt { get; set; } - - internal DateTimeOffset? NextRunAt { get; set; } - - internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - - public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOptions) - { - Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); - Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); - - HashSet updatedFields = new HashSet(); - - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) - { - this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); - } - - if (scheduleConfigUpdateOptions.OrchestrationInput == null) - { - this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); - } - - if (scheduleConfigUpdateOptions.StartAt.HasValue) - { - this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); - } - - if (scheduleConfigUpdateOptions.EndAt.HasValue) - { - this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); - } - - if (scheduleConfigUpdateOptions.Interval.HasValue) - { - this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; - updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); - } - - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) - { - this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; - updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); - } - - if (scheduleConfigUpdateOptions.MaxOccurrence != 0) - { - this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; - updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); - } - - // Only update if the customer explicitly set a value - if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) - { - this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); - } - - return updatedFields; - } - - // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully - // This could incur little overhead as ideally the runSchedule with old token should be killed immediately - // but there is no support to cancel pending entity operations currently, can be a todo item - public void RefreshScheduleRunExecutionToken() - { - this.ExecutionToken = Guid.NewGuid().ToString("N"); - } -} +public record ScheduleDescription( + string ScheduleId, + string OrchestrationName, + string? OrchestrationInput, + string? OrchestrationInstanceId, + DateTimeOffset? StartAt, + DateTimeOffset? EndAt, + TimeSpan? Interval, + string? CronExpression, + int MaxOccurrence, + bool? StartImmediatelyIfLate, + ScheduleStatus Status, + string ExecutionToken, + DateTimeOffset? LastRunAt, + DateTimeOffset? NextRunAt +); diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index b2e667f0..c7a23931 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -3,7 +3,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; -enum ScheduleStatus +public enum ScheduleStatus { Uninitialized, // Schedule has not been created Active, // Schedule is active and running diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/ScheduledTaskClient.cs index 71ff06e4..3a1a663a 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/ScheduledTaskClient.cs @@ -50,19 +50,9 @@ public async Task DeleteScheduleAsync(string scheduleId) } /// - public async Task GetScheduleAsync(string scheduleId) + public async Task GetScheduleAsync(string scheduleId) { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - - if (metadata == null || !metadata.IncludesState) - { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); - } - - return metadata.State; + throw new NotImplementedException(); } /// From 1e968d1b719c13692f736ef6590c112779bca531 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 07:48:05 -0800 Subject: [PATCH 029/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 43 +++++++++ .../Client/IScheduledTaskClient.cs | 29 ++++++ .../ScheduleHandle.cs} | 91 +++++++------------ .../Client/ScheduledTaskClient.cs | 60 ++++++++++++ src/ScheduledTasks/IScheduledTaskClient.cs | 58 ------------ .../Models/ScheduleDescription.cs | 3 +- .../{ => Models}/ScheduleTransitions.cs | 0 7 files changed, 167 insertions(+), 117 deletions(-) create mode 100644 src/ScheduledTasks/Client/IScheduleHandle.cs create mode 100644 src/ScheduledTasks/Client/IScheduledTaskClient.cs rename src/ScheduledTasks/{ScheduledTaskClient.cs => Client/ScheduleHandle.cs} (60%) create mode 100644 src/ScheduledTasks/Client/ScheduledTaskClient.cs delete mode 100644 src/ScheduledTasks/IScheduledTaskClient.cs rename src/ScheduledTasks/{ => Models}/ScheduleTransitions.cs (100%) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs new file mode 100644 index 00000000..b935aec6 --- /dev/null +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -0,0 +1,43 @@ +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents a handle to a schedule, allowing operations on a specific schedule instance. +/// +public interface IScheduleHandle +{ + /// + /// Gets the ID of the schedule. + /// + string ScheduleId { get; } + + /// + /// Retrieves the current details of this schedule. + /// + /// The schedule details. + Task GetAsync(); + + /// + /// Deletes this schedule. + /// + /// A task that completes when the schedule is deleted. + Task DeleteAsync(); + + /// + /// Pauses this schedule. + /// + /// A task that completes when the schedule is paused. + Task PauseAsync(); + + /// + /// Resumes this schedule. + /// + /// A task that completes when the schedule is resumed. + Task ResumeAsync(); + + /// + /// Updates this schedule with new configuration. + /// + /// The options for updating the schedule configuration. + /// A task that completes when the schedule is updated. + Task UpdateAsync(ScheduleUpdateOptions updateOptions); +} diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs new file mode 100644 index 00000000..4f84a621 --- /dev/null +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Interface for managing scheduled tasks in a Durable Task application. +/// +public interface IScheduledTaskClient +{ + /// + /// Gets a handle to a schedule, allowing operations on it. + /// + /// The ID of the schedule. + /// A handle to manage the schedule. + IScheduleHandle GetScheduleHandle(string scheduleId); + + /// + /// Gets a list of all schedules. + /// + /// A list of schedule descriptions. + Task> ListSchedulesAsync(); + + /// + /// Creates a new schedule with the specified configuration. + /// + /// The ID of the newly created schedule. + Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); +} diff --git a/src/ScheduledTasks/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs similarity index 60% rename from src/ScheduledTasks/ScheduledTaskClient.cs rename to src/ScheduledTasks/Client/ScheduleHandle.cs index 3a1a663a..8df3d362 100644 --- a/src/ScheduledTasks/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -2,79 +2,42 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.Entities; -using Microsoft.DurableTask.Entities; namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Client for managing scheduled tasks in a Durable Task application. +/// Represents a handle to a scheduled task, providing operations for managing the schedule. /// -public class ScheduledTaskClient : IScheduledTaskClient +public class ScheduleHandle : IScheduleHandle { - readonly DurableTaskClient durableTaskClient; + readonly DurableTaskClient client; /// - /// Initializes a new instance of the class. + /// Gets the ID of the schedule. /// - /// The Durable Task client to use for orchestration operations. - public ScheduledTaskClient(DurableTaskClient durableTaskClient) - { - this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); - } + public string ScheduleId { get; } - /// - public async Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions) + /// + /// Initializes a new instance of the class. + /// + /// The durable task client. + /// The ID of the schedule. + /// Thrown if or is null. + public ScheduleHandle(DurableTaskClient client, string scheduleId) { - Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); - - var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); - - return scheduleConfigCreateOptions.ScheduleId; + client = client ?? throw new ArgumentNullException(nameof(client)); + this.ScheduleId = scheduleId ?? throw new ArgumentNullException(nameof(scheduleId)); } /// - public async Task DeleteScheduleAsync(string scheduleId) + public Task GetAsync() { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata == null) - { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); - } - - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); + // 1. } - /// - public async Task GetScheduleAsync(string scheduleId) - { - throw new NotImplementedException(); - } /// - public async Task> ListSchedulesAsync() - { - var query = new EntityQuery - { - InstanceIdStartsWith = $"@{nameof(Schedule)}@", - IncludeState = false, - }; - - var scheduleIds = new List(); - await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) - { - scheduleIds.Add(metadata.Id.Key); - } - - return scheduleIds; - } - - /// - public async Task PauseScheduleAsync(string scheduleId) + public async Task PauseAsync() { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); @@ -89,7 +52,7 @@ public async Task PauseScheduleAsync(string scheduleId) } /// - public async Task ResumeScheduleAsync(string scheduleId) + public async Task ResumeAsync() { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); @@ -104,9 +67,8 @@ public async Task ResumeScheduleAsync(string scheduleId) } /// - public async Task UpdateScheduleAsync(string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions) + public async Task UpdateAsync(ScheduleUpdateOptions scheduleConfigurationUpdateOptions) { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); Check.NotNull(scheduleConfigurationUpdateOptions, nameof(scheduleConfigurationUpdateOptions)); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); @@ -132,4 +94,19 @@ public async Task UpdateScheduleAsync(string scheduleId, ScheduleUpdateOptions s await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); } + + /// + public async Task DeleteAsync() + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + } + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); + } } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs new file mode 100644 index 00000000..e50aea89 --- /dev/null +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Client for managing scheduled tasks in a Durable Task application. +/// +public class ScheduledTaskClient : IScheduledTaskClient +{ + readonly DurableTaskClient durableTaskClient; + + /// + /// Initializes a new instance of the class. + /// + /// The Durable Task client to use for orchestration operations. + public ScheduledTaskClient(DurableTaskClient durableTaskClient) + { + this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); + } + + /// + public IScheduleHandle GetScheduleHandle(string scheduleId) + { + return new ScheduleHandle(this.durableTaskClient, scheduleId); + } + + /// + public async Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions) + { + Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); + + var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); + + return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId); + } + + /// + public async Task> ListSchedulesAsync() + { + var query = new EntityQuery + { + InstanceIdStartsWith = $"@{nameof(Schedule)}@", + IncludeState = false, + }; + + var schedules = new List(); + await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + { + schedules.Add(new ScheduleDescription(metadata.Id.Key)); + } + + return schedules; + } +} diff --git a/src/ScheduledTasks/IScheduledTaskClient.cs b/src/ScheduledTasks/IScheduledTaskClient.cs deleted file mode 100644 index a20a8bad..00000000 --- a/src/ScheduledTasks/IScheduledTaskClient.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Interface for managing scheduled tasks in a Durable Task application. -/// -public interface IScheduledTaskClient -{ - /// - /// Gets the current state of a schedule. - /// - /// The ID of the schedule to retrieve. - /// The current state of the schedule. - Task GetScheduleAsync(string scheduleId); - - /// - /// Gets a list of all schedule IDs. - /// - /// A list of schedule IDs. - Task> ListSchedulesAsync(); - - /// - /// Creates a new schedule with the specified configuration. - /// - /// The ID of the newly created schedule. - Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); - - /// - /// Deletes an existing schedule. - /// - /// The ID of the schedule to delete. - /// A task that completes when the schedule is deleted. - Task DeleteScheduleAsync(string scheduleId); - - /// - /// Pauses an active schedule. - /// - /// The ID of the schedule to pause. - /// A task that completes when the schedule is paused. - Task PauseScheduleAsync(string scheduleId); - - /// - /// Resumes a paused schedule. - /// - /// The ID of the schedule to resume. - /// A task that completes when the schedule is resumed. - Task ResumeScheduleAsync(string scheduleId); - - /// - /// Updates an existing schedule with new configuration. - /// - /// The ID of the schedule to update. - /// The options for updating the schedule configuration. - /// A task that completes when the schedule is updated. - Task UpdateScheduleAsync(string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); -} diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 2ce27b9a..f4dfb267 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -20,5 +20,4 @@ public record ScheduleDescription( ScheduleStatus Status, string ExecutionToken, DateTimeOffset? LastRunAt, - DateTimeOffset? NextRunAt -); + DateTimeOffset? NextRunAt); diff --git a/src/ScheduledTasks/ScheduleTransitions.cs b/src/ScheduledTasks/Models/ScheduleTransitions.cs similarity index 100% rename from src/ScheduledTasks/ScheduleTransitions.cs rename to src/ScheduledTasks/Models/ScheduleTransitions.cs From a93af826b07adfede1ce725d7cf28c43d9e5a76d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 08:34:09 -0800 Subject: [PATCH 030/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 25 +++--- .../Client/ScheduledTaskClient.cs | 33 +++++-- .../Models/ScheduleCreationOptions.cs | 88 ++++++++++--------- 3 files changed, 88 insertions(+), 58 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 8df3d362..7722b699 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -5,6 +5,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; +// TODO: Validaiton + /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// @@ -35,7 +37,6 @@ public Task GetAsync() // 1. } - /// public async Task PauseAsync() { @@ -67,9 +68,9 @@ public async Task ResumeAsync() } /// - public async Task UpdateAsync(ScheduleUpdateOptions scheduleConfigurationUpdateOptions) + public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { - Check.NotNull(scheduleConfigurationUpdateOptions, nameof(scheduleConfigurationUpdateOptions)); + Check.NotNull(updateOptions, nameof(updateOptions)); var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); @@ -81,15 +82,15 @@ public async Task UpdateAsync(ScheduleUpdateOptions scheduleConfigurationUpdateO // Convert ScheduleConfiguration to ScheduleConfigurationUpdateOptions var updateOptions = new ScheduleUpdateOptions { - OrchestrationName = scheduleConfigurationUpdateOptions.OrchestrationName, - OrchestrationInput = scheduleConfigurationUpdateOptions.OrchestrationInput, - OrchestrationInstanceId = scheduleConfigurationUpdateOptions.OrchestrationInstanceId, - StartAt = scheduleConfigurationUpdateOptions.StartAt, - EndAt = scheduleConfigurationUpdateOptions.EndAt, - Interval = scheduleConfigurationUpdateOptions.Interval, - CronExpression = scheduleConfigurationUpdateOptions.CronExpression, - MaxOccurrence = scheduleConfigurationUpdateOptions.MaxOccurrence, - StartImmediatelyIfLate = scheduleConfigurationUpdateOptions.StartImmediatelyIfLate, + OrchestrationName = updateOptions.OrchestrationName, + OrchestrationInput = updateOptions.OrchestrationInput, + OrchestrationInstanceId = updateOptions.OrchestrationInstanceId, + StartAt = updateOptions.StartAt, + EndAt = updateOptions.EndAt, + Interval = updateOptions.Interval, + CronExpression = updateOptions.CronExpression, + MaxOccurrence = updateOptions.MaxOccurrence, + StartImmediatelyIfLate = updateOptions.StartImmediatelyIfLate, }; await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index e50aea89..acfef6cc 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -7,6 +7,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; +// TODO: validation + /// /// Client for managing scheduled tasks in a Durable Task application. /// @@ -34,7 +36,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s { Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId); @@ -43,16 +45,35 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s /// public async Task> ListSchedulesAsync() { - var query = new EntityQuery + EntityQuery query = new EntityQuery { - InstanceIdStartsWith = $"@{nameof(Schedule)}@", - IncludeState = false, + InstanceIdStartsWith = nameof(Schedule), // Automatically ensures correct formatting + IncludeState = true, }; - var schedules = new List(); + List schedules = new List(); + await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) { - schedules.Add(new ScheduleDescription(metadata.Id.Key)); + if (metadata.State is { ScheduleConfiguration: not null }) + { + ScheduleConfiguration config = metadata.State.ScheduleConfiguration; + schedules.Add(new ScheduleDescription( + metadata.Id.Key, + config.OrchestrationName, + config.OrchestrationInput, + config.OrchestrationInstanceId, + config.StartAt, + config.EndAt, + config.Interval, + config.CronExpression, + config.MaxOccurrence, + config.StartImmediatelyIfLate, + metadata.State.Status, + metadata.State.ExecutionToken, + metadata.State.LastRunAt, + metadata.State.NextRunAt)); + } } return schedules; diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index adc2ec64..cd05ff54 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -6,69 +6,77 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Configuration for a scheduled task. /// -public class ScheduleCreationOptions +public record ScheduleCreationOptions { /// - /// Initializes a new instance of the class. + /// The name of the orchestration function to schedule. /// - /// - /// - public ScheduleCreationOptions(string orchestrationName, string scheduleId) - { - this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); - this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); - } - - string orchestrationName; + public string OrchestrationName { get; init; } - public string OrchestrationName + /// + /// Initializes a new instance of the class. + /// + /// The name of the orchestration function to schedule. + /// Thrown when is null or empty. + public ScheduleCreationOptions(string orchestrationName) { - get => this.orchestrationName; - set - { - this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } + Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.OrchestrationName = orchestrationName; } - public string ScheduleId { get; init; } - - public string? OrchestrationInput { get; set; } + /// + /// The ID of the schedule, if not provided, default to a new GUID. + /// + public string ScheduleId { get; init; } = Guid.NewGuid().ToString("N"); - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + /// + /// The input to the orchestration function. + /// + public string? OrchestrationInput { get; init; } - public DateTimeOffset? StartAt { get; set; } + /// + /// The instance ID of the orchestration function, if not provided, default to a new GUID. + /// + public string OrchestrationInstanceId { get; init; } = Guid.NewGuid().ToString("N"); - public DateTimeOffset? EndAt { get; set; } + /// + /// The start time of the schedule. + /// + public DateTimeOffset? StartAt { get; init; } - TimeSpan? interval; + /// + /// The end time of the schedule. + /// + public DateTimeOffset? EndAt { get; init; } + /// + /// The interval of the schedule. + /// public TimeSpan? Interval { get => this.interval; - set + init { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) + if (value.HasValue) { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } } this.interval = value; } } - public string? CronExpression { get; set; } + // public string? CronExpression { get; init; } - public int MaxOccurrence { get; set; } + // public int MaxOccurrence { get; init; } - public bool? StartImmediatelyIfLate { get; set; } + // public bool? StartImmediatelyIfLate { get; init; } } From 588da820889dd6ab29bfc54541f092fd5a489323 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 08:45:17 -0800 Subject: [PATCH 031/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 70 +++++++++++-------- .../Models/ScheduleCreationOptions.cs | 23 +++--- src/ScheduledTasks/Schedule.cs | 10 +-- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 7722b699..37a6e628 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -2,17 +2,20 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; namespace Microsoft.DurableTask.ScheduledTasks; // TODO: Validaiton +// TODO: GET if config is null what to return /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// public class ScheduleHandle : IScheduleHandle { - readonly DurableTaskClient client; + readonly DurableTaskClient durableTaskClient; /// /// Gets the ID of the schedule. @@ -32,21 +35,46 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId) } /// - public Task GetAsync() + public async Task GetAsync() { - // 1. + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); + + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata == null) + { + throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + } + + ScheduleState state = metadata.State; + ScheduleConfiguration? config = state.ScheduleConfiguration; + return new ScheduleDescription( + this.ScheduleId, + config.OrchestrationName, + config.OrchestrationInput, + config.OrchestrationInstanceId, + config.StartAt, + config.EndAt, + config.Interval, + config.CronExpression, + config.MaxOccurrence, + config.StartImmediatelyIfLate, + state.Status, + state.ExecutionToken, + state.LastRunAt, + state.NextRunAt); } /// public async Task PauseAsync() { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.PauseSchedule)); @@ -55,13 +83,13 @@ public async Task PauseAsync() /// public async Task ResumeAsync() { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.ResumeSchedule)); @@ -72,40 +100,26 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { Check.NotNull(updateOptions, nameof(updateOptions)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); } - // Convert ScheduleConfiguration to ScheduleConfigurationUpdateOptions - var updateOptions = new ScheduleUpdateOptions - { - OrchestrationName = updateOptions.OrchestrationName, - OrchestrationInput = updateOptions.OrchestrationInput, - OrchestrationInstanceId = updateOptions.OrchestrationInstanceId, - StartAt = updateOptions.StartAt, - EndAt = updateOptions.EndAt, - Interval = updateOptions.Interval, - CronExpression = updateOptions.CronExpression, - MaxOccurrence = updateOptions.MaxOccurrence, - StartImmediatelyIfLate = updateOptions.StartImmediatelyIfLate, - }; - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); } /// public async Task DeleteAsync() { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {scheduleId} does not exist."); + throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index cd05ff54..de21b176 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -9,7 +9,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; public record ScheduleCreationOptions { /// - /// The name of the orchestration function to schedule. + /// Gets the name of the orchestration function to schedule. /// public string OrchestrationName { get; init; } @@ -25,33 +25,38 @@ public ScheduleCreationOptions(string orchestrationName) } /// - /// The ID of the schedule, if not provided, default to a new GUID. + /// Gets the ID of the schedule, if not provided, default to a new GUID. /// public string ScheduleId { get; init; } = Guid.NewGuid().ToString("N"); /// - /// The input to the orchestration function. + /// Gets the input to the orchestration function. /// public string? OrchestrationInput { get; init; } /// - /// The instance ID of the orchestration function, if not provided, default to a new GUID. + /// Gets the instance ID of the orchestration function, if not provided, default to a new GUID. /// public string OrchestrationInstanceId { get; init; } = Guid.NewGuid().ToString("N"); /// - /// The start time of the schedule. + /// Gets the start time of the schedule. /// public DateTimeOffset? StartAt { get; init; } /// - /// The end time of the schedule. + /// Gets the end time of the schedule. /// public DateTimeOffset? EndAt { get; init; } /// /// The interval of the schedule. /// + TimeSpan? interval; + + /// + /// Gets the interval of the schedule. + /// public TimeSpan? Interval { get => this.interval; @@ -74,9 +79,9 @@ public TimeSpan? Interval } } - // public string? CronExpression { get; init; } + public string? CronExpression { get; init; } - // public int MaxOccurrence { get; init; } + public int MaxOccurrence { get; init; } - // public bool? StartImmediatelyIfLate { get; init; } + public bool? StartImmediatelyIfLate { get; init; } } diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index c8f2d079..686e8dc6 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -49,17 +49,17 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc /// Updates an existing schedule. /// /// The task entity context. - /// The options for updating the schedule configuration. + /// The options for updating the schedule configuration. /// Thrown when scheduleConfigUpdateOptions is null. /// Thrown when the schedule is not created. - public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions scheduleConfigUpdateOptions) + public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions scheduleUpdateOptions) { - Verify.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); + Verify.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.logger.UpdatingSchedule(this.State.ScheduleConfiguration.ScheduleId, scheduleConfigUpdateOptions); + this.logger.UpdatingSchedule(this.State.ScheduleConfiguration.ScheduleId, scheduleUpdateOptions); - HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleConfigUpdateOptions); + HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleUpdateOptions); if (updatedScheduleConfigFields.Count == 0) { // no need to interrupt and update current schedule run as there is no change in the schedule config From 37b4f4794c551a1da373df873d6a48b7ab511fbb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:02:07 -0800 Subject: [PATCH 032/203] save --- src/ScheduledTasks/DurableTaskClientExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs index e09a8bdf..87dbebdb 100644 --- a/src/ScheduledTasks/DurableTaskClientExtensions.cs +++ b/src/ScheduledTasks/DurableTaskClientExtensions.cs @@ -17,6 +17,8 @@ public static class DurableTaskClientExtensions /// A client for managing scheduled tasks. public static ScheduledTaskClient ScheduledTasks(this DurableTaskClient client) { + // ScheduledTaskClient is not a resource-intensive object, we shall create a new instance to avoid + // any potential thread safety issues. return new ScheduledTaskClient(client); } } From 6bdbc58f2d1fa0c88d2f759286c5188a14a347e1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:12:51 -0800 Subject: [PATCH 033/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 2 +- .../Client/IScheduledTaskClient.cs | 6 ++- src/ScheduledTasks/Client/ScheduleHandle.cs | 27 +++++++++---- .../Client/ScheduledTaskClient.cs | 12 +++++- .../ScheduleAlreadyExistException.cs | 36 ++++++++++++++++++ .../Exception/ScheduleInternalException.cs | 38 +++++++++++++++++++ .../Exception/ScheduleNotFoundException.cs | 36 ++++++++++++++++++ .../ScheduleStillBeingProvisionedException.cs | 36 ++++++++++++++++++ src/ScheduledTasks/Logs.cs | 1 - src/ScheduledTasks/Schedule.cs | 8 ++-- 10 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs create mode 100644 src/ScheduledTasks/Exception/ScheduleInternalException.cs create mode 100644 src/ScheduledTasks/Exception/ScheduleNotFoundException.cs create mode 100644 src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index b935aec6..8c47ed38 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -14,7 +14,7 @@ public interface IScheduleHandle /// Retrieves the current details of this schedule. /// /// The schedule details. - Task GetAsync(); + Task DescribeAsync(); /// /// Deletes this schedule. diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 4f84a621..326a55ca 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -16,14 +16,16 @@ public interface IScheduledTaskClient IScheduleHandle GetScheduleHandle(string scheduleId); /// - /// Gets a list of all schedules. + /// Gets a list of all initialized schedules. /// /// A list of schedule descriptions. - Task> ListSchedulesAsync(); + Task> ListInitializedSchedulesAsync(); /// /// Creates a new schedule with the specified configuration. /// /// The ID of the newly created schedule. Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); + + // TODO: list uninitialized schedules? } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 37a6e628..d2931fb5 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -35,19 +35,32 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId) } /// - public async Task GetAsync() + public async Task DescribeAsync() { Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + EntityMetadata? metadata = + await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + throw new ScheduleNotFoundException(this.ScheduleId); } ScheduleState state = metadata.State; + if (state.Status == ScheduleStatus.Uninitialized) + { + throw new ScheduleStillBeingProvisionedException(this.ScheduleId); + } + + // this should never happen ScheduleConfiguration? config = state.ScheduleConfiguration; + if (config == null) + { + throw new ScheduleInternalException(this.ScheduleId, + $"Schedule configuration is not available even though the schedule status is {state.Status}."); + } + return new ScheduleDescription( this.ScheduleId, config.OrchestrationName, @@ -74,7 +87,7 @@ public async Task PauseAsync() var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + throw new ScheduleNotFoundException(this.ScheduleId); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.PauseSchedule)); @@ -89,7 +102,7 @@ public async Task ResumeAsync() var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + throw new ScheduleNotFoundException(this.ScheduleId); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.ResumeSchedule)); @@ -104,7 +117,7 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + throw new ScheduleNotFoundException(this.ScheduleId); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); @@ -119,7 +132,7 @@ public async Task DeleteAsync() var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { - throw new InvalidOperationException($"Schedule with ID {this.ScheduleId} does not exist."); + throw new ScheduleNotFoundException(this.ScheduleId); } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index acfef6cc..5a0a5245 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -37,13 +37,21 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); + + // Check if schedule already exists + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata != null) + { + throw new ScheduleAlreadyExistException(scheduleConfigCreateOptions.ScheduleId); + } + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId); } /// - public async Task> ListSchedulesAsync() + public async Task> ListInitializedSchedulesAsync() { EntityQuery query = new EntityQuery { @@ -55,7 +63,7 @@ public async Task> ListSchedulesAsync() await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) { - if (metadata.State is { ScheduleConfiguration: not null }) + if (metadata.State.Status != ScheduleStatus.Uninitialized) { ScheduleConfiguration config = metadata.State.ScheduleConfiguration; schedules.Add(new ScheduleDescription( diff --git a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs new file mode 100644 index 00000000..41f8ce7a --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when attempting to create a schedule with an ID that already exists. +/// +public class ScheduleAlreadyExistException : Exception +{ + /// + /// Gets the ID of the schedule that already exists. + /// + public string ScheduleId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that already exists. + public ScheduleAlreadyExistException(string scheduleId) + : base($"A schedule with ID '{scheduleId}' already exists.") + { + this.ScheduleId = scheduleId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that already exists. + /// The exception that is the cause of the current exception. + public ScheduleAlreadyExistException(string scheduleId, Exception innerException) + : base($"A schedule with ID '{scheduleId}' already exists.", innerException) + { + this.ScheduleId = scheduleId; + } +} diff --git a/src/ScheduledTasks/Exception/ScheduleInternalException.cs b/src/ScheduledTasks/Exception/ScheduleInternalException.cs new file mode 100644 index 00000000..225ea41e --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleInternalException.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when an internal server error occurs while processing a schedule. +/// +public class ScheduleInternalException : Exception +{ + /// + /// Gets the ID of the schedule that encountered the internal error. + /// + public string ScheduleId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that encountered the error. + /// The error message that explains the reason for the exception. + public ScheduleInternalException(string scheduleId, string message) + : base($"An internal error occurred while processing schedule '{scheduleId}': {message}") + { + this.ScheduleId = scheduleId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that encountered the error. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public ScheduleInternalException(string scheduleId, string message, Exception innerException) + : base($"An internal error occurred while processing schedule '{scheduleId}': {message}", innerException) + { + this.ScheduleId = scheduleId; + } +} diff --git a/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs new file mode 100644 index 00000000..44ea62ea --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when attempting to access a schedule that does not exist. +/// +public class ScheduleNotFoundException : Exception +{ + /// + /// Gets the ID of the schedule that was not found. + /// + public string ScheduleId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that was not found. + public ScheduleNotFoundException(string scheduleId) + : base($"Schedule with ID '{scheduleId}' was not found.") + { + this.ScheduleId = scheduleId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that was not found. + /// The exception that is the cause of the current exception. + public ScheduleNotFoundException(string scheduleId, Exception innerException) + : base($"Schedule with ID '{scheduleId}' was not found.", innerException) + { + this.ScheduleId = scheduleId; + } +} diff --git a/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs b/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs new file mode 100644 index 00000000..3c726307 --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when attempting to perform an operation on a schedule that is still being provisioned. +/// +public class ScheduleStillBeingProvisionedException : Exception +{ + /// + /// Gets the ID of the schedule that is still being provisioned. + /// + public string ScheduleId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that is still being provisioned. + public ScheduleStillBeingProvisionedException(string scheduleId) + : base($"Schedule '{scheduleId}' is still being provisioned.") + { + this.ScheduleId = scheduleId; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that is still being provisioned. + /// The exception that is the cause of the current exception. + public ScheduleStillBeingProvisionedException(string scheduleId, Exception innerException) + : base($"Schedule '{scheduleId}' is still being provisioned.", innerException) + { + this.ScheduleId = scheduleId; + } +} diff --git a/src/ScheduledTasks/Logs.cs b/src/ScheduledTasks/Logs.cs index 163b8b59..92225f73 100644 --- a/src/ScheduledTasks/Logs.cs +++ b/src/ScheduledTasks/Logs.cs @@ -24,7 +24,6 @@ static partial class Logs [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] public static partial void ResumingSchedule(this ILogger logger, string scheduleId); - // run schedule logging [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Running schedule '{scheduleId}'")] public static partial void RunningSchedule(this ILogger logger, string scheduleId); diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Schedule.cs index 686e8dc6..e1f0c2b8 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Schedule.cs @@ -25,19 +25,19 @@ class Schedule(ILogger logger) : TaskEntity /// Creates a new schedule with the specified configuration. /// /// The task entity context. - /// The configuration options for creating the schedule. + /// The configuration options for creating the schedule. /// Thrown when scheduleConfigurationCreateOptions is null. /// Thrown when the schedule is already created. - public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions scheduleConfigurationCreateOptions) + public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions scheduleCreationOptions) { - Verify.NotNull(scheduleConfigurationCreateOptions, nameof(scheduleConfigurationCreateOptions)); + Verify.NotNull(scheduleCreationOptions, nameof(scheduleCreationOptions)); if (this.State.Status != ScheduleStatus.Uninitialized) { throw new InvalidOperationException("Schedule is already created."); } - this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleConfigurationCreateOptions); + this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); this.TryStatusTransition(ScheduleStatus.Active); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately From 2f0e09944efe3ac9757c2c956f857bd058d5a721 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:53:23 -0800 Subject: [PATCH 034/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 5 ++ .../Client/ScheduledTaskClient.cs | 3 + src/ScheduledTasks/{ => Entity}/Schedule.cs | 36 +++++++---- src/ScheduledTasks/Logging/Client/Logs.cs | 40 +++++++++++++ src/ScheduledTasks/Logging/Entity/Logs.cs | 59 +++++++++++++++++++ src/ScheduledTasks/Logs.cs | 32 ---------- 6 files changed, 133 insertions(+), 42 deletions(-) rename src/ScheduledTasks/{ => Entity}/Schedule.cs (82%) create mode 100644 src/ScheduledTasks/Logging/Client/Logs.cs create mode 100644 src/ScheduledTasks/Logging/Entity/Logs.cs delete mode 100644 src/ScheduledTasks/Logs.cs diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index d2931fb5..08165b5d 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -37,6 +37,7 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId) /// public async Task DescribeAsync() { + this.logger.HandleDescribingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -81,6 +82,7 @@ public async Task DescribeAsync() /// public async Task PauseAsync() { + this.logger.HandlePausingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -96,6 +98,7 @@ public async Task PauseAsync() /// public async Task ResumeAsync() { + this.logger.HandleResumingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -111,6 +114,7 @@ public async Task ResumeAsync() /// public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { + this.logger.HandleUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -126,6 +130,7 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) /// public async Task DeleteAsync() { + this.logger.HandleDeletingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 5a0a5245..fa2aa7c3 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -28,12 +28,14 @@ public ScheduledTaskClient(DurableTaskClient durableTaskClient) /// public IScheduleHandle GetScheduleHandle(string scheduleId) { + this.logger.ClientGettingScheduleHandle(scheduleId); return new ScheduleHandle(this.durableTaskClient, scheduleId); } /// public async Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions) { + this.logger.ClientCreatingSchedule(scheduleConfigCreateOptions); Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); @@ -53,6 +55,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s /// public async Task> ListInitializedSchedulesAsync() { + this.logger.ClientListingSchedules(); EntityQuery query = new EntityQuery { InstanceIdStartsWith = nameof(Schedule), // Automatically ensures correct formatting diff --git a/src/ScheduledTasks/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs similarity index 82% rename from src/ScheduledTasks/Schedule.cs rename to src/ScheduledTasks/Entity/Schedule.cs index e1f0c2b8..6aa180b1 100644 --- a/src/ScheduledTasks/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -34,12 +34,17 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (this.State.Status != ScheduleStatus.Uninitialized) { - throw new InvalidOperationException("Schedule is already created."); + string errorMessage = "Schedule is already created."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); + throw exception; } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); this.TryStatusTransition(ScheduleStatus.Active); + this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); @@ -57,13 +62,11 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche Verify.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.logger.UpdatingSchedule(this.State.ScheduleConfiguration.ScheduleId, scheduleUpdateOptions); - HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleUpdateOptions); if (updatedScheduleConfigFields.Count == 0) { // no need to interrupt and update current schedule run as there is no change in the schedule config - this.logger.LogInformation("Schedule configuration is up to date."); + this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration.ScheduleId, nameof(this.UpdateSchedule), "Schedule configuration is up to date."); return; } @@ -85,6 +88,8 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche this.State.RefreshScheduleRunExecutionToken(); + this.logger.UpdatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); @@ -105,7 +110,7 @@ public void PauseSchedule() this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); - this.logger.LogInformation("Schedule paused."); + this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); } /// @@ -118,12 +123,15 @@ public void ResumeSchedule(TaskEntityContext context) Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); if (this.State.Status != ScheduleStatus.Paused) { - throw new InvalidOperationException("Schedule must be in Paused state to resume."); + string errorMessage = "Schedule must be in Paused state to resume."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.ResumeSchedule), errorMessage, exception); + throw exception; } this.TryStatusTransition(ScheduleStatus.Active); this.State.NextRunAt = null; - this.logger.LogInformation("Schedule resumed."); + this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); // compute next run based on startat and interval context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); @@ -141,21 +149,28 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { + this.logger.RunningSchedule(this.State.ScheduleConfiguration.ScheduleId); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); if (this.State.ScheduleConfiguration.Interval == null) { - throw new InvalidOperationException("Schedule interval must be specified."); + string errorMessage = "Schedule interval must be specified."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); + throw exception; } if (executionToken != this.State.ExecutionToken) { - this.logger.LogInformation("Cancel schedule run - execution token {token} has expired", executionToken); + this.logger.ScheduleRunCancelled(this.State.ScheduleConfiguration.ScheduleId, executionToken); return; } if (this.State.Status != ScheduleStatus.Active) { - throw new InvalidOperationException("Schedule must be in Active status to run."); + string errorMessage = "Schedule must be in Active status to run."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); + throw exception; } // run schedule based on next run at @@ -191,6 +206,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; } + this.logger.CompletedScheduleRun(this.State.ScheduleConfiguration.ScheduleId); context.SignalEntity( new EntityInstanceId( nameof(Schedule), diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs new file mode 100644 index 00000000..e33cd2a4 --- /dev/null +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Log messages. +/// +/// +/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. +/// +// TODO: Do we really need all of these? +static partial class Logs +{ + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Client: Creating schedule with options: {scheduleConfigCreateOptions}")] + static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); + + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Client: Getting schedule handle for schedule '{scheduleId}'")] + static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Client: Listing initialized schedules")] + static partial void ClientListingSchedules(this ILogger logger); + + [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Client: Describing schedule '{scheduleId}'")] + static partial void ClientDescribingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Client: Pausing schedule '{scheduleId}'")] + static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Client: Resuming schedule '{scheduleId}'")] + static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Client: Updating schedule '{scheduleId}'")] + static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Client: Deleting schedule '{scheduleId}'")] + static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); +} diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Entity/Logs.cs new file mode 100644 index 00000000..f8433765 --- /dev/null +++ b/src/ScheduledTasks/Logging/Entity/Logs.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; +/// +/// Log messages. +/// +/// +/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. +/// +static partial class Logs +{ + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Schedule is being created with options: {scheduleConfigurationCreateOptions}")] + static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); + + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is created")] + static partial void CreatedSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being updated with options: {scheduleConfigurationUpdateOptions}")] + static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); + + [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is updated")] + static partial void UpdatedSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being paused")] + static partial void PausingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is paused")] + static partial void PausedSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being resumed")] + static partial void ResumingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is resumed")] + static partial void ResumedSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is running")] + static partial void RunningSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 10, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is executed")] + static partial void CompletedScheduleRun(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 11, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being deleted")] + static partial void DeletingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 12, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is deleted")] + static partial void DeletedSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 13, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] + static partial void ScheduleOperationInfo(this ILogger logger, string scheduleId, string operationName, string infoMessage); + + [LoggerMessage(EventId = 14, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] + static partial void ScheduleOperationWarning(this ILogger logger, string scheduleId, string operationName, string warningMessage); + + [LoggerMessage(EventId = 15, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] + static partial void ScheduleOperationError(this ILogger logger, string scheduleId, string operationName, string errorMessage, Exception? exception = null); +} \ No newline at end of file diff --git a/src/ScheduledTasks/Logs.cs b/src/ScheduledTasks/Logs.cs deleted file mode 100644 index 92225f73..00000000 --- a/src/ScheduledTasks/Logs.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; - -namespace Microsoft.DurableTask.ScheduledTasks; -/// -/// Log messages. -/// -/// -/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. -/// -static partial class Logs -{ - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigurationCreateOptions}")] - public static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); - - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}' with options: {scheduleConfigurationUpdateOptions}")] - public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); - - [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] - public static partial void PausingSchedule(this ILogger logger, string scheduleId); - - [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] - public static partial void ResumingSchedule(this ILogger logger, string scheduleId); - - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Running schedule '{scheduleId}'")] - public static partial void RunningSchedule(this ILogger logger, string scheduleId); - - [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] - public static partial void DeletingSchedule(this ILogger logger, string scheduleId); -} \ No newline at end of file From 4ef7640d83c981f846327171093a988d81fe4cd3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:07:05 -0800 Subject: [PATCH 035/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 23 ++++++++----- .../Client/ScheduledTaskClient.cs | 10 ++++-- .../DurableTaskClientExtensions.cs | 6 ++-- src/ScheduledTasks/Logging/Client/Logs.cs | 16 ++++----- src/ScheduledTasks/Logging/Entity/Logs.cs | 33 ++++++++++--------- 5 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 08165b5d..60c87d6a 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -4,6 +4,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -16,6 +17,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; public class ScheduleHandle : IScheduleHandle { readonly DurableTaskClient durableTaskClient; + readonly ILogger logger; /// /// Gets the ID of the schedule. @@ -27,21 +29,23 @@ public class ScheduleHandle : IScheduleHandle /// /// The durable task client. /// The ID of the schedule. + /// /// Thrown if or is null. - public ScheduleHandle(DurableTaskClient client, string scheduleId) + public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logger) { client = client ?? throw new ArgumentNullException(nameof(client)); this.ScheduleId = scheduleId ?? throw new ArgumentNullException(nameof(scheduleId)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task DescribeAsync() { - this.logger.HandleDescribingSchedule(this.ScheduleId); + this.logger.ClientDescribingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { @@ -58,8 +62,9 @@ public async Task DescribeAsync() ScheduleConfiguration? config = state.ScheduleConfiguration; if (config == null) { - throw new ScheduleInternalException(this.ScheduleId, - $"Schedule configuration is not available even though the schedule status is {state.Status}."); + throw new ScheduleInternalException( + this.ScheduleId, + $"Schedule configuration is not available even though the schedule status is {state.Status}."); } return new ScheduleDescription( @@ -82,7 +87,7 @@ public async Task DescribeAsync() /// public async Task PauseAsync() { - this.logger.HandlePausingSchedule(this.ScheduleId); + this.logger.ClientPausingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -98,7 +103,7 @@ public async Task PauseAsync() /// public async Task ResumeAsync() { - this.logger.HandleResumingSchedule(this.ScheduleId); + this.logger.ClientResumingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -114,7 +119,7 @@ public async Task ResumeAsync() /// public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { - this.logger.HandleUpdatingSchedule(this.ScheduleId); + this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); @@ -130,7 +135,7 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) /// public async Task DeleteAsync() { - this.logger.HandleDeletingSchedule(this.ScheduleId); + this.logger.ClientDeletingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index fa2aa7c3..89a8a65e 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -4,6 +4,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -15,21 +16,24 @@ namespace Microsoft.DurableTask.ScheduledTasks; public class ScheduledTaskClient : IScheduledTaskClient { readonly DurableTaskClient durableTaskClient; + readonly ILogger logger; /// /// Initializes a new instance of the class. /// /// The Durable Task client to use for orchestration operations. - public ScheduledTaskClient(DurableTaskClient durableTaskClient) + /// + public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) { this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); + this.logger = Check.NotNull(logger, nameof(logger)); } /// public IScheduleHandle GetScheduleHandle(string scheduleId) { this.logger.ClientGettingScheduleHandle(scheduleId); - return new ScheduleHandle(this.durableTaskClient, scheduleId); + return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); } /// @@ -49,7 +53,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); - return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId); + return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); } /// diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs index 87dbebdb..fd505e04 100644 --- a/src/ScheduledTasks/DurableTaskClientExtensions.cs +++ b/src/ScheduledTasks/DurableTaskClientExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -14,11 +15,12 @@ public static class DurableTaskClientExtensions /// Gets a client for working with scheduled tasks. /// /// The DurableTaskClient instance. + /// logger for ScheduledTaskClient. /// A client for managing scheduled tasks. - public static ScheduledTaskClient ScheduledTasks(this DurableTaskClient client) + public static ScheduledTaskClient ScheduledTasks(this DurableTaskClient client, ILogger logger) { // ScheduledTaskClient is not a resource-intensive object, we shall create a new instance to avoid // any potential thread safety issues. - return new ScheduledTaskClient(client); + return new ScheduledTaskClient(client, logger); } } diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index e33cd2a4..7b9e1500 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -15,26 +15,26 @@ namespace Microsoft.DurableTask.ScheduledTasks; static partial class Logs { [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Client: Creating schedule with options: {scheduleConfigCreateOptions}")] - static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); + public static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Client: Getting schedule handle for schedule '{scheduleId}'")] - static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); + public static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Client: Listing initialized schedules")] - static partial void ClientListingSchedules(this ILogger logger); + public static partial void ClientListingSchedules(this ILogger logger); [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Client: Describing schedule '{scheduleId}'")] - static partial void ClientDescribingSchedule(this ILogger logger, string scheduleId); + public static partial void ClientDescribingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Client: Pausing schedule '{scheduleId}'")] - static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); + public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Client: Resuming schedule '{scheduleId}'")] - static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); + public static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Client: Updating schedule '{scheduleId}'")] - static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); + public static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Client: Deleting schedule '{scheduleId}'")] - static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); + public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); } diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Entity/Logs.cs index f8433765..b9bb8f88 100644 --- a/src/ScheduledTasks/Logging/Entity/Logs.cs +++ b/src/ScheduledTasks/Logging/Entity/Logs.cs @@ -13,47 +13,50 @@ namespace Microsoft.DurableTask.ScheduledTasks; static partial class Logs { [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Schedule is being created with options: {scheduleConfigurationCreateOptions}")] - static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); + public static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is created")] - static partial void CreatedSchedule(this ILogger logger, string scheduleId); + public static partial void CreatedSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being updated with options: {scheduleConfigurationUpdateOptions}")] - static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); + public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is updated")] - static partial void UpdatedSchedule(this ILogger logger, string scheduleId); + public static partial void UpdatedSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being paused")] - static partial void PausingSchedule(this ILogger logger, string scheduleId); + public static partial void PausingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is paused")] - static partial void PausedSchedule(this ILogger logger, string scheduleId); + public static partial void PausedSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being resumed")] - static partial void ResumingSchedule(this ILogger logger, string scheduleId); + public static partial void ResumingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is resumed")] - static partial void ResumedSchedule(this ILogger logger, string scheduleId); + public static partial void ResumedSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is running")] - static partial void RunningSchedule(this ILogger logger, string scheduleId); + public static partial void RunningSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 10, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is executed")] - static partial void CompletedScheduleRun(this ILogger logger, string scheduleId); + public static partial void CompletedScheduleRun(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 11, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being deleted")] - static partial void DeletingSchedule(this ILogger logger, string scheduleId); + public static partial void DeletingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 12, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is deleted")] - static partial void DeletedSchedule(this ILogger logger, string scheduleId); + public static partial void DeletedSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 13, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] - static partial void ScheduleOperationInfo(this ILogger logger, string scheduleId, string operationName, string infoMessage); + public static partial void ScheduleOperationInfo(this ILogger logger, string scheduleId, string operationName, string infoMessage); [LoggerMessage(EventId = 14, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] - static partial void ScheduleOperationWarning(this ILogger logger, string scheduleId, string operationName, string warningMessage); + public static partial void ScheduleOperationWarning(this ILogger logger, string scheduleId, string operationName, string warningMessage); [LoggerMessage(EventId = 15, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] - static partial void ScheduleOperationError(this ILogger logger, string scheduleId, string operationName, string errorMessage, Exception? exception = null); + public static partial void ScheduleOperationError(this ILogger logger, string scheduleId, string operationName, string errorMessage, Exception? exception = null); + + [LoggerMessage(EventId = 16, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] + public static partial void ScheduleRunCancelled(this ILogger logger, string scheduleId, string executionToken); } \ No newline at end of file From c6ca883d1b89c43b7a34a79e7d211507c48b009d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:12:59 -0800 Subject: [PATCH 036/203] save --- samples/ScheduleDemo/ScheduleConfiguration.cs | 58 ------------------- samples/ScheduleDemo/ScheduleDemo.csproj | 8 ++- src/ScheduledTasks/Client/ScheduleHandle.cs | 2 +- .../Client/ScheduledTaskClient.cs | 2 +- 4 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 samples/ScheduleDemo/ScheduleConfiguration.cs diff --git a/samples/ScheduleDemo/ScheduleConfiguration.cs b/samples/ScheduleDemo/ScheduleConfiguration.cs deleted file mode 100644 index 142b0f57..00000000 --- a/samples/ScheduleDemo/ScheduleConfiguration.cs +++ /dev/null @@ -1,58 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -// class ScheduleConfiguration -// { -// public ScheduleConfiguration(string orchestrationName, string scheduleId) -// { -// this.OrchestrationName = orchestrationName; -// this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); -// this.Version++; -// } - -// public string OrchestrationName { get; set; } - -// public string ScheduleId { get; set; } - -// public string? OrchestrationInput { get; set; } - -// public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); - -// public DateTimeOffset? StartAt { get; set; } - -// public DateTimeOffset? EndAt { get; set; } - -// TimeSpan? interval; - -// public TimeSpan? Interval -// { -// get => this.interval; -// set -// { -// if (!value.HasValue) -// { -// return; -// } - -// if (value.Value <= TimeSpan.Zero) -// { -// throw new ArgumentException("Interval must be positive", nameof(value)); -// } - -// if (value.Value.TotalSeconds < 1) -// { -// throw new ArgumentException("Interval must be at least 1 second", nameof(value)); -// } - -// this.interval = value; -// } -// } - -// public string? CronExpression { get; set; } - -// public int MaxOccurrence { get; set; } - -// public bool? StartImmediatelyIfLate { get; set; } - -// internal int Version { get; set; } // Tracking schedule config version -// } \ No newline at end of file diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index 45dd1ae3..48313277 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -1,4 +1,4 @@ - + + + + + diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 60c87d6a..41cb91fa 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -29,7 +29,7 @@ public class ScheduleHandle : IScheduleHandle /// /// The durable task client. /// The ID of the schedule. - /// + /// The logger. /// Thrown if or is null. public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logger) { diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 89a8a65e..94bf72a6 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -22,7 +22,7 @@ public class ScheduledTaskClient : IScheduledTaskClient /// Initializes a new instance of the class. /// /// The Durable Task client to use for orchestration operations. - /// + /// logger. public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) { this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); From 878e658154dcd8efe6611a1af1606e10c266c69f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:15:36 -0800 Subject: [PATCH 037/203] save --- .../DurableTaskClientBuilderExtensions.cs | 31 +++++++++++++++++++ .../DurableTaskClientExtensions.cs | 26 ---------------- .../DurableTaskSchedulerWorkerExtensions.cs | 2 +- src/ScheduledTasks/Entity/Schedule.cs | 2 +- 4 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs delete mode 100644 src/ScheduledTasks/DurableTaskClientExtensions.cs diff --git a/src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs new file mode 100644 index 00000000..22881441 --- /dev/null +++ b/src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Extension methods for configuring Durable Task clients to use scheduled tasks. +/// +public static class DurableTaskClientBuilderExtensions +{ + /// + /// Enables scheduled task support for the client builder. + /// + /// The client builder to add scheduled task support to. + /// The original builder, for call chaining. + public static IDurableTaskClientBuilder EnableScheduledTasksSupport(this IDurableTaskClientBuilder builder) + { + builder.Services.AddTransient(sp => + { + DurableTaskClient client = sp.GetRequiredService(); + ILogger logger = sp.GetRequiredService>(); + return new ScheduledTaskClient(client, logger); + }); + + return builder; + } +} diff --git a/src/ScheduledTasks/DurableTaskClientExtensions.cs b/src/ScheduledTasks/DurableTaskClientExtensions.cs deleted file mode 100644 index fd505e04..00000000 --- a/src/ScheduledTasks/DurableTaskClientExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.DurableTask.Client; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Extension methods for working with . -/// -public static class DurableTaskClientExtensions -{ - /// - /// Gets a client for working with scheduled tasks. - /// - /// The DurableTaskClient instance. - /// logger for ScheduledTaskClient. - /// A client for managing scheduled tasks. - public static ScheduledTaskClient ScheduledTasks(this DurableTaskClient client, ILogger logger) - { - // ScheduledTaskClient is not a resource-intensive object, we shall create a new instance to avoid - // any potential thread safety issues. - return new ScheduledTaskClient(client, logger); - } -} diff --git a/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs index c1f64a2d..b7253d1a 100644 --- a/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs @@ -14,7 +14,7 @@ public static class DurableTaskSchedulerWorkerExtensions /// Adds scheduled task support to the worker builder. /// /// The worker builder to add scheduled task support to. - public static void EnableScheduleSupport(this IDurableTaskWorkerBuilder builder) + public static void EnableScheduledTasksSupport(this IDurableTaskWorkerBuilder builder) { builder.AddTasks(r => r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp))); } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 6aa180b1..2a1d61e9 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -36,7 +36,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc { string errorMessage = "Schedule is already created."; Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); throw exception; } From d14c484a2a1913a16cb175f909916da5e4431262 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:16:28 -0800 Subject: [PATCH 038/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 41cb91fa..34c3504e 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -33,7 +33,7 @@ public class ScheduleHandle : IScheduleHandle /// Thrown if or is null. public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logger) { - client = client ?? throw new ArgumentNullException(nameof(client)); + this.durableTaskClient = client ?? throw new ArgumentNullException(nameof(client)); this.ScheduleId = scheduleId ?? throw new ArgumentNullException(nameof(scheduleId)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } From ac7cd58e6353da4cc1c51bfc8b073d10ade664a6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:30:18 -0800 Subject: [PATCH 039/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 34c3504e..35f2dafe 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -19,11 +19,6 @@ public class ScheduleHandle : IScheduleHandle readonly DurableTaskClient durableTaskClient; readonly ILogger logger; - /// - /// Gets the ID of the schedule. - /// - public string ScheduleId { get; } - /// /// Initializes a new instance of the class. /// @@ -38,6 +33,11 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + /// Gets the ID of the schedule. + /// + public string ScheduleId { get; } + /// public async Task DescribeAsync() { @@ -147,4 +147,6 @@ public async Task DeleteAsync() await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); } + + } From 53d7193edeca03e7579378604ee25726a9523bba Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:57:21 -0800 Subject: [PATCH 040/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 12 +++++++ src/ScheduledTasks/Client/ScheduleHandle.cs | 36 +++++++++++++++----- src/ScheduledTasks/Logging/Client/Logs.cs | 3 ++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 8c47ed38..c914f312 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -1,3 +1,5 @@ +using Microsoft.DurableTask.Client; + namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -40,4 +42,14 @@ public interface IScheduleHandle /// The options for updating the schedule configuration. /// A task that completes when the schedule is updated. Task UpdateAsync(ScheduleUpdateOptions updateOptions); + + /// + /// Gets the details of the schedule's underlying orchestration instance. + /// + /// If true, includes the serialized inputs and outputs in the returned metadata. + /// Optional cancellation token. + /// The orchestration metadata for the schedule instance, or null if not found. + Task GetScheduleInstanceDetailsAsync( + bool getInputsAndOutputs = false, + CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 35f2dafe..5ce03500 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -90,8 +90,8 @@ public async Task PauseAsync() this.logger.ClientPausingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); @@ -106,8 +106,8 @@ public async Task ResumeAsync() this.logger.ClientResumingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); @@ -122,8 +122,8 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); - var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); @@ -138,8 +138,8 @@ public async Task DeleteAsync() this.logger.ClientDeletingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - var entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - var metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); @@ -148,5 +148,23 @@ public async Task DeleteAsync() await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); } - + /// + /// Gets the details of the schedule's underlying orchestration instance. + /// + /// If true, includes the serialized inputs and outputs in the returned metadata. + /// Optional cancellation token. + /// The orchestration metadata for the schedule instance, or null if not found. + public async Task GetScheduleInstanceDetailsAsync( + bool getInputsAndOutputs = false, + CancellationToken cancellation = default) + { + this.logger.ClientGettingScheduleInstanceDetails(this.ScheduleId); + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); + + string instanceId = new EntityInstanceId(nameof(Schedule), this.ScheduleId).ToString(); + return await this.durableTaskClient.GetInstanceAsync( + instanceId, + getInputsAndOutputs, + cancellation); + } } diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 7b9e1500..1cc33a98 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -37,4 +37,7 @@ static partial class Logs [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Client: Deleting schedule '{scheduleId}'")] public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); + + [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Client: Getting instance details for schedule '{scheduleId}'")] + public static partial void ClientGettingScheduleInstanceDetails(this ILogger logger, string scheduleId); } From 3d832d3d6c6160ffa08d847f3d3b20035b581bd5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:12:09 -0800 Subject: [PATCH 041/203] save --- .../Client/ScheduledTaskClient.cs | 2 +- .../DurableTaskClientBuilderExtensions.cs | 0 .../DurableTaskSchedulerWorkerExtensions.cs | 0 .../Models/ScheduleUpdateOptions.cs | 82 ++++++++++++------- 4 files changed, 53 insertions(+), 31 deletions(-) rename src/ScheduledTasks/{ => Extension}/DurableTaskClientBuilderExtensions.cs (100%) rename src/ScheduledTasks/{ => Extension}/DurableTaskSchedulerWorkerExtensions.cs (100%) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 94bf72a6..15fff171 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -72,7 +72,7 @@ public async Task> ListInitializedSchedulesAsyn { if (metadata.State.Status != ScheduleStatus.Uninitialized) { - ScheduleConfiguration config = metadata.State.ScheduleConfiguration; + ScheduleConfiguration config = metadata.State.ScheduleConfiguration!; schedules.Add(new ScheduleDescription( metadata.Id.Key, config.OrchestrationName, diff --git a/src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs similarity index 100% rename from src/ScheduledTasks/DurableTaskClientBuilderExtensions.cs rename to src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs diff --git a/src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs similarity index 100% rename from src/ScheduledTasks/DurableTaskSchedulerWorkerExtensions.cs rename to src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 9aee8c2d..ecbd7472 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -3,56 +3,78 @@ namespace Microsoft.DurableTask.ScheduledTasks; -public class ScheduleUpdateOptions +/// +/// Options for updating an existing schedule. +/// +public record ScheduleUpdateOptions { - string? orchestrationName; + /// + /// Gets or initializes the name of the orchestration function to schedule. + /// + public string? OrchestrationName { get; init; } - public string? OrchestrationName - { - get => this.orchestrationName; - set - { - this.orchestrationName = value; - } - } - - public string? OrchestrationInput { get; set; } + /// + /// Gets or initializes the input to the orchestration function. + /// + public string? OrchestrationInput { get; init; } - public string? OrchestrationInstanceId { get; set; } + /// + /// Gets or initializes the instance ID of the orchestration function. + /// + public string? OrchestrationInstanceId { get; init; } - public DateTimeOffset? StartAt { get; set; } + /// + /// Gets or initializes the start time of the schedule. + /// + public DateTimeOffset? StartAt { get; init; } - public DateTimeOffset? EndAt { get; set; } + /// + /// Gets or initializes the end time of the schedule. + /// + public DateTimeOffset? EndAt { get; init; } + /// + /// The interval of the schedule. + /// TimeSpan? interval; + /// + /// Gets or initializes the interval of the schedule. + /// public TimeSpan? Interval { get => this.interval; - set + init { - if (!value.HasValue) + if (value.HasValue) { - return; - } + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } } this.interval = value; } } - public string? CronExpression { get; set; } + /// + /// Gets or initializes the cron expression for the schedule. + /// + public string? CronExpression { get; init; } - public int MaxOccurrence { get; set; } + /// + /// Gets or initializes the maximum number of times the schedule should run. + /// + public int MaxOccurrence { get; init; } - public bool? StartImmediatelyIfLate { get; set; } + /// + /// Gets or initializes whether the schedule should start immediately if it's late. + /// + public bool? StartImmediatelyIfLate { get; init; } } From baf0a51c358fb3df44af5b7122794195050bcb21 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:16:04 -0800 Subject: [PATCH 042/203] save --- src/ScheduledTasks/Models/ScheduleUpdateOptions.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index ecbd7472..69e94db1 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -8,6 +8,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public record ScheduleUpdateOptions { + TimeSpan? interval; + /// /// Gets or initializes the name of the orchestration function to schedule. /// @@ -33,11 +35,6 @@ public record ScheduleUpdateOptions /// public DateTimeOffset? EndAt { get; init; } - /// - /// The interval of the schedule. - /// - TimeSpan? interval; - /// /// Gets or initializes the interval of the schedule. /// From 387932921efedcf21a2932953a5bcd1982637226 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:17:30 -0800 Subject: [PATCH 043/203] save --- src/ScheduledTasks/Models/ScheduleStatus.cs | 20 ++++++++++++++++--- .../Models/ScheduleTransitions.cs | 16 ++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index c7a23931..94e0c191 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -3,9 +3,23 @@ namespace Microsoft.DurableTask.ScheduledTasks; +/// +/// Represents the current status of a schedule. +/// public enum ScheduleStatus { - Uninitialized, // Schedule has not been created - Active, // Schedule is active and running - Paused, // Schedule is paused + /// + /// Schedule has not been created. + /// + Uninitialized, + + /// + /// Schedule is active and running. + /// + Active, + + /// + /// Schedule is paused. + /// + Paused, } diff --git a/src/ScheduledTasks/Models/ScheduleTransitions.cs b/src/ScheduledTasks/Models/ScheduleTransitions.cs index 0cc39f7c..cc936461 100644 --- a/src/ScheduledTasks/Models/ScheduleTransitions.cs +++ b/src/ScheduledTasks/Models/ScheduleTransitions.cs @@ -3,8 +3,14 @@ namespace Microsoft.DurableTask.ScheduledTasks; +/// +/// Manages valid state transitions for schedules. +/// static class ScheduleTransitions { + /// + /// Maps schedule states to their valid target states. + /// static readonly Dictionary> ValidTransitions = new Dictionary> { @@ -13,8 +19,16 @@ static class ScheduleTransitions { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, }; + /// + /// Attempts to get the valid target states for a given schedule state. + /// + /// The current schedule state. + /// When this method returns, contains the valid target states if found; otherwise, an empty set. + /// True if valid transitions exist for the given state; otherwise, false. public static bool TryGetValidTransitions(ScheduleStatus from, out HashSet validTargetStates) { - return ValidTransitions.TryGetValue(from, out validTargetStates); + bool exists = ValidTransitions.TryGetValue(from, out HashSet? states); + validTargetStates = states ?? new HashSet(); + return exists; } } From bf2ece5cbb1f3819d57947e58603fb8712ae1617 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:18:43 -0800 Subject: [PATCH 044/203] save --- src/ScheduledTasks/Models/ScheduleState.cs | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 6ea343e1..c104f446 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -8,16 +8,36 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleState { + /// + /// Gets or sets the current status of the schedule. + /// internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; + /// + /// Gets or sets the execution token used to validate schedule operations. + /// internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets the last time the schedule was run. + /// internal DateTimeOffset? LastRunAt { get; set; } + /// + /// Gets or sets the next scheduled run time. + /// internal DateTimeOffset? NextRunAt { get; set; } + /// + /// Gets or sets the schedule configuration. + /// internal ScheduleConfiguration? ScheduleConfiguration { get; set; } + /// + /// Updates the schedule configuration with the provided options. + /// + /// The update options to apply. + /// A set of field names that were updated. public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOptions) { Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); @@ -77,9 +97,9 @@ public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOp return updatedFields; } - // To stop potential runSchedule operation scheduled after the schedule update/pause, invalidate the execution token and let it exit gracefully - // This could incur little overhead as ideally the runSchedule with old token should be killed immediately - // but there is no support to cancel pending entity operations currently, can be a todo item + /// + /// Refreshes the execution token to invalidate pending schedule operations. + /// public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); From 30a843e1e20c672d2aecf307b7b845fdf5bb6064 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:19:19 -0800 Subject: [PATCH 045/203] SAVE --- src/ScheduledTasks/Models/ScheduleState.cs | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index c104f446..2f4f2d52 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -36,61 +36,61 @@ class ScheduleState /// /// Updates the schedule configuration with the provided options. /// - /// The update options to apply. + /// The update options to apply. /// A set of field names that were updated. - public HashSet UpdateConfig(ScheduleUpdateOptions scheduleConfigUpdateOptions) + public HashSet UpdateConfig(ScheduleUpdateOptions scheduleUpdateOptions) { Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); - Check.NotNull(scheduleConfigUpdateOptions, nameof(scheduleConfigUpdateOptions)); + Check.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); HashSet updatedFields = new HashSet(); - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.OrchestrationName)) + if (!string.IsNullOrEmpty(scheduleUpdateOptions.OrchestrationName)) { - this.ScheduleConfiguration.OrchestrationName = scheduleConfigUpdateOptions.OrchestrationName; + this.ScheduleConfiguration.OrchestrationName = scheduleUpdateOptions.OrchestrationName; updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); } - if (scheduleConfigUpdateOptions.OrchestrationInput == null) + if (scheduleUpdateOptions.OrchestrationInput == null) { - this.ScheduleConfiguration.OrchestrationInput = scheduleConfigUpdateOptions.OrchestrationInput; + this.ScheduleConfiguration.OrchestrationInput = scheduleUpdateOptions.OrchestrationInput; updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); } - if (scheduleConfigUpdateOptions.StartAt.HasValue) + if (scheduleUpdateOptions.StartAt.HasValue) { - this.ScheduleConfiguration.StartAt = scheduleConfigUpdateOptions.StartAt; + this.ScheduleConfiguration.StartAt = scheduleUpdateOptions.StartAt; updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); } - if (scheduleConfigUpdateOptions.EndAt.HasValue) + if (scheduleUpdateOptions.EndAt.HasValue) { - this.ScheduleConfiguration.EndAt = scheduleConfigUpdateOptions.EndAt; + this.ScheduleConfiguration.EndAt = scheduleUpdateOptions.EndAt; updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); } - if (scheduleConfigUpdateOptions.Interval.HasValue) + if (scheduleUpdateOptions.Interval.HasValue) { - this.ScheduleConfiguration.Interval = scheduleConfigUpdateOptions.Interval; + this.ScheduleConfiguration.Interval = scheduleUpdateOptions.Interval; updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); } - if (!string.IsNullOrEmpty(scheduleConfigUpdateOptions.CronExpression)) + if (!string.IsNullOrEmpty(scheduleUpdateOptions.CronExpression)) { - this.ScheduleConfiguration.CronExpression = scheduleConfigUpdateOptions.CronExpression; + this.ScheduleConfiguration.CronExpression = scheduleUpdateOptions.CronExpression; updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); } - if (scheduleConfigUpdateOptions.MaxOccurrence != 0) + if (scheduleUpdateOptions.MaxOccurrence != 0) { - this.ScheduleConfiguration.MaxOccurrence = scheduleConfigUpdateOptions.MaxOccurrence; + this.ScheduleConfiguration.MaxOccurrence = scheduleUpdateOptions.MaxOccurrence; updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); } // Only update if the customer explicitly set a value - if (scheduleConfigUpdateOptions.StartImmediatelyIfLate.HasValue) + if (scheduleUpdateOptions.StartImmediatelyIfLate.HasValue) { - this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleConfigUpdateOptions.StartImmediatelyIfLate.Value; + this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleUpdateOptions.StartImmediatelyIfLate.Value; updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); } From 8093ac0004d3c42d7e1fcabeeb63ea1fe33eb061 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:29:10 -0800 Subject: [PATCH 046/203] save --- .../Models/ScheduleConfiguration.cs | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 01c0c441..7f4217e4 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -8,14 +8,23 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleConfiguration { + string orchestrationName; + TimeSpan? interval; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the orchestration to schedule. + /// The ID of the schedule, or null to generate one. public ScheduleConfiguration(string orchestrationName, string scheduleId) { this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); } - string orchestrationName; - + /// + /// Gets or sets the name of the orchestration function to schedule. + /// public string OrchestrationName { get => this.orchestrationName; @@ -25,18 +34,34 @@ public string OrchestrationName } } + /// + /// Gets the ID of the schedule. + /// public string ScheduleId { get; init; } + /// + /// Gets or sets the input to the orchestration function. + /// public string? OrchestrationInput { get; set; } + /// + /// Gets or sets the instance ID of the orchestration function. + /// public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets the start time of the schedule. + /// public DateTimeOffset? StartAt { get; set; } + /// + /// Gets or sets the end time of the schedule. + /// public DateTimeOffset? EndAt { get; set; } - TimeSpan? interval; - + /// + /// Gets or sets the interval between schedule executions. + /// public TimeSpan? Interval { get => this.interval; @@ -61,12 +86,26 @@ public TimeSpan? Interval } } + /// + /// Gets or sets the cron expression for the schedule. + /// public string? CronExpression { get; set; } + /// + /// Gets or sets the maximum number of times the schedule should run. + /// public int MaxOccurrence { get; set; } + /// + /// Gets or sets whether the schedule should start immediately if it's late. + /// public bool? StartImmediatelyIfLate { get; set; } + /// + /// Creates a new configuration from the provided creation options. + /// + /// The options to create the configuration from. + /// A new schedule configuration. public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions createOptions) { return new ScheduleConfiguration(createOptions.OrchestrationName, createOptions.ScheduleId) From 3a833dfd61482f6cb2099b8ee5bf8408ac6146b6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:43:37 -0800 Subject: [PATCH 047/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 2 +- .../Models/ScheduleConfiguration.cs | 67 +++++++++++++++++++ src/ScheduledTasks/Models/ScheduleState.cs | 64 ------------------ 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 2a1d61e9..a937490f 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -62,7 +62,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche Verify.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - HashSet updatedScheduleConfigFields = this.State.UpdateConfig(scheduleUpdateOptions); + HashSet updatedScheduleConfigFields = this.State.ScheduleConfiguration.Update(scheduleUpdateOptions); if (updatedScheduleConfigFields.Count == 0) { // no need to interrupt and update current schedule run as there is no change in the schedule config diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 7f4217e4..0db4e525 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -120,4 +120,71 @@ public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions cr StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate, }; } + + /// + /// Updates this configuration with the provided update options. + /// + /// The options to update the configuration with. + /// A set of field names that were updated. + public HashSet Update(ScheduleUpdateOptions updateOptions) + { + Check.NotNull(updateOptions, nameof(updateOptions)); + HashSet updatedFields = new HashSet(); + + if (!string.IsNullOrEmpty(updateOptions.OrchestrationName)) + { + this.OrchestrationName = updateOptions.OrchestrationName; + updatedFields.Add(nameof(this.OrchestrationName)); + } + + if (updateOptions.OrchestrationInput == null) + { + this.OrchestrationInput = updateOptions.OrchestrationInput; + updatedFields.Add(nameof(this.OrchestrationInput)); + } + + if (!string.IsNullOrEmpty(updateOptions.OrchestrationInstanceId)) + { + this.OrchestrationInstanceId = updateOptions.OrchestrationInstanceId; + updatedFields.Add(nameof(this.OrchestrationInstanceId)); + } + + if (updateOptions.StartAt.HasValue) + { + this.StartAt = updateOptions.StartAt; + updatedFields.Add(nameof(this.StartAt)); + } + + if (updateOptions.EndAt.HasValue) + { + this.EndAt = updateOptions.EndAt; + updatedFields.Add(nameof(this.EndAt)); + } + + if (updateOptions.Interval.HasValue) + { + this.Interval = updateOptions.Interval; + updatedFields.Add(nameof(this.Interval)); + } + + if (!string.IsNullOrEmpty(updateOptions.CronExpression)) + { + this.CronExpression = updateOptions.CronExpression; + updatedFields.Add(nameof(this.CronExpression)); + } + + if (updateOptions.MaxOccurrence != 0) + { + this.MaxOccurrence = updateOptions.MaxOccurrence; + updatedFields.Add(nameof(this.MaxOccurrence)); + } + + if (updateOptions.StartImmediatelyIfLate.HasValue) + { + this.StartImmediatelyIfLate = updateOptions.StartImmediatelyIfLate.Value; + updatedFields.Add(nameof(this.StartImmediatelyIfLate)); + } + + return updatedFields; + } } diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 2f4f2d52..29cadb9a 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -33,70 +33,6 @@ class ScheduleState /// internal ScheduleConfiguration? ScheduleConfiguration { get; set; } - /// - /// Updates the schedule configuration with the provided options. - /// - /// The update options to apply. - /// A set of field names that were updated. - public HashSet UpdateConfig(ScheduleUpdateOptions scheduleUpdateOptions) - { - Check.NotNull(this.ScheduleConfiguration, nameof(this.ScheduleConfiguration)); - Check.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); - - HashSet updatedFields = new HashSet(); - - if (!string.IsNullOrEmpty(scheduleUpdateOptions.OrchestrationName)) - { - this.ScheduleConfiguration.OrchestrationName = scheduleUpdateOptions.OrchestrationName; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationName)); - } - - if (scheduleUpdateOptions.OrchestrationInput == null) - { - this.ScheduleConfiguration.OrchestrationInput = scheduleUpdateOptions.OrchestrationInput; - updatedFields.Add(nameof(this.ScheduleConfiguration.OrchestrationInput)); - } - - if (scheduleUpdateOptions.StartAt.HasValue) - { - this.ScheduleConfiguration.StartAt = scheduleUpdateOptions.StartAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartAt)); - } - - if (scheduleUpdateOptions.EndAt.HasValue) - { - this.ScheduleConfiguration.EndAt = scheduleUpdateOptions.EndAt; - updatedFields.Add(nameof(this.ScheduleConfiguration.EndAt)); - } - - if (scheduleUpdateOptions.Interval.HasValue) - { - this.ScheduleConfiguration.Interval = scheduleUpdateOptions.Interval; - updatedFields.Add(nameof(this.ScheduleConfiguration.Interval)); - } - - if (!string.IsNullOrEmpty(scheduleUpdateOptions.CronExpression)) - { - this.ScheduleConfiguration.CronExpression = scheduleUpdateOptions.CronExpression; - updatedFields.Add(nameof(this.ScheduleConfiguration.CronExpression)); - } - - if (scheduleUpdateOptions.MaxOccurrence != 0) - { - this.ScheduleConfiguration.MaxOccurrence = scheduleUpdateOptions.MaxOccurrence; - updatedFields.Add(nameof(this.ScheduleConfiguration.MaxOccurrence)); - } - - // Only update if the customer explicitly set a value - if (scheduleUpdateOptions.StartImmediatelyIfLate.HasValue) - { - this.ScheduleConfiguration.StartImmediatelyIfLate = scheduleUpdateOptions.StartImmediatelyIfLate.Value; - updatedFields.Add(nameof(this.ScheduleConfiguration.StartImmediatelyIfLate)); - } - - return updatedFields; - } - /// /// Refreshes the execution token to invalidate pending schedule operations. /// From 7c0d7af1e0cfa8eb021a9e4d92bb1baf1dddbc9a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:44:37 -0800 Subject: [PATCH 048/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index a937490f..9d3ff598 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -110,7 +110,7 @@ public void PauseSchedule() this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); - this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.logger.PausedSchedule(this.State.ScheduleConfiguration!.ScheduleId); } /// @@ -149,7 +149,7 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { - this.logger.RunningSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); if (this.State.ScheduleConfiguration.Interval == null) { From 5f7c1681647ad1c48fd006988ab9de1b7c5f7819 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 00:13:31 -0800 Subject: [PATCH 049/203] save --- samples/ScheduleDemo/Program.cs | 231 +++++++++++------------ samples/ScheduleDemo/ScheduleDemo.csproj | 5 - src/Client/Grpc/GrpcDurableTaskClient.cs | 2 +- src/ScheduledTasks/Entity/Schedule.cs | 2 - 4 files changed, 113 insertions(+), 127 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 64ff4628..c375a161 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -1,122 +1,115 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//using Microsoft.DurableTask.Client; -//using Microsoft.DurableTask.Client.AzureManaged; -//using Microsoft.DurableTask.Worker; -//using Microsoft.DurableTask.Worker.AzureManaged; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Hosting; -//using Microsoft.Extensions.Logging; - -//class Program -//{ -// static async Task Main(string[] args) -// { -// // Create the host builder -// IHost host = Host.CreateDefaultBuilder(args) -// .ConfigureServices(services => -// { -// string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") -// ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); - -// // Configure the worker -// services.AddDurableTaskWorker(builder => -// { -// // Add the Schedule entity and demo orchestration -// _ = builder.AddTasks(r => -// { -// // Add a demo orchestration that will be triggered by the schedule -// r.AddOrchestratorFunc("DemoOrchestration", async context => -// { -// string input = context.GetInput(); -// await context.CallActivityAsync("ProcessMessage", input); -// return $"Completed processing at {DateTime.UtcNow}"; -// }); -// // Add a demo activity -// r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); -// }); - -// builder.UseDurableTaskScheduler(connectionString); -// }); - -// // Configure the client -// services.AddDurableTaskClient(builder => -// { -// builder.UseDurableTaskScheduler(connectionString); -// }); - -// // Configure console logging -// services.AddLogging(logging => -// { -// logging.AddSimpleConsole(options => -// { -// options.SingleLine = true; -// options.UseUtcTimestamp = true; -// options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; -// }); -// }); -// }) -// .Build(); - -// await host.StartAsync(); -// await using DurableTaskClient client = host.Services.GetRequiredService(); - -// try -// { -// // Create a schedule that runs every 30 seconds -// ScheduleConfiguration scheduleConfig = new ScheduleConfiguration( -// orchestrationName: "DemoOrchestration", -// scheduleId: "demo-schedule") -// { -// Interval = TimeSpan.FromSeconds(30), -// StartAt = DateTimeOffset.UtcNow, -// OrchestrationInput = "This is a scheduled message!" -// }; - -// // Create the schedule -// Console.WriteLine("Creating schedule..."); -// await client.CreateScheduleAsync(scheduleConfig); -// Console.WriteLine($"Created schedule with ID: {scheduleConfig.ScheduleId}"); - -// // Monitor the schedule for a while -// Console.WriteLine("\nMonitoring schedule for 2 minutes..."); -// for (int i = 0; i < 4; i++) -// { -// await Task.Delay(TimeSpan.FromSeconds(30)); -// var schedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); -// Console.WriteLine($"\nSchedule status: {schedule.Status}"); -// Console.WriteLine($"Last run at: {schedule.LastRunAt}"); -// Console.WriteLine($"Next run at: {schedule.NextRunAt}"); -// } - -// // Pause the schedule -// Console.WriteLine("\nPausing schedule..."); -// await client.PauseScheduleAsync(scheduleConfig.ScheduleId); - -// var pausedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); -// Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); - -// // Resume the schedule -// Console.WriteLine("\nResuming schedule..."); -// await client.ResumeScheduleAsync(scheduleConfig.ScheduleId); - -// var resumedSchedule = await client.GetScheduleAsync(scheduleConfig.ScheduleId); -// Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); -// Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); - -// Console.WriteLine("\nPress any key to delete the schedule and exit..."); -// Console.ReadKey(); - -// // Delete the schedule -// await client.DeleteScheduleAsync(scheduleConfig.ScheduleId); -// Console.WriteLine("Schedule deleted."); -// } -// catch (Exception ex) -// { -// Console.WriteLine($"Error: {ex.Message}"); -// } - -// await host.StopAsync(); -// } -//} +using Microsoft.DurableTask.ScheduledTasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +// Create the host builder +IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + + // Configure the worker + services.AddDurableTaskWorker(builder => + { + // Add the Schedule entity and demo orchestration + builder.AddTasks(r => + { + // Add a demo orchestration that will be triggered by the schedule + r.AddOrchestratorFunc("DemoOrchestration", async context => + { + string input = context.GetInput(); + await context.CallActivityAsync("ProcessMessage", input); + return $"Completed processing at {DateTime.UtcNow}"; + }); + // Add a demo activity + r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); + }); + + // Enable scheduled tasks support + builder.EnableScheduledTasksSupport(); + builder.UseDurableTaskScheduler(connectionString); + }); + + // Configure the client + services.AddDurableTaskClient(builder => + { + builder.EnableScheduledTasksSupport(); + builder.UseDurableTaskScheduler(connectionString); + }); + + // Configure console logging + services.AddLogging(logging => + { + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); + }); + }) + .Build(); + +await host.StartAsync(); +IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); + +try +{ + // Create schedule options that runs every 30 seconds + var scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") + { + ScheduleId = "demo-schedule", + Interval = TimeSpan.FromSeconds(30), + StartAt = DateTimeOffset.UtcNow, + OrchestrationInput = "This is a scheduled message!" + }; + + // Create the schedule + Console.WriteLine("Creating schedule..."); + IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + Console.WriteLine($"Created schedule with ID: {scheduleHandle.ScheduleId}"); + + // Monitor the schedule for a while + Console.WriteLine("\nMonitoring schedule for 2 minutes..."); + for (int i = 0; i < 4; i++) + { + await Task.Delay(TimeSpan.FromSeconds(30)); + var scheduleDescription = await scheduleHandle.DescribeAsync(); + Console.WriteLine($"\nSchedule status: {scheduleDescription.Status}"); + Console.WriteLine($"Last run at: {scheduleDescription.LastRunAt}"); + Console.WriteLine($"Next run at: {scheduleDescription.NextRunAt}"); + } + + // Pause the schedule + Console.WriteLine("\nPausing schedule..."); + await scheduleHandle.PauseAsync(); + + var pausedSchedule = await scheduleHandle.DescribeAsync(); + Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); + + // Resume the schedule + Console.WriteLine("\nResuming schedule..."); + await scheduleHandle.ResumeAsync(); + + var resumedSchedule = await scheduleHandle.DescribeAsync(); + Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); + Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); + + Console.WriteLine("\nPress any key to delete the schedule and exit..."); + Console.ReadKey(); + + // Delete the schedule + await scheduleHandle.DeleteAsync(); + Console.WriteLine("Schedule deleted."); +} +catch (Exception ex) +{ + Console.WriteLine($"Error: {ex.Message}"); +} + +await host.StopAsync(); \ No newline at end of file diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index 48313277..5aa3bc75 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -15,11 +15,6 @@ - - - - - diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index 0fa87d95..f0d6e6e8 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -431,4 +431,4 @@ OrchestrationMetadata CreateMetadata(P.OrchestrationState state, bool includeInp DataConverter = includeInputsAndOutputs ? this.DataConverter : null, }; } -} \ No newline at end of file +} diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 9d3ff598..f5e65446 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -6,8 +6,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: logging - /// /// Entity that manages the state and execution of a scheduled task. /// From 88e6f771f3efd51e59fedd391925f3ddbaac75bb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 00:28:07 -0800 Subject: [PATCH 050/203] save --- samples/ScheduleDemo/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index c375a161..085b701d 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.AzureManaged; using Microsoft.DurableTask.ScheduledTasks; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; From dd5f9e4e6f6e27e6c7e10e7761150c9a47b506e8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:28:31 -0800 Subject: [PATCH 051/203] save --- samples/ScheduleDemo/Program.cs | 104 ++++++++++-------- src/ScheduledTasks/Client/IScheduleHandle.cs | 3 + src/ScheduledTasks/Logging/Entity/Logs.cs | 5 +- .../Models/ScheduleCreationOptions.cs | 19 +++- 4 files changed, 80 insertions(+), 51 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 085b701d..d013b68a 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -11,60 +11,61 @@ // Create the host builder IHost host = Host.CreateDefaultBuilder(args) - .ConfigureServices(services => + .ConfigureServices(services => + { + string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + + // Configure the worker + _ = services.AddDurableTaskWorker(builder => + { + // Add the Schedule entity and demo orchestration + builder.AddTasks(r => { - string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") - ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); - - // Configure the worker - services.AddDurableTaskWorker(builder => + // Add a demo orchestration that will be triggered by the schedule + r.AddOrchestratorFunc("DemoOrchestration", async context => { - // Add the Schedule entity and demo orchestration - builder.AddTasks(r => - { - // Add a demo orchestration that will be triggered by the schedule - r.AddOrchestratorFunc("DemoOrchestration", async context => - { - string input = context.GetInput(); - await context.CallActivityAsync("ProcessMessage", input); - return $"Completed processing at {DateTime.UtcNow}"; - }); - // Add a demo activity - r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); - }); - - // Enable scheduled tasks support - builder.EnableScheduledTasksSupport(); - builder.UseDurableTaskScheduler(connectionString); + string? input = context.GetInput(); + await context.CallActivityAsync("ProcessMessage", input); + return $"Completed processing at {DateTime.UtcNow}"; }); - - // Configure the client - services.AddDurableTaskClient(builder => - { - builder.EnableScheduledTasksSupport(); - builder.UseDurableTaskScheduler(connectionString); - }); - - // Configure console logging - services.AddLogging(logging => - { - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); - }); - }) - .Build(); + // Add a demo activity + r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); + }); + + // Enable scheduled tasks support + builder.EnableScheduledTasksSupport(); + builder.UseDurableTaskScheduler(connectionString); + }); + + // Configure the client + services.AddDurableTaskClient(builder => + { + builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); + }); + + // Configure console logging + services.AddLogging(logging => + { + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); + }); + }) + .Build(); await host.StartAsync(); + IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); try { // Create schedule options that runs every 30 seconds - var scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") + ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { ScheduleId = "demo-schedule", Interval = TimeSpan.FromSeconds(30), @@ -106,6 +107,21 @@ Console.WriteLine("\nPress any key to delete the schedule and exit..."); Console.ReadKey(); + // intentionally call schedule to trigger exceptions + await scheduleHandle.ResumeAsync(); + await scheduleHandle.ResumeAsync(); + + // Get schedule instance details + var scheduleInstanceDetails = await scheduleHandle.GetScheduleInstanceDetailsAsync(true); + Console.WriteLine($"\nSchedule instance details:"); + Console.WriteLine($"{scheduleInstanceDetails}"); + Console.WriteLine($"Instance ID: {scheduleInstanceDetails!.InstanceId}"); + Console.WriteLine($"Created time: {scheduleInstanceDetails.CreatedAt}"); + Console.WriteLine($"Name: {scheduleInstanceDetails.Name}"); + Console.WriteLine($"RuntimeStatus: {scheduleInstanceDetails.RuntimeStatus}"); + Console.WriteLine($"LastUpdatedAt: {scheduleInstanceDetails.LastUpdatedAt}"); + Console.WriteLine($"FailureDetails: {scheduleInstanceDetails.FailureDetails}"); + Console.WriteLine(); // Add blank line between instances // Delete the schedule await scheduleHandle.DeleteAsync(); Console.WriteLine("Schedule deleted."); diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index c914f312..5b7e1ee4 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using Microsoft.DurableTask.Client; namespace Microsoft.DurableTask.ScheduledTasks; diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Entity/Logs.cs index b9bb8f88..a0354842 100644 --- a/src/ScheduledTasks/Logging/Entity/Logs.cs +++ b/src/ScheduledTasks/Logging/Entity/Logs.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; + /// /// Log messages. /// @@ -41,7 +42,7 @@ static partial class Logs [LoggerMessage(EventId = 10, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is executed")] public static partial void CompletedScheduleRun(this ILogger logger, string scheduleId); - + [LoggerMessage(EventId = 11, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being deleted")] public static partial void DeletingSchedule(this ILogger logger, string scheduleId); @@ -59,4 +60,4 @@ static partial class Logs [LoggerMessage(EventId = 16, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] public static partial void ScheduleRunCancelled(this ILogger logger, string scheduleId, string executionToken); -} \ No newline at end of file +} diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index de21b176..33ec7754 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -8,6 +8,11 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public record ScheduleCreationOptions { + /// + /// The interval of the schedule. + /// + TimeSpan? interval; + /// /// Gets the name of the orchestration function to schedule. /// @@ -49,11 +54,6 @@ public ScheduleCreationOptions(string orchestrationName) /// public DateTimeOffset? EndAt { get; init; } - /// - /// The interval of the schedule. - /// - TimeSpan? interval; - /// /// Gets the interval of the schedule. /// @@ -79,9 +79,18 @@ public TimeSpan? Interval } } + /// + /// Gets the cron expression for the schedule. + /// public string? CronExpression { get; init; } + /// + /// Gets the maximum number of occurrences for the schedule. + /// public int MaxOccurrence { get; init; } + /// + /// Gets a value indicating whether to start the schedule immediately if it is late. + /// public bool? StartImmediatelyIfLate { get; init; } } From 436850e21820f5f227c07f696369f644796369b6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:32:47 -0800 Subject: [PATCH 052/203] save --- src/Client/Core/DurableTaskClient.cs | 2 +- src/ScheduledTasks/Client/IScheduledTaskClient.cs | 1 + .../Exception/ScheduleAlreadyExistException.cs | 10 +++++----- .../Exception/ScheduleInternalException.cs | 10 +++++----- .../Exception/ScheduleNotFoundException.cs | 10 +++++----- .../ScheduleStillBeingProvisionedException.cs | 10 +++++----- src/ScheduledTasks/Models/ScheduleCreationOptions.cs | 10 +++++----- 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index dcf33f93..16d6d842 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -408,4 +408,4 @@ public virtual Task PurgeAllInstancesAsync( /// /// A that completes when the disposal completes. public abstract ValueTask DisposeAsync(); -} \ No newline at end of file +} diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 326a55ca..85ec05e5 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -24,6 +24,7 @@ public interface IScheduledTaskClient /// /// Creates a new schedule with the specified configuration. /// + /// The configuration options for creating the schedule. /// The ID of the newly created schedule. Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); diff --git a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs index 41f8ce7a..e05955ca 100644 --- a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs +++ b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs @@ -8,11 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleAlreadyExistException : Exception { - /// - /// Gets the ID of the schedule that already exists. - /// - public string ScheduleId { get; } - /// /// Initializes a new instance of the class. /// @@ -33,4 +28,9 @@ public ScheduleAlreadyExistException(string scheduleId, Exception innerException { this.ScheduleId = scheduleId; } + + /// + /// Gets the ID of the schedule that already exists. + /// + public string ScheduleId { get; } } diff --git a/src/ScheduledTasks/Exception/ScheduleInternalException.cs b/src/ScheduledTasks/Exception/ScheduleInternalException.cs index 225ea41e..a5d682c8 100644 --- a/src/ScheduledTasks/Exception/ScheduleInternalException.cs +++ b/src/ScheduledTasks/Exception/ScheduleInternalException.cs @@ -8,11 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleInternalException : Exception { - /// - /// Gets the ID of the schedule that encountered the internal error. - /// - public string ScheduleId { get; } - /// /// Initializes a new instance of the class. /// @@ -35,4 +30,9 @@ public ScheduleInternalException(string scheduleId, string message, Exception in { this.ScheduleId = scheduleId; } + + /// + /// Gets the ID of the schedule that encountered the internal error. + /// + public string ScheduleId { get; } } diff --git a/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs index 44ea62ea..24334008 100644 --- a/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs +++ b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs @@ -8,11 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleNotFoundException : Exception { - /// - /// Gets the ID of the schedule that was not found. - /// - public string ScheduleId { get; } - /// /// Initializes a new instance of the class. /// @@ -33,4 +28,9 @@ public ScheduleNotFoundException(string scheduleId, Exception innerException) { this.ScheduleId = scheduleId; } + + /// + /// Gets the ID of the schedule that was not found. + /// + public string ScheduleId { get; } } diff --git a/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs b/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs index 3c726307..0247cfee 100644 --- a/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs +++ b/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs @@ -8,11 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleStillBeingProvisionedException : Exception { - /// - /// Gets the ID of the schedule that is still being provisioned. - /// - public string ScheduleId { get; } - /// /// Initializes a new instance of the class. /// @@ -33,4 +28,9 @@ public ScheduleStillBeingProvisionedException(string scheduleId, Exception inner { this.ScheduleId = scheduleId; } + + /// + /// Gets the ID of the schedule that is still being provisioned. + /// + public string ScheduleId { get; } } diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 33ec7754..d328ab2b 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -13,11 +13,6 @@ public record ScheduleCreationOptions /// TimeSpan? interval; - /// - /// Gets the name of the orchestration function to schedule. - /// - public string OrchestrationName { get; init; } - /// /// Initializes a new instance of the class. /// @@ -29,6 +24,11 @@ public ScheduleCreationOptions(string orchestrationName) this.OrchestrationName = orchestrationName; } + /// + /// Gets the name of the orchestration function to schedule. + /// + public string OrchestrationName { get; init; } + /// /// Gets the ID of the schedule, if not provided, default to a new GUID. /// From d4eb72fad354e54e92dcee42b0fac0b1834b1ce4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 07:51:28 -0800 Subject: [PATCH 053/203] save --- samples/ScheduleDemo/Program.cs | 2 +- samples/ScheduleDemo/ScheduleDemo.csproj | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index d013b68a..7ee82c4a 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -34,8 +34,8 @@ }); // Enable scheduled tasks support - builder.EnableScheduledTasksSupport(); builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); }); // Configure the client diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index 5aa3bc75..d748c86e 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -10,12 +10,15 @@ - - + + + + From 66dfaf4c77f6fcc33be1d738df8a68c63cd2579f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 08:56:02 -0800 Subject: [PATCH 054/203] save --- samples/ScheduleDemo/ScheduleDemo.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index d748c86e..9d28d20b 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -7,13 +7,13 @@ - - - + + + - + From 78dc92d478428559ad562a203871474be75efcd1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 09:02:41 -0800 Subject: [PATCH 055/203] save --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a1e78379..3933bd67 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + From c16fd50a5329ca6d9d7058b39224757cdf80cfb0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:40:30 -0800 Subject: [PATCH 056/203] save --- samples/ScheduleDemo/Program.cs | 130 ++++++++++-------- .../Converters/JsonDataConverter.cs | 2 + src/ScheduledTasks/Client/ScheduleHandle.cs | 2 +- .../Client/ScheduledTaskClient.cs | 11 +- src/ScheduledTasks/Entity/Schedule.cs | 12 +- src/ScheduledTasks/Models/ScheduleState.cs | 10 +- 6 files changed, 94 insertions(+), 73 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 7ee82c4a..b9cef07a 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -25,12 +25,32 @@ // Add a demo orchestration that will be triggered by the schedule r.AddOrchestratorFunc("DemoOrchestration", async context => { - string? input = context.GetInput(); - await context.CallActivityAsync("ProcessMessage", input); - return $"Completed processing at {DateTime.UtcNow}"; + const string stockSymbol = "MSFT"; // Hardcoded to Microsoft stock + var logger = context.CreateReplaySafeLogger("DemoOrchestration"); + logger.LogInformation("Getting stock price for: {symbol}", stockSymbol); + + try + { + // Get current stock price + decimal currentPrice = await context.CallActivityAsync("GetStockPrice", stockSymbol); + + logger.LogInformation("Current price for {symbol} is ${price:F2}", stockSymbol, currentPrice); + + return $"Stock {stockSymbol} price: ${currentPrice:F2} at {DateTime.UtcNow}"; + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing stock price for {symbol}", stockSymbol); + throw; + } + }); + + // Add required activities + r.AddActivityFunc("GetStockPrice", (context, symbol) => + { + // Mock implementation - would normally call stock API + return 100.00m; }); - // Add a demo activity - r.AddActivityFunc("ProcessMessage", (context, message) => $"Processing message: {message}"); }); // Enable scheduled tasks support @@ -67,64 +87,62 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule", - Interval = TimeSpan.FromSeconds(30), + ScheduleId = "demo-schedule1", + Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "This is a scheduled message!" }; // Create the schedule - Console.WriteLine("Creating schedule..."); IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); - Console.WriteLine($"Created schedule with ID: {scheduleHandle.ScheduleId}"); - + await Task.Delay(TimeSpan.FromSeconds(120)); // Monitor the schedule for a while - Console.WriteLine("\nMonitoring schedule for 2 minutes..."); - for (int i = 0; i < 4; i++) - { - await Task.Delay(TimeSpan.FromSeconds(30)); - var scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine($"\nSchedule status: {scheduleDescription.Status}"); - Console.WriteLine($"Last run at: {scheduleDescription.LastRunAt}"); - Console.WriteLine($"Next run at: {scheduleDescription.NextRunAt}"); - } - - // Pause the schedule - Console.WriteLine("\nPausing schedule..."); - await scheduleHandle.PauseAsync(); - - var pausedSchedule = await scheduleHandle.DescribeAsync(); - Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); - - // Resume the schedule - Console.WriteLine("\nResuming schedule..."); - await scheduleHandle.ResumeAsync(); - - var resumedSchedule = await scheduleHandle.DescribeAsync(); - Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); - Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); - - Console.WriteLine("\nPress any key to delete the schedule and exit..."); - Console.ReadKey(); - - // intentionally call schedule to trigger exceptions - await scheduleHandle.ResumeAsync(); - await scheduleHandle.ResumeAsync(); - - // Get schedule instance details - var scheduleInstanceDetails = await scheduleHandle.GetScheduleInstanceDetailsAsync(true); - Console.WriteLine($"\nSchedule instance details:"); - Console.WriteLine($"{scheduleInstanceDetails}"); - Console.WriteLine($"Instance ID: {scheduleInstanceDetails!.InstanceId}"); - Console.WriteLine($"Created time: {scheduleInstanceDetails.CreatedAt}"); - Console.WriteLine($"Name: {scheduleInstanceDetails.Name}"); - Console.WriteLine($"RuntimeStatus: {scheduleInstanceDetails.RuntimeStatus}"); - Console.WriteLine($"LastUpdatedAt: {scheduleInstanceDetails.LastUpdatedAt}"); - Console.WriteLine($"FailureDetails: {scheduleInstanceDetails.FailureDetails}"); - Console.WriteLine(); // Add blank line between instances - // Delete the schedule - await scheduleHandle.DeleteAsync(); - Console.WriteLine("Schedule deleted."); + //Console.WriteLine("\nMonitoring schedule for 2 minutes..."); + //for (int i = 0; i < 4; i++) + //{ + // await Task.Delay(TimeSpan.FromSeconds(30)); + // var scheduleDescription = await scheduleHandle.DescribeAsync(); + // Console.WriteLine($"\nSchedule status: {scheduleDescription.Status}"); + // Console.WriteLine($"Last run at: {scheduleDescription.LastRunAt}"); + // Console.WriteLine($"Next run at: {scheduleDescription.NextRunAt}"); + //} + + // // Pause the schedule + // Console.WriteLine("\nPausing schedule..."); + // await scheduleHandle.PauseAsync(); + + // var pausedSchedule = await scheduleHandle.DescribeAsync(); + // Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); + + // // Resume the schedule + // Console.WriteLine("\nResuming schedule..."); + // await scheduleHandle.ResumeAsync(); + + // var resumedSchedule = await scheduleHandle.DescribeAsync(); + // Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); + // Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); + + // Console.WriteLine("\nPress any key to delete the schedule and exit..."); + // Console.ReadKey(); + + // // intentionally call schedule to trigger exceptions + // await scheduleHandle.ResumeAsync(); + // await scheduleHandle.ResumeAsync(); + + // // Get schedule instance details + // var scheduleInstanceDetails = await scheduleHandle.GetScheduleInstanceDetailsAsync(true); + // Console.WriteLine($"\nSchedule instance details:"); + // Console.WriteLine($"{scheduleInstanceDetails}"); + // Console.WriteLine($"Instance ID: {scheduleInstanceDetails!.InstanceId}"); + // Console.WriteLine($"Created time: {scheduleInstanceDetails.CreatedAt}"); + // Console.WriteLine($"Name: {scheduleInstanceDetails.Name}"); + // Console.WriteLine($"RuntimeStatus: {scheduleInstanceDetails.RuntimeStatus}"); + // Console.WriteLine($"LastUpdatedAt: {scheduleInstanceDetails.LastUpdatedAt}"); + // Console.WriteLine($"FailureDetails: {scheduleInstanceDetails.FailureDetails}"); + // Console.WriteLine(); // Add blank line between instances + // // Delete the schedule + // await scheduleHandle.DeleteAsync(); + // Console.WriteLine("Schedule deleted."); } catch (Exception ex) { diff --git a/src/Abstractions/Converters/JsonDataConverter.cs b/src/Abstractions/Converters/JsonDataConverter.cs index eeca67a3..7e4d1467 100644 --- a/src/Abstractions/Converters/JsonDataConverter.cs +++ b/src/Abstractions/Converters/JsonDataConverter.cs @@ -35,6 +35,8 @@ public JsonDataConverter(JsonSerializerOptions? options = null) /// public override string? Serialize(object? value) { + // Console.WriteLine("Serializing value: " + value); + // Console.WriteLine("After serializaed value: " + JsonSerializer.Serialize(value, this.options)); return value != null ? JsonSerializer.Serialize(value, this.options) : null; } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 5ce03500..6c4306cf 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -41,8 +41,8 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge /// public async Task DescribeAsync() { - this.logger.ClientDescribingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); + this.logger.ClientDescribingSchedule(this.ScheduleId); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); EntityMetadata? metadata = diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 15fff171..9cbf5668 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -45,12 +45,11 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); // Check if schedule already exists - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata != null) - { - throw new ScheduleAlreadyExistException(scheduleConfigCreateOptions.ScheduleId); - } - + // EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + // if (metadata != null) + // { + // throw new ScheduleAlreadyExistException(scheduleConfigCreateOptions.ScheduleId); + // } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index f5e65446..a0de82e2 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -32,10 +32,12 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (this.State.Status != ScheduleStatus.Uninitialized) { - string errorMessage = "Schedule is already created."; - Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); - throw exception; + return; + + // string errorMessage = "Schedule is already created."; + // Exception exception = new InvalidOperationException(errorMessage); + // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); + // throw exception; } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); @@ -147,8 +149,8 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { - this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); if (this.State.ScheduleConfiguration.Interval == null) { string errorMessage = "Schedule interval must be specified."; diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 29cadb9a..35067939 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -11,27 +11,27 @@ class ScheduleState /// /// Gets or sets the current status of the schedule. /// - internal ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; + public ScheduleStatus Status { get; set; } = ScheduleStatus.Uninitialized; /// /// Gets or sets the execution token used to validate schedule operations. /// - internal string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); + public string ExecutionToken { get; set; } = Guid.NewGuid().ToString("N"); /// /// Gets or sets the last time the schedule was run. /// - internal DateTimeOffset? LastRunAt { get; set; } + public DateTimeOffset? LastRunAt { get; set; } /// /// Gets or sets the next scheduled run time. /// - internal DateTimeOffset? NextRunAt { get; set; } + public DateTimeOffset? NextRunAt { get; set; } /// /// Gets or sets the schedule configuration. /// - internal ScheduleConfiguration? ScheduleConfiguration { get; set; } + public ScheduleConfiguration? ScheduleConfiguration { get; set; } /// /// Refreshes the execution token to invalidate pending schedule operations. From a10cd16f814aa21782dc3422e913875d6a3d3491 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:54:52 -0800 Subject: [PATCH 057/203] save --- samples/ScheduleDemo/Program.cs | 15 ++--------- src/ScheduledTasks/Client/IScheduleHandle.cs | 10 -------- src/ScheduledTasks/Client/ScheduleHandle.cs | 20 --------------- src/ScheduledTasks/Entity/Schedule.cs | 26 +++++++++++++------- 4 files changed, 19 insertions(+), 52 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index b9cef07a..44ca4dc3 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -87,7 +87,7 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule1", + ScheduleId = "demo-schedule2", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "This is a scheduled message!" @@ -95,7 +95,6 @@ // Create the schedule IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); - await Task.Delay(TimeSpan.FromSeconds(120)); // Monitor the schedule for a while //Console.WriteLine("\nMonitoring schedule for 2 minutes..."); //for (int i = 0; i < 4; i++) @@ -129,17 +128,7 @@ // await scheduleHandle.ResumeAsync(); // await scheduleHandle.ResumeAsync(); - // // Get schedule instance details - // var scheduleInstanceDetails = await scheduleHandle.GetScheduleInstanceDetailsAsync(true); - // Console.WriteLine($"\nSchedule instance details:"); - // Console.WriteLine($"{scheduleInstanceDetails}"); - // Console.WriteLine($"Instance ID: {scheduleInstanceDetails!.InstanceId}"); - // Console.WriteLine($"Created time: {scheduleInstanceDetails.CreatedAt}"); - // Console.WriteLine($"Name: {scheduleInstanceDetails.Name}"); - // Console.WriteLine($"RuntimeStatus: {scheduleInstanceDetails.RuntimeStatus}"); - // Console.WriteLine($"LastUpdatedAt: {scheduleInstanceDetails.LastUpdatedAt}"); - // Console.WriteLine($"FailureDetails: {scheduleInstanceDetails.FailureDetails}"); - // Console.WriteLine(); // Add blank line between instances + await Task.Delay(TimeSpan.FromSeconds(120)); // // Delete the schedule // await scheduleHandle.DeleteAsync(); // Console.WriteLine("Schedule deleted."); diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 5b7e1ee4..babdd2f5 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -45,14 +45,4 @@ public interface IScheduleHandle /// The options for updating the schedule configuration. /// A task that completes when the schedule is updated. Task UpdateAsync(ScheduleUpdateOptions updateOptions); - - /// - /// Gets the details of the schedule's underlying orchestration instance. - /// - /// If true, includes the serialized inputs and outputs in the returned metadata. - /// Optional cancellation token. - /// The orchestration metadata for the schedule instance, or null if not found. - Task GetScheduleInstanceDetailsAsync( - bool getInputsAndOutputs = false, - CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 6c4306cf..5d7ef3ce 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -147,24 +147,4 @@ public async Task DeleteAsync() await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); } - - /// - /// Gets the details of the schedule's underlying orchestration instance. - /// - /// If true, includes the serialized inputs and outputs in the returned metadata. - /// Optional cancellation token. - /// The orchestration metadata for the schedule instance, or null if not found. - public async Task GetScheduleInstanceDetailsAsync( - bool getInputsAndOutputs = false, - CancellationToken cancellation = default) - { - this.logger.ClientGettingScheduleInstanceDetails(this.ScheduleId); - Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - - string instanceId = new EntityInstanceId(nameof(Schedule), this.ScheduleId).ToString(); - return await this.durableTaskClient.GetInstanceAsync( - instanceId, - getInputsAndOutputs, - cancellation); - } } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index a0de82e2..d5b309c9 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -32,12 +32,13 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (this.State.Status != ScheduleStatus.Uninitialized) { - return; + // this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Schedule is already created."); + // return; - // string errorMessage = "Schedule is already created."; - // Exception exception = new InvalidOperationException(errorMessage); - // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); - // throw exception; + string errorMessage = "Schedule is already created."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); + throw exception; } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); @@ -150,7 +151,7 @@ public void ResumeSchedule(TaskEntityContext context) public void RunSchedule(TaskEntityContext context, string executionToken) { Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); + // this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); if (this.State.ScheduleConfiguration.Interval == null) { string errorMessage = "Schedule interval must be specified."; @@ -206,7 +207,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; } - this.logger.CompletedScheduleRun(this.State.ScheduleConfiguration.ScheduleId); + // this.logger.CompletedScheduleRun(this.State.ScheduleConfiguration.ScheduleId); context.SignalEntity( new EntityInstanceId( nameof(Schedule), @@ -218,8 +219,15 @@ public void RunSchedule(TaskEntityContext context, string executionToken) void StartOrchestrationIfNotRunning(TaskEntityContext context) { - ScheduleConfiguration? config = this.State.ScheduleConfiguration; - context.ScheduleNewOrchestration(new TaskName(config!.OrchestrationName), config!.OrchestrationInput, new StartOrchestrationOptions(config!.OrchestrationInstanceId)); + try + { + ScheduleConfiguration? config = this.State.ScheduleConfiguration; + context.ScheduleNewOrchestration(new TaskName(config!.OrchestrationName), config!.OrchestrationInput, new StartOrchestrationOptions(config!.OrchestrationInstanceId)); + } + catch (Exception ex) + { + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.StartOrchestrationIfNotRunning), "Failed to start orchestration", ex); + } } void TryStatusTransition(ScheduleStatus to) From c39d8f2bcf1f01b2b68c5356a2d5622f147b1d5c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:34:19 -0800 Subject: [PATCH 058/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 7 - src/ScheduledTasks/Client/IScheduleWaiter.cs | 44 ++++++ src/ScheduledTasks/Client/ScheduleWaiter.cs | 142 +++++++++++++++++++ src/ScheduledTasks/Client/WaitOptions.cs | 44 ++++++ 4 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 src/ScheduledTasks/Client/IScheduleWaiter.cs create mode 100644 src/ScheduledTasks/Client/ScheduleWaiter.cs create mode 100644 src/ScheduledTasks/Client/WaitOptions.cs diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index babdd2f5..83440094 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask.Client; - namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -10,11 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public interface IScheduleHandle { - /// - /// Gets the ID of the schedule. - /// - string ScheduleId { get; } - /// /// Retrieves the current details of this schedule. /// diff --git a/src/ScheduledTasks/Client/IScheduleWaiter.cs b/src/ScheduledTasks/Client/IScheduleWaiter.cs new file mode 100644 index 00000000..ae866d85 --- /dev/null +++ b/src/ScheduledTasks/Client/IScheduleWaiter.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Provides waiter functionality for schedule state transitions. +/// +public interface IScheduleWaiter +{ + /// + /// Waits until the schedule is paused. + /// + /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. + /// Optional cancellation token. + /// The schedule description once paused. + Task WaitUntilPausedAsync( + WaitOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Waits until the schedule is active. + /// + /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. + /// Optional cancellation token. + /// The schedule description once active. + Task WaitUntilActiveAsync( + WaitOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Waits until the schedule is deleted. + /// + /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. + /// Optional cancellation token. + /// True if the schedule was deleted, false otherwise. + Task WaitUntilDeletedAsync( + WaitOptions? options = null, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs new file mode 100644 index 00000000..cd189e28 --- /dev/null +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DurableTask.Client; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Provides waiter functionality for schedule state transitions. +/// +public class ScheduleWaiter +{ + private readonly IScheduleHandle scheduleHandle; + private readonly TimeSpan defaultPollingInterval = TimeSpan.FromSeconds(5); + private readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(5); + + /// + /// Initializes a new instance of the class. + /// + /// The schedule handle to wait on. + public ScheduleWaiter(IScheduleHandle scheduleHandle) + { + this.scheduleHandle = scheduleHandle ?? throw new ArgumentNullException(nameof(scheduleHandle)); + } + + /// + /// Waits until the schedule is paused. + /// + /// Optional timeout duration. Defaults to 5 minutes. + /// Optional polling interval. Defaults to 5 seconds. + /// Optional cancellation token. + /// The schedule description once paused. + public Task WaitUntilPausedAsync( + TimeSpan? timeout = null, + TimeSpan? pollingInterval = null, + CancellationToken cancellationToken = default) + { + return WaitForStatusAsync(ScheduleStatus.Paused, timeout, pollingInterval, cancellationToken); + } + + /// + /// Waits until the schedule is running. + /// + /// Optional timeout duration. Defaults to 5 minutes. + /// Optional polling interval. Defaults to 5 seconds. + /// Optional cancellation token. + /// The schedule description once running. + public Task WaitUntilRunningAsync( + TimeSpan? timeout = null, + TimeSpan? pollingInterval = null, + CancellationToken cancellationToken = default) + { + return WaitForStatusAsync(ScheduleStatus.Running, timeout, pollingInterval, cancellationToken); + } + + /// + /// Waits until the schedule is deleted. + /// + /// Optional timeout duration. Defaults to 5 minutes. + /// Optional polling interval. Defaults to 5 seconds. + /// Optional cancellation token. + /// The schedule description once deleted. + public async Task WaitUntilDeletedAsync( + TimeSpan? timeout = null, + TimeSpan? pollingInterval = null, + CancellationToken cancellationToken = default) + { + timeout ??= defaultTimeout; + pollingInterval ??= defaultPollingInterval; + + using var timeoutCts = new CancellationTokenSource(timeout.Value); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + + try + { + while (!linkedCts.Token.IsCancellationRequested) + { + try + { + await this.scheduleHandle.Describe(); + await Task.Delay(pollingInterval.Value, linkedCts.Token); + } + catch (ScheduleNotFoundException) + { + return true; + } + } + + linkedCts.Token.ThrowIfCancellationRequested(); + throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to be deleted"); + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to be deleted"); + } + } + + /// + /// Waits until the schedule reaches the specified status. + /// + /// The status to wait for. + /// Optional timeout duration. Defaults to 5 minutes. + /// Optional polling interval. Defaults to 5 seconds. + /// Optional cancellation token. + /// The schedule description once the desired status is reached. + public async Task WaitForStatusAsync( + ScheduleStatus desiredStatus, + TimeSpan? timeout = null, + TimeSpan? pollingInterval = null, + CancellationToken cancellationToken = default) + { + timeout ??= defaultTimeout; + pollingInterval ??= defaultPollingInterval; + + using var timeoutCts = new CancellationTokenSource(timeout.Value); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + + try + { + while (!linkedCts.Token.IsCancellationRequested) + { + var description = await this.scheduleHandle.Describe(); + if (description.Status == desiredStatus) + { + return description; + } + + await Task.Delay(pollingInterval.Value, linkedCts.Token); + } + + linkedCts.Token.ThrowIfCancellationRequested(); + throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to reach status {desiredStatus}"); + } + catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) + { + throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to reach status {desiredStatus}"); + } + } +} \ No newline at end of file diff --git a/src/ScheduledTasks/Client/WaitOptions.cs b/src/ScheduledTasks/Client/WaitOptions.cs new file mode 100644 index 00000000..02cc02fc --- /dev/null +++ b/src/ScheduledTasks/Client/WaitOptions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Options for configuring wait behavior when waiting for schedule state transitions. +/// +public record WaitOptions +{ + /// + /// Gets the timeout duration for the wait operation. + /// If not specified, defaults to 5 minutes. + /// + public TimeSpan? Timeout { get; init; } + + /// + /// Gets the initial polling interval between status checks. + /// If not specified, defaults to 5 seconds. + /// + public TimeSpan? PollingInterval { get; init; } + + /// + /// Gets whether to use exponential backoff for polling intervals. + /// When enabled, the polling interval will increase exponentially between retries. + /// + public bool UseExponentialBackoff { get; init; } + + /// + /// Gets the maximum polling interval when using exponential backoff. + /// Only applicable when UseExponentialBackoff is true. + /// If not specified, defaults to 30 seconds. + /// + public TimeSpan? MaxPollingInterval { get; init; } + + /// + /// Gets the exponential backoff multiplier. + /// Only applicable when UseExponentialBackoff is true. + /// If not specified, defaults to 2.0. + /// + public double BackoffMultiplier { get; init; } = 2.0; +} From e99de8c3fbbe079229239487aa5efcfc96cfcd78 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:49:15 -0800 Subject: [PATCH 059/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 5 + src/ScheduledTasks/Client/ScheduleWaiter.cs | 127 +++++++++---------- 2 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 83440094..b3c10630 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -8,6 +8,11 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public interface IScheduleHandle { + /// + /// Gets the ID of this schedule. + /// + string ScheduleId { get; } + /// /// Retrieves the current details of this schedule. /// diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index cd189e28..0fa83d41 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -1,21 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.DurableTask.Client; - namespace Microsoft.DurableTask.ScheduledTasks; /// /// Provides waiter functionality for schedule state transitions. /// -public class ScheduleWaiter +public class ScheduleWaiter : IScheduleWaiter { - private readonly IScheduleHandle scheduleHandle; - private readonly TimeSpan defaultPollingInterval = TimeSpan.FromSeconds(5); - private readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(5); + readonly IScheduleHandle scheduleHandle; + readonly TimeSpan defaultPollingInterval = TimeSpan.FromSeconds(5); + readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(5); + readonly TimeSpan defaultMaxPollingInterval = TimeSpan.FromSeconds(30); /// /// Initializes a new instance of the class. @@ -26,53 +22,37 @@ public ScheduleWaiter(IScheduleHandle scheduleHandle) this.scheduleHandle = scheduleHandle ?? throw new ArgumentNullException(nameof(scheduleHandle)); } - /// - /// Waits until the schedule is paused. - /// - /// Optional timeout duration. Defaults to 5 minutes. - /// Optional polling interval. Defaults to 5 seconds. - /// Optional cancellation token. - /// The schedule description once paused. + /// public Task WaitUntilPausedAsync( - TimeSpan? timeout = null, - TimeSpan? pollingInterval = null, + WaitOptions? options = null, CancellationToken cancellationToken = default) { - return WaitForStatusAsync(ScheduleStatus.Paused, timeout, pollingInterval, cancellationToken); + return this.WaitForStatusAsync(ScheduleStatus.Paused, options, cancellationToken); } - /// - /// Waits until the schedule is running. - /// - /// Optional timeout duration. Defaults to 5 minutes. - /// Optional polling interval. Defaults to 5 seconds. - /// Optional cancellation token. - /// The schedule description once running. - public Task WaitUntilRunningAsync( - TimeSpan? timeout = null, - TimeSpan? pollingInterval = null, + /// + public Task WaitUntilActiveAsync( + WaitOptions? options = null, CancellationToken cancellationToken = default) { - return WaitForStatusAsync(ScheduleStatus.Running, timeout, pollingInterval, cancellationToken); + return this.WaitForStatusAsync(ScheduleStatus.Active, options, cancellationToken); } - /// - /// Waits until the schedule is deleted. - /// - /// Optional timeout duration. Defaults to 5 minutes. - /// Optional polling interval. Defaults to 5 seconds. - /// Optional cancellation token. - /// The schedule description once deleted. + /// public async Task WaitUntilDeletedAsync( - TimeSpan? timeout = null, - TimeSpan? pollingInterval = null, + WaitOptions? options = null, CancellationToken cancellationToken = default) { - timeout ??= defaultTimeout; - pollingInterval ??= defaultPollingInterval; + TimeSpan timeout = options?.Timeout ?? this.defaultTimeout; + TimeSpan pollingInterval = options?.PollingInterval ?? this.defaultPollingInterval; + TimeSpan maxPollingInterval = options?.MaxPollingInterval ?? this.defaultMaxPollingInterval; + bool useExponentialBackoff = options?.UseExponentialBackoff ?? false; + double backoffMultiplier = options?.BackoffMultiplier ?? 2.0; + + using CancellationTokenSource timeoutCts = new CancellationTokenSource(timeout); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); - using var timeoutCts = new CancellationTokenSource(timeout.Value); - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + TimeSpan currentPollingInterval = pollingInterval; try { @@ -80,8 +60,17 @@ public async Task WaitUntilDeletedAsync( { try { - await this.scheduleHandle.Describe(); - await Task.Delay(pollingInterval.Value, linkedCts.Token); + await this.scheduleHandle.DescribeAsync(); + + // Calculate next polling interval with exponential backoff if enabled + if (useExponentialBackoff) + { + currentPollingInterval = TimeSpan.FromTicks(Math.Min( + currentPollingInterval.Ticks * (long)backoffMultiplier, + maxPollingInterval.Ticks)); + } + + await Task.Delay(currentPollingInterval, linkedCts.Token); } catch (ScheduleNotFoundException) { @@ -90,53 +79,57 @@ public async Task WaitUntilDeletedAsync( } linkedCts.Token.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to be deleted"); + throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to be deleted"); } catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) { - throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to be deleted"); + throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to be deleted"); } } - /// - /// Waits until the schedule reaches the specified status. - /// - /// The status to wait for. - /// Optional timeout duration. Defaults to 5 minutes. - /// Optional polling interval. Defaults to 5 seconds. - /// Optional cancellation token. - /// The schedule description once the desired status is reached. - public async Task WaitForStatusAsync( + async Task WaitForStatusAsync( ScheduleStatus desiredStatus, - TimeSpan? timeout = null, - TimeSpan? pollingInterval = null, + WaitOptions? options = null, CancellationToken cancellationToken = default) { - timeout ??= defaultTimeout; - pollingInterval ??= defaultPollingInterval; + TimeSpan timeout = options?.Timeout ?? this.defaultTimeout; + TimeSpan pollingInterval = options?.PollingInterval ?? this.defaultPollingInterval; + TimeSpan maxPollingInterval = options?.MaxPollingInterval ?? this.defaultMaxPollingInterval; + bool useExponentialBackoff = options?.UseExponentialBackoff ?? false; + double backoffMultiplier = options?.BackoffMultiplier ?? 2.0; - using var timeoutCts = new CancellationTokenSource(timeout.Value); - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + using CancellationTokenSource timeoutCts = new CancellationTokenSource(timeout); + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); + + TimeSpan currentPollingInterval = pollingInterval; try { while (!linkedCts.Token.IsCancellationRequested) { - var description = await this.scheduleHandle.Describe(); + ScheduleDescription description = await this.scheduleHandle.DescribeAsync(); if (description.Status == desiredStatus) { return description; } - await Task.Delay(pollingInterval.Value, linkedCts.Token); + // Calculate next polling interval with exponential backoff if enabled + if (useExponentialBackoff) + { + currentPollingInterval = TimeSpan.FromTicks(Math.Min( + currentPollingInterval.Ticks * (long)backoffMultiplier, + maxPollingInterval.Ticks)); + } + + await Task.Delay(currentPollingInterval, linkedCts.Token); } linkedCts.Token.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to reach status {desiredStatus}"); + throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to reach status {desiredStatus}"); } catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) { - throw new TimeoutException($"Timed out waiting for schedule {scheduleHandle.ScheduleId} to reach status {desiredStatus}"); + throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to reach status {desiredStatus}"); } } -} \ No newline at end of file +} From eefa761f174f0179a07a41e60ef7a8721fe88b80 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:52:08 -0800 Subject: [PATCH 060/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 8 ++++---- src/ScheduledTasks/Client/IScheduleWaiter.cs | 6 +----- src/ScheduledTasks/Client/ScheduleHandle.cs | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index b3c10630..3b18f4d4 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -23,24 +23,24 @@ public interface IScheduleHandle /// Deletes this schedule. /// /// A task that completes when the schedule is deleted. - Task DeleteAsync(); + Task DeleteAsync(); /// /// Pauses this schedule. /// /// A task that completes when the schedule is paused. - Task PauseAsync(); + Task PauseAsync(); /// /// Resumes this schedule. /// /// A task that completes when the schedule is resumed. - Task ResumeAsync(); + Task ResumeAsync(); /// /// Updates this schedule with new configuration. /// /// The options for updating the schedule configuration. /// A task that completes when the schedule is updated. - Task UpdateAsync(ScheduleUpdateOptions updateOptions); + Task UpdateAsync(ScheduleUpdateOptions updateOptions); } diff --git a/src/ScheduledTasks/Client/IScheduleWaiter.cs b/src/ScheduledTasks/Client/IScheduleWaiter.cs index ae866d85..cb51c91e 100644 --- a/src/ScheduledTasks/Client/IScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/IScheduleWaiter.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Threading; -using System.Threading.Tasks; - namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -41,4 +37,4 @@ Task WaitUntilActiveAsync( Task WaitUntilDeletedAsync( WaitOptions? options = null, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 5d7ef3ce..cebeaec4 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -9,7 +9,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; // TODO: Validaiton -// TODO: GET if config is null what to return /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. From 6a2ea1863e9fd1d76f24c9708f8cfb3dd5915608 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:55:39 -0800 Subject: [PATCH 061/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 8 ++++---- src/ScheduledTasks/Client/ScheduleHandle.cs | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 3b18f4d4..f7ad2182 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -23,24 +23,24 @@ public interface IScheduleHandle /// Deletes this schedule. /// /// A task that completes when the schedule is deleted. - Task DeleteAsync(); + Task DeleteAsync(); /// /// Pauses this schedule. /// /// A task that completes when the schedule is paused. - Task PauseAsync(); + Task PauseAsync(); /// /// Resumes this schedule. /// /// A task that completes when the schedule is resumed. - Task ResumeAsync(); + Task ResumeAsync(); /// /// Updates this schedule with new configuration. /// /// The options for updating the schedule configuration. /// A task that completes when the schedule is updated. - Task UpdateAsync(ScheduleUpdateOptions updateOptions); + Task UpdateAsync(ScheduleUpdateOptions updateOptions); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index cebeaec4..c33a31c9 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -84,7 +84,7 @@ public async Task DescribeAsync() } /// - public async Task PauseAsync() + public async Task PauseAsync() { this.logger.ClientPausingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); @@ -97,10 +97,11 @@ public async Task PauseAsync() } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.PauseSchedule)); + return new ScheduleWaiter(this); } /// - public async Task ResumeAsync() + public async Task ResumeAsync() { this.logger.ClientResumingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); @@ -113,10 +114,11 @@ public async Task ResumeAsync() } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.ResumeSchedule)); + return new ScheduleWaiter(this); } /// - public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) + public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); @@ -129,10 +131,11 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); + return new ScheduleWaiter(this); } /// - public async Task DeleteAsync() + public async Task DeleteAsync() { this.logger.ClientDeletingSchedule(this.ScheduleId); Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); @@ -145,5 +148,6 @@ public async Task DeleteAsync() } await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); + return new ScheduleWaiter(this); } } From c5daf43ff3f3bb6cec6b508bbfecd1d445b3db16 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:53:19 -0800 Subject: [PATCH 062/203] save --- samples/ScheduleDemo/Program.cs | 16 ++----- .../Client/IScheduledTaskClient.cs | 4 +- src/ScheduledTasks/Client/ScheduleHandle.cs | 45 +++++-------------- .../Client/ScheduledTaskClient.cs | 14 +++--- src/ScheduledTasks/Logging/Client/Logs.cs | 10 ++++- 5 files changed, 31 insertions(+), 58 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 44ca4dc3..e41ca5c8 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -95,20 +95,12 @@ // Create the schedule IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); - // Monitor the schedule for a while - //Console.WriteLine("\nMonitoring schedule for 2 minutes..."); - //for (int i = 0; i < 4; i++) - //{ - // await Task.Delay(TimeSpan.FromSeconds(30)); - // var scheduleDescription = await scheduleHandle.DescribeAsync(); - // Console.WriteLine($"\nSchedule status: {scheduleDescription.Status}"); - // Console.WriteLine($"Last run at: {scheduleDescription.LastRunAt}"); - // Console.WriteLine($"Next run at: {scheduleDescription.NextRunAt}"); - //} + // // Pause the schedule // Console.WriteLine("\nPausing schedule..."); - // await scheduleHandle.PauseAsync(); + IScheduleWaiter waiter = await scheduleHandle.PauseAsync(); + await waiter.WaitUntilPausedAsync(); // var pausedSchedule = await scheduleHandle.DescribeAsync(); // Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); @@ -128,7 +120,7 @@ // await scheduleHandle.ResumeAsync(); // await scheduleHandle.ResumeAsync(); - await Task.Delay(TimeSpan.FromSeconds(120)); + //await Task.Delay(TimeSpan.FromSeconds(120)); // // Delete the schedule // await scheduleHandle.DeleteAsync(); // Console.WriteLine("Schedule deleted."); diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 85ec05e5..36f5e649 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -19,7 +19,7 @@ public interface IScheduledTaskClient /// Gets a list of all initialized schedules. /// /// A list of schedule descriptions. - Task> ListInitializedSchedulesAsync(); + Task> ListSchedulesAsync(); /// /// Creates a new schedule with the specified configuration. @@ -27,6 +27,4 @@ public interface IScheduledTaskClient /// The configuration options for creating the schedule. /// The ID of the newly created schedule. Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); - - // TODO: list uninitialized schedules? } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index c33a31c9..72a5e453 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -30,6 +30,7 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge this.durableTaskClient = client ?? throw new ArgumentNullException(nameof(client)); this.ScheduleId = scheduleId ?? throw new ArgumentNullException(nameof(scheduleId)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.EntityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); } /// @@ -37,6 +38,11 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge /// public string ScheduleId { get; } + /// + /// Gets the entity ID of the schedule. + /// + public EntityInstanceId EntityId { get; } + /// public async Task DescribeAsync() { @@ -87,16 +93,8 @@ public async Task DescribeAsync() public async Task PauseAsync() { this.logger.ClientPausingSchedule(this.ScheduleId); - Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata == null) - { - throw new ScheduleNotFoundException(this.ScheduleId); - } - - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.PauseSchedule)); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.PauseSchedule)); return new ScheduleWaiter(this); } @@ -104,16 +102,9 @@ public async Task PauseAsync() public async Task ResumeAsync() { this.logger.ClientResumingSchedule(this.ScheduleId); - Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata == null) - { - throw new ScheduleNotFoundException(this.ScheduleId); - } + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.ResumeSchedule)); - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.ResumeSchedule)); return new ScheduleWaiter(this); } @@ -121,16 +112,8 @@ public async Task ResumeAsync() public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { this.logger.ClientUpdatingSchedule(this.ScheduleId); - Check.NotNull(updateOptions, nameof(updateOptions)); - - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata == null) - { - throw new ScheduleNotFoundException(this.ScheduleId); - } - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.UpdateSchedule), updateOptions); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); return new ScheduleWaiter(this); } @@ -138,16 +121,8 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptio public async Task DeleteAsync() { this.logger.ClientDeletingSchedule(this.ScheduleId); - Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata == null) - { - throw new ScheduleNotFoundException(this.ScheduleId); - } - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, "delete"); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete"); return new ScheduleWaiter(this); } } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 9cbf5668..2199c712 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -45,18 +45,20 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); // Check if schedule already exists - // EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - // if (metadata != null) - // { - // throw new ScheduleAlreadyExistException(scheduleConfigCreateOptions.ScheduleId); - // } + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); + if (metadata != null) + { + this.logger.ClientWarning("Schedule with ID {ScheduleId} already exists. Returning existing handle.", scheduleConfigCreateOptions.ScheduleId); + return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); + } + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); } /// - public async Task> ListInitializedSchedulesAsync() + public async Task> ListSchedulesAsync() { this.logger.ClientListingSchedules(); EntityQuery query = new EntityQuery diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 1cc33a98..86b3f60a 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -38,6 +38,12 @@ static partial class Logs [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Client: Deleting schedule '{scheduleId}'")] public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Client: Getting instance details for schedule '{scheduleId}'")] - public static partial void ClientGettingScheduleInstanceDetails(this ILogger logger, string scheduleId); + [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Client: {message} (ScheduleId: {scheduleId})")] + public static partial void ClientInfo(this ILogger logger, string message, string scheduleId); + + [LoggerMessage(EventId = 10, Level = LogLevel.Warning, Message = "Client Warning: {message} (ScheduleId: {scheduleId})")] + public static partial void ClientWarning(this ILogger logger, string message, string scheduleId); + + [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "Client Error: {message} (ScheduleId: {scheduleId})")] + public static partial void ClientError(this ILogger logger, string message, string scheduleId); } From 6f19602f0d4720ada55d2df11ad4830c90fdb00a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:57:39 -0800 Subject: [PATCH 063/203] save --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 2199c712..08e098ed 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -44,14 +44,6 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); - // Check if schedule already exists - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId); - if (metadata != null) - { - this.logger.ClientWarning("Schedule with ID {ScheduleId} already exists. Returning existing handle.", scheduleConfigCreateOptions.ScheduleId); - return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); - } - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); From 4dce0b0288d3f70059a4ec450fdd001d28cb1945 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:12:29 -0800 Subject: [PATCH 064/203] save --- src/ScheduledTasks/Models/ScheduleStatus.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index 94e0c191..0f53b259 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -22,4 +22,9 @@ public enum ScheduleStatus /// Schedule is paused. /// Paused, + + /// + /// Schedule is being deleted. + /// + Failed, } From bec67a1c262ca032c00f1d4a20434bec1f68e616 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:40:31 -0800 Subject: [PATCH 065/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 3 +- .../Client/IScheduledTaskClient.cs | 3 +- src/ScheduledTasks/Client/ScheduleHandle.cs | 13 ++++- .../Client/ScheduledTaskClient.cs | 12 +++- src/ScheduledTasks/Entity/Schedule.cs | 54 +++++++++++++++--- .../Models/ScheduleActivityLog.cs | 56 +++++++++++++++++++ .../Models/ScheduleDescription.cs | 5 +- src/ScheduledTasks/Models/ScheduleState.cs | 37 ++++++++++++ 8 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 src/ScheduledTasks/Models/ScheduleActivityLog.cs diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index f7ad2182..a1f1ec01 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -16,8 +16,9 @@ public interface IScheduleHandle /// /// Retrieves the current details of this schedule. /// + /// Whether to include full activity logs in the returned schedule details. /// The schedule details. - Task DescribeAsync(); + Task DescribeAsync(bool includeFullActivityLogs); /// /// Deletes this schedule. diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 36f5e649..0349be80 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -18,8 +18,9 @@ public interface IScheduledTaskClient /// /// Gets a list of all initialized schedules. /// + /// Whether to include full activity logs in the returned schedules. /// A list of schedule descriptions. - Task> ListSchedulesAsync(); + Task> ListSchedulesAsync(bool includeFullActivityLogs); /// /// Creates a new schedule with the specified configuration. diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 72a5e453..29cbc949 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Threading.Tasks; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; @@ -44,7 +46,7 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge public EntityInstanceId EntityId { get; } /// - public async Task DescribeAsync() + public async Task DescribeAsync(bool includeFullActivityLogs = false) { Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); this.logger.ClientDescribingSchedule(this.ScheduleId); @@ -72,6 +74,12 @@ public async Task DescribeAsync() $"Schedule configuration is not available even though the schedule status is {state.Status}."); } + IReadOnlyCollection activityLogs = state.ActivityLogs; + if (!includeFullActivityLogs && activityLogs.Any()) + { + activityLogs = new ScheduleActivityLog[] { activityLogs.Last() }; + } + return new ScheduleDescription( this.ScheduleId, config.OrchestrationName, @@ -86,7 +94,8 @@ public async Task DescribeAsync() state.Status, state.ExecutionToken, state.LastRunAt, - state.NextRunAt); + state.NextRunAt, + activityLogs); } /// diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 08e098ed..5497be8e 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -50,7 +50,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions s } /// - public async Task> ListSchedulesAsync() + public async Task> ListSchedulesAsync(bool includeFullActivityLogs = false) { this.logger.ClientListingSchedules(); EntityQuery query = new EntityQuery @@ -61,11 +61,16 @@ public async Task> ListSchedulesAsync() List schedules = new List(); - await foreach (var metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) { if (metadata.State.Status != ScheduleStatus.Uninitialized) { ScheduleConfiguration config = metadata.State.ScheduleConfiguration!; + IReadOnlyCollection activityLogs = metadata.State.ActivityLogs; + if (!includeFullActivityLogs && activityLogs.Any()) + { + activityLogs = new[] { activityLogs.Last() }; + } schedules.Add(new ScheduleDescription( metadata.Id.Key, config.OrchestrationName, @@ -80,7 +85,8 @@ public async Task> ListSchedulesAsync() metadata.State.Status, metadata.State.ExecutionToken, metadata.State.LastRunAt, - metadata.State.NextRunAt)); + metadata.State.NextRunAt, + activityLogs)); } } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index d5b309c9..134bd628 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; @@ -32,12 +33,15 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (this.State.Status != ScheduleStatus.Uninitialized) { - // this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Schedule is already created."); - // return; - string errorMessage = "Schedule is already created."; Exception exception = new InvalidOperationException(errorMessage); this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); + this.State.AddActivityLog("Create", "Failed", new FailureDetails + { + Reason = errorMessage, + Type = "InvalidOperation", + OccurredAt = DateTimeOffset.UtcNow + }); throw exception; } @@ -45,6 +49,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc this.TryStatusTransition(ScheduleStatus.Active); this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog("Create", "Success"); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities @@ -99,11 +104,21 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche /// /// Pauses the schedule. /// - public void PauseSchedule() + public void PauseSchedule(TaskEntityContext context) { + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); if (this.State.Status != ScheduleStatus.Active) { - throw new InvalidOperationException("Schedule must be in Active status to pause."); + string errorMessage = "Schedule must be in Active state to pause."; + Exception exception = new InvalidOperationException(errorMessage); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.PauseSchedule), errorMessage, exception); + this.State.AddActivityLog("Pause", "Failed", new FailureDetails + { + Reason = errorMessage, + Type = "InvalidOperation", + OccurredAt = DateTimeOffset.UtcNow + }); + throw exception; } // Transition to Paused state @@ -111,7 +126,8 @@ public void PauseSchedule() this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); - this.logger.PausedSchedule(this.State.ScheduleConfiguration!.ScheduleId); + this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog("Pause", "Success"); } /// @@ -127,12 +143,19 @@ public void ResumeSchedule(TaskEntityContext context) string errorMessage = "Schedule must be in Paused state to resume."; Exception exception = new InvalidOperationException(errorMessage); this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.ResumeSchedule), errorMessage, exception); + this.State.AddActivityLog("Resume", "Failed", new FailureDetails + { + Reason = errorMessage, + Type = "InvalidOperation", + OccurredAt = DateTimeOffset.UtcNow + }); throw exception; } this.TryStatusTransition(ScheduleStatus.Active); this.State.NextRunAt = null; this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog("Resume", "Success"); // compute next run based on startat and interval context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); @@ -151,18 +174,29 @@ public void ResumeSchedule(TaskEntityContext context) public void RunSchedule(TaskEntityContext context, string executionToken) { Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - // this.logger.RunningSchedule(this.State.ScheduleConfiguration!.ScheduleId); if (this.State.ScheduleConfiguration.Interval == null) { string errorMessage = "Schedule interval must be specified."; Exception exception = new InvalidOperationException(errorMessage); this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); + this.State.AddActivityLog("Run", "Failed", new FailureDetails + { + Reason = errorMessage, + Type = "InvalidConfiguration", + OccurredAt = DateTimeOffset.UtcNow + }); throw exception; } if (executionToken != this.State.ExecutionToken) { this.logger.ScheduleRunCancelled(this.State.ScheduleConfiguration.ScheduleId, executionToken); + this.State.AddActivityLog("Run", "Cancelled", new FailureDetails + { + Reason = "Execution token mismatch", + Type = "TokenExpired", + OccurredAt = DateTimeOffset.UtcNow + }); return; } @@ -171,6 +205,12 @@ public void RunSchedule(TaskEntityContext context, string executionToken) string errorMessage = "Schedule must be in Active status to run."; Exception exception = new InvalidOperationException(errorMessage); this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); + this.State.AddActivityLog("Run", "Failed", new FailureDetails + { + Reason = errorMessage, + Type = "InvalidOperation", + OccurredAt = DateTimeOffset.UtcNow + }); throw exception; } diff --git a/src/ScheduledTasks/Models/ScheduleActivityLog.cs b/src/ScheduledTasks/Models/ScheduleActivityLog.cs new file mode 100644 index 00000000..6f8b487a --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleActivityLog.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents a log entry for schedule activity. +/// +public class ScheduleActivityLog +{ + /// + /// Gets or sets the operation performed. + /// + public string Operation { get; set; } = string.Empty; + + /// + /// Gets or sets the status of the operation. + /// + public string Status { get; set; } = string.Empty; + + /// + /// Gets or sets the timestamp when the operation occurred. + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Gets or sets the failure details if the operation failed. + /// + public FailureDetails? FailureDetails { get; set; } +} + +/// +/// Represents details about a failure that occurred during schedule execution. +/// +public class FailureDetails +{ + /// + /// Gets or sets the reason for the failure. + /// + public string Reason { get; set; } = string.Empty; + + /// + /// Gets or sets the type of failure. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets when the failure occurred. + /// + public DateTimeOffset OccurredAt { get; set; } + + /// + /// Gets or sets the suggested fix for the failure. + /// + public string? SuggestedFix { get; set; } +} \ No newline at end of file diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index f4dfb267..9664a539 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -20,4 +22,5 @@ public record ScheduleDescription( ScheduleStatus Status, string ExecutionToken, DateTimeOffset? LastRunAt, - DateTimeOffset? NextRunAt); + DateTimeOffset? NextRunAt, + IReadOnlyCollection ActivityLogs); diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 35067939..b49d363f 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Linq; + namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -8,6 +12,9 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleState { + private const int MaxActivityLogItems = 100; + private readonly Queue activityLogs = new(); + /// /// Gets or sets the current status of the schedule. /// @@ -33,6 +40,11 @@ class ScheduleState /// public ScheduleConfiguration? ScheduleConfiguration { get; set; } + /// + /// Gets the activity logs for this schedule. + /// + public IReadOnlyCollection ActivityLogs => this.activityLogs.ToList().AsReadOnly(); + /// /// Refreshes the execution token to invalidate pending schedule operations. /// @@ -40,4 +52,29 @@ public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } + + /// + /// Adds an activity log entry to the schedule's history. + /// + /// The operation being performed. + /// The status of the operation. + /// Optional failure details if the operation failed. + public void AddActivityLog(string operation, string status, FailureDetails? failureDetails = null) + { + var log = new ScheduleActivityLog + { + Operation = operation, + Status = status, + Timestamp = DateTimeOffset.UtcNow, + FailureDetails = failureDetails + }; + + this.activityLogs.Enqueue(log); + + // Keep only the most recent MaxActivityLogItems + while (this.activityLogs.Count > MaxActivityLogItems) + { + this.activityLogs.Dequeue(); + } + } } From 8d28be45ddb0b6343c6d04e57aba4a84a490217e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:51:05 -0800 Subject: [PATCH 066/203] save --- src/ScheduledTasks/Client/IScheduleHandle.cs | 2 +- src/ScheduledTasks/Client/ScheduleHandle.cs | 4 +- .../Client/ScheduledTaskClient.cs | 3 +- src/ScheduledTasks/Entity/Schedule.cs | 13 +++---- .../Models/ScheduleActivityLog.cs | 38 +++++++++---------- .../Models/ScheduleDescription.cs | 2 - src/ScheduledTasks/Models/ScheduleState.cs | 12 ++---- 7 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index a1f1ec01..60e5b2fe 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -18,7 +18,7 @@ public interface IScheduleHandle /// /// Whether to include full activity logs in the returned schedule details. /// The schedule details. - Task DescribeAsync(bool includeFullActivityLogs); + Task DescribeAsync(bool includeFullActivityLogs = false); /// /// Deletes this schedule. diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 29cbc949..0c0d9a02 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Threading.Tasks; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; @@ -75,7 +73,7 @@ public async Task DescribeAsync(bool includeFullActivityLog } IReadOnlyCollection activityLogs = state.ActivityLogs; - if (!includeFullActivityLogs && activityLogs.Any()) + if (!includeFullActivityLogs && activityLogs.Count != 0) { activityLogs = new ScheduleActivityLog[] { activityLogs.Last() }; } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 5497be8e..4e60ac6e 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -67,10 +67,11 @@ public async Task> ListSchedulesAsync(bool incl { ScheduleConfiguration config = metadata.State.ScheduleConfiguration!; IReadOnlyCollection activityLogs = metadata.State.ActivityLogs; - if (!includeFullActivityLogs && activityLogs.Any()) + if (!includeFullActivityLogs && activityLogs.Count != 0) { activityLogs = new[] { activityLogs.Last() }; } + schedules.Add(new ScheduleDescription( metadata.Id.Key, config.OrchestrationName, diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 134bd628..c7a18548 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; @@ -40,7 +39,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc { Reason = errorMessage, Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); throw exception; } @@ -116,7 +115,7 @@ public void PauseSchedule(TaskEntityContext context) { Reason = errorMessage, Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); throw exception; } @@ -147,7 +146,7 @@ public void ResumeSchedule(TaskEntityContext context) { Reason = errorMessage, Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); throw exception; } @@ -183,7 +182,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { Reason = errorMessage, Type = "InvalidConfiguration", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); throw exception; } @@ -195,7 +194,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { Reason = "Execution token mismatch", Type = "TokenExpired", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); return; } @@ -209,7 +208,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { Reason = errorMessage, Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow + OccurredAt = DateTimeOffset.UtcNow, }); throw exception; } diff --git a/src/ScheduledTasks/Models/ScheduleActivityLog.cs b/src/ScheduledTasks/Models/ScheduleActivityLog.cs index 6f8b487a..a4478614 100644 --- a/src/ScheduledTasks/Models/ScheduleActivityLog.cs +++ b/src/ScheduledTasks/Models/ScheduleActivityLog.cs @@ -6,51 +6,51 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents a log entry for schedule activity. /// -public class ScheduleActivityLog +public record ScheduleActivityLog { /// - /// Gets or sets the operation performed. + /// Gets the operation performed. /// - public string Operation { get; set; } = string.Empty; + public string Operation { get; init; } = string.Empty; /// - /// Gets or sets the status of the operation. + /// Gets the status of the operation. /// - public string Status { get; set; } = string.Empty; + public string Status { get; init; } = string.Empty; /// - /// Gets or sets the timestamp when the operation occurred. + /// Gets the timestamp when the operation occurred. /// - public DateTimeOffset Timestamp { get; set; } + public DateTimeOffset Timestamp { get; init; } /// - /// Gets or sets the failure details if the operation failed. + /// Gets the failure details if the operation failed. /// - public FailureDetails? FailureDetails { get; set; } + public FailureDetails? FailureDetails { get; init; } } /// /// Represents details about a failure that occurred during schedule execution. /// -public class FailureDetails +public record FailureDetails { /// - /// Gets or sets the reason for the failure. + /// Gets the reason for the failure. /// - public string Reason { get; set; } = string.Empty; + public string Reason { get; init; } = string.Empty; /// - /// Gets or sets the type of failure. + /// Gets the type of failure. /// - public string Type { get; set; } = string.Empty; + public string Type { get; init; } = string.Empty; /// - /// Gets or sets when the failure occurred. + /// Gets when the failure occurred. /// - public DateTimeOffset OccurredAt { get; set; } + public DateTimeOffset OccurredAt { get; init; } /// - /// Gets or sets the suggested fix for the failure. + /// Gets the suggested fix for the failure. /// - public string? SuggestedFix { get; set; } -} \ No newline at end of file + public string? SuggestedFix { get; init; } +} \ No newline at end of file diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 9664a539..67d68277 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Microsoft.DurableTask.ScheduledTasks; /// diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index b49d363f..d82e09f8 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; - namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -12,8 +8,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleState { - private const int MaxActivityLogItems = 100; - private readonly Queue activityLogs = new(); + const int MaxActivityLogItems = 100; + readonly Queue activityLogs = new(); /// /// Gets or sets the current status of the schedule. @@ -61,12 +57,12 @@ public void RefreshScheduleRunExecutionToken() /// Optional failure details if the operation failed. public void AddActivityLog(string operation, string status, FailureDetails? failureDetails = null) { - var log = new ScheduleActivityLog + ScheduleActivityLog log = new ScheduleActivityLog { Operation = operation, Status = status, Timestamp = DateTimeOffset.UtcNow, - FailureDetails = failureDetails + FailureDetails = failureDetails, }; this.activityLogs.Enqueue(log); From 07a7c0d441722fe2a4e23d18044145e260eea1e3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:53:49 -0800 Subject: [PATCH 067/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 0c0d9a02..7d293f02 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -72,11 +72,8 @@ public async Task DescribeAsync(bool includeFullActivityLog $"Schedule configuration is not available even though the schedule status is {state.Status}."); } - IReadOnlyCollection activityLogs = state.ActivityLogs; - if (!includeFullActivityLogs && activityLogs.Count != 0) - { - activityLogs = new ScheduleActivityLog[] { activityLogs.Last() }; - } + IReadOnlyCollection activityLogs = + includeFullActivityLogs ? state.ActivityLogs : state.ActivityLogs.TakeLast(1).ToArray(); return new ScheduleDescription( this.ScheduleId, From 5d556ad4f8c4e20920734a9cc710806418f362b6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:00:37 -0800 Subject: [PATCH 068/203] save --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 4e60ac6e..bcccffae 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -66,11 +66,9 @@ public async Task> ListSchedulesAsync(bool incl if (metadata.State.Status != ScheduleStatus.Uninitialized) { ScheduleConfiguration config = metadata.State.ScheduleConfiguration!; - IReadOnlyCollection activityLogs = metadata.State.ActivityLogs; - if (!includeFullActivityLogs && activityLogs.Count != 0) - { - activityLogs = new[] { activityLogs.Last() }; - } + + IReadOnlyCollection activityLogs = + includeFullActivityLogs ? metadata.State.ActivityLogs : metadata.State.ActivityLogs.TakeLast(1).ToArray(); schedules.Add(new ScheduleDescription( metadata.Id.Key, From 8eb0f4b08a63e92ecaf263da659e60b89d07c136 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:40:56 -0800 Subject: [PATCH 069/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 41 ++++---- src/ScheduledTasks/Client/ScheduleWaiter.cs | 16 +++- .../ScheduleOperationFailedException.cs | 47 ++++++++++ .../Models/ScheduleDescription.cs | 93 +++++++++++++++---- 4 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 7d293f02..1565b6e6 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -63,34 +63,29 @@ public async Task DescribeAsync(bool includeFullActivityLog throw new ScheduleStillBeingProvisionedException(this.ScheduleId); } - // this should never happen ScheduleConfiguration? config = state.ScheduleConfiguration; - if (config == null) - { - throw new ScheduleInternalException( - this.ScheduleId, - $"Schedule configuration is not available even though the schedule status is {state.Status}."); - } IReadOnlyCollection activityLogs = includeFullActivityLogs ? state.ActivityLogs : state.ActivityLogs.TakeLast(1).ToArray(); - return new ScheduleDescription( - this.ScheduleId, - config.OrchestrationName, - config.OrchestrationInput, - config.OrchestrationInstanceId, - config.StartAt, - config.EndAt, - config.Interval, - config.CronExpression, - config.MaxOccurrence, - config.StartImmediatelyIfLate, - state.Status, - state.ExecutionToken, - state.LastRunAt, - state.NextRunAt, - activityLogs); + return new ScheduleDescription + { + ScheduleId = this.ScheduleId, + OrchestrationName = config?.OrchestrationName, + OrchestrationInput = config?.OrchestrationInput, + OrchestrationInstanceId = config?.OrchestrationInstanceId, + StartAt = config?.StartAt, + EndAt = config?.EndAt, + Interval = config?.Interval, + CronExpression = config?.CronExpression, + MaxOccurrence = config?.MaxOccurrence ?? 0, + StartImmediatelyIfLate = config?.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + ActivityLogs = activityLogs, + }; } /// diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index 0fa83d41..0ecf97c7 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -107,10 +107,20 @@ async Task WaitForStatusAsync( { while (!linkedCts.Token.IsCancellationRequested) { - ScheduleDescription description = await this.scheduleHandle.DescribeAsync(); - if (description.Status == desiredStatus) + try { - return description; + ScheduleDescription description = await this.scheduleHandle.DescribeAsync(); + if (description.Status == desiredStatus) + { + return description; + } + } + catch (ScheduleStillBeingProvisionedException) + { + if (desiredStatus != ScheduleStatus.Active) + { + throw; + } } // Calculate next polling interval with exponential backoff if enabled diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs new file mode 100644 index 00000000..7f6a8a04 --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when a schedule operation fails. +/// +public class ScheduleOperationFailedException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed. + /// The operation that failed. + /// The error message that explains the reason for the failure. + public ScheduleOperationFailedException(string scheduleId, string operation, string message) + : base($"Operation '{operation}' failed for schedule '{scheduleId}': {message}") + { + this.ScheduleId = scheduleId; + this.Operation = operation; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed. + /// The operation that failed. + /// The error message that explains the reason for the failure. + /// The exception that is the cause of the current exception. + public ScheduleOperationFailedException(string scheduleId, string operation, string message, Exception innerException) + : base($"Operation '{operation}' failed for schedule '{scheduleId}': {message}", innerException) + { + this.ScheduleId = scheduleId; + this.Operation = operation; + } + + /// + /// Gets the ID of the schedule that failed. + /// + public string ScheduleId { get; } + + /// + /// Gets the operation that failed. + /// + public string Operation { get; } +} diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 67d68277..c6bf4b8d 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -6,19 +6,80 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents the comprehensive details of a schedule. /// -public record ScheduleDescription( - string ScheduleId, - string OrchestrationName, - string? OrchestrationInput, - string? OrchestrationInstanceId, - DateTimeOffset? StartAt, - DateTimeOffset? EndAt, - TimeSpan? Interval, - string? CronExpression, - int MaxOccurrence, - bool? StartImmediatelyIfLate, - ScheduleStatus Status, - string ExecutionToken, - DateTimeOffset? LastRunAt, - DateTimeOffset? NextRunAt, - IReadOnlyCollection ActivityLogs); +public record ScheduleDescription +{ + /// + /// Gets the unique identifier for the schedule. + /// + public string ScheduleId { get; init; } = string.Empty; + + /// + /// Gets the name of the orchestration to run. + /// + public string? OrchestrationName { get; init; }; + + /// + /// Gets the optional input for the orchestration. + /// + public string? OrchestrationInput { get; init; } + + /// + /// Gets the optional instance ID for the orchestration. + /// + public string? OrchestrationInstanceId { get; init; } + + /// + /// Gets the optional start time for the schedule. + /// + public DateTimeOffset? StartAt { get; init; } + + /// + /// Gets the optional end time for the schedule. + /// + public DateTimeOffset? EndAt { get; init; } + + /// + /// Gets the optional interval between schedule runs. + /// + public TimeSpan? Interval { get; init; } + + /// + /// Gets the optional cron expression for the schedule. + /// + public string? CronExpression { get; init; } + + /// + /// Gets the maximum number of times the schedule should run. + /// + public int MaxOccurrence { get; init; } + + /// + /// Gets whether the schedule should run immediately if started late. + /// + public bool? StartImmediatelyIfLate { get; init; } + + /// + /// Gets the current status of the schedule. + /// + public ScheduleStatus Status { get; init; } + + /// + /// Gets the execution token used to validate schedule operations. + /// + public string ExecutionToken { get; init; } = string.Empty; + + /// + /// Gets the last time the schedule was run. + /// + public DateTimeOffset? LastRunAt { get; init; } + + /// + /// Gets the next scheduled run time. + /// + public DateTimeOffset? NextRunAt { get; init; } + + /// + /// Gets the activity logs for this schedule. + /// + public IReadOnlyCollection ActivityLogs { get; init; } = Array.Empty(); +} From 6812fb18eb848e0d88d4f7bf67b027e1fcbefedc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:23:26 -0800 Subject: [PATCH 070/203] save --- src/ScheduledTasks/Client/ScheduleWaiter.cs | 5 ++++ .../ScheduleOperationFailedException.cs | 30 ++++++++----------- .../Models/ScheduleDescription.cs | 8 ++++- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index 0ecf97c7..3a14168c 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -114,6 +114,11 @@ async Task WaitForStatusAsync( { return description; } + + if (description.Status == ScheduleStatus.Failed) + { + throw new ScheduleOperationFailedException(description); + } } catch (ScheduleStillBeingProvisionedException) { diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs index 7f6a8a04..8f0b8033 100644 --- a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs +++ b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs @@ -11,37 +11,31 @@ public class ScheduleOperationFailedException : Exception /// /// Initializes a new instance of the class. /// - /// The ID of the schedule that failed. - /// The operation that failed. - /// The error message that explains the reason for the failure. - public ScheduleOperationFailedException(string scheduleId, string operation, string message) - : base($"Operation '{operation}' failed for schedule '{scheduleId}': {message}") + /// The schedule that failed. + public ScheduleOperationFailedException(ScheduleDescription schedule) + : base($"Operation failed for schedule '{schedule.ScheduleId}'. Refer to schedule details {schedule.ToJsonString()} for more information.") { - this.ScheduleId = scheduleId; - this.Operation = operation; + this.Schedule = schedule; } /// /// Initializes a new instance of the class. /// - /// The ID of the schedule that failed. - /// The operation that failed. - /// The error message that explains the reason for the failure. + /// The schedule that failed. /// The exception that is the cause of the current exception. - public ScheduleOperationFailedException(string scheduleId, string operation, string message, Exception innerException) - : base($"Operation '{operation}' failed for schedule '{scheduleId}': {message}", innerException) + public ScheduleOperationFailedException(ScheduleDescription schedule, Exception innerException) + : base($"Operation failed for schedule '{schedule.ScheduleId}'. Refer to schedule details {schedule.ToJsonString()} for more information.", innerException) { - this.ScheduleId = scheduleId; - this.Operation = operation; + this.Schedule = schedule; } /// - /// Gets the ID of the schedule that failed. + /// Gets the schedule that failed. /// - public string ScheduleId { get; } + public ScheduleDescription Schedule { get; } /// - /// Gets the operation that failed. + /// Gets the ID of the schedule that failed. /// - public string Operation { get; } + public string ScheduleId => this.Schedule.ScheduleId; } diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index c6bf4b8d..a0b4d7b1 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -16,7 +16,7 @@ public record ScheduleDescription /// /// Gets the name of the orchestration to run. /// - public string? OrchestrationName { get; init; }; + public string? OrchestrationName { get; init; } /// /// Gets the optional input for the orchestration. @@ -82,4 +82,10 @@ public record ScheduleDescription /// Gets the activity logs for this schedule. /// public IReadOnlyCollection ActivityLogs { get; init; } = Array.Empty(); + + /// + /// Returns a JSON string representation of the schedule description. + /// + /// A JSON string containing the schedule details. + public string ToJsonString() => System.Text.Json.JsonSerializer.Serialize(this); } From cc130f2e56ff6bead7d17112ed5f4b73dc65914b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:27:00 -0800 Subject: [PATCH 071/203] save --- .../Client/ScheduledTaskClient.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index bcccffae..2feebe52 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -70,22 +70,24 @@ public async Task> ListSchedulesAsync(bool incl IReadOnlyCollection activityLogs = includeFullActivityLogs ? metadata.State.ActivityLogs : metadata.State.ActivityLogs.TakeLast(1).ToArray(); - schedules.Add(new ScheduleDescription( - metadata.Id.Key, - config.OrchestrationName, - config.OrchestrationInput, - config.OrchestrationInstanceId, - config.StartAt, - config.EndAt, - config.Interval, - config.CronExpression, - config.MaxOccurrence, - config.StartImmediatelyIfLate, - metadata.State.Status, - metadata.State.ExecutionToken, - metadata.State.LastRunAt, - metadata.State.NextRunAt, - activityLogs)); + schedules.Add(new ScheduleDescription + { + ScheduleId = metadata.Id.Key, + OrchestrationName = config.OrchestrationName, + OrchestrationInput = config.OrchestrationInput, + OrchestrationInstanceId = config.OrchestrationInstanceId, + StartAt = config.StartAt, + EndAt = config.EndAt, + Interval = config.Interval, + CronExpression = config.CronExpression, + MaxOccurrence = config.MaxOccurrence, + StartImmediatelyIfLate = config.StartImmediatelyIfLate, + Status = metadata.State.Status, + ExecutionToken = metadata.State.ExecutionToken, + LastRunAt = metadata.State.LastRunAt, + NextRunAt = metadata.State.NextRunAt, + ActivityLogs = activityLogs, + }); } } From 6a46b3a8ca27bb902ef679ce622ab06f4b3ab73f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:54:15 -0800 Subject: [PATCH 072/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 82 +++++++++++++------ .../ScheduleClientValidationException.cs | 26 ++++++ .../ScheduleInvalidTransitionException.cs | 39 +++++++++ .../Models/ScheduleOperationFailureType.cs | 30 +++++++ .../Models/ScheduleOperationStatus.cs | 20 +++++ src/ScheduledTasks/Models/ScheduleState.cs | 7 ++ src/ScheduledTasks/Models/ScheduleStatus.cs | 5 -- .../Models/ScheduleTransitions.cs | 44 ++++++++-- 8 files changed, 216 insertions(+), 37 deletions(-) create mode 100644 src/ScheduledTasks/Exception/ScheduleClientValidationException.cs create mode 100644 src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs create mode 100644 src/ScheduledTasks/Models/ScheduleOperationFailureType.cs create mode 100644 src/ScheduledTasks/Models/ScheduleOperationStatus.cs diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index c7a18548..bd07c40f 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -28,31 +28,59 @@ class Schedule(ILogger logger) : TaskEntity /// Thrown when the schedule is already created. public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions scheduleCreationOptions) { - Verify.NotNull(scheduleCreationOptions, nameof(scheduleCreationOptions)); + try + { + if (!this.CanTransitionTo(nameof(this.CreateSchedule), ScheduleStatus.Active)) + { + throw new ScheduleInvalidTransitionException(scheduleCreationOptions.ScheduleId, this.State.Status, ScheduleStatus.Active); + } - if (this.State.Status != ScheduleStatus.Uninitialized) + // CreateSchedule is allowed, we shall throw exception if any following step failed to inform caller + Verify.NotNull(scheduleCreationOptions, nameof(scheduleCreationOptions)); + + this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); + this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); + + this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Succeeded.ToString()); + + // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately + // or later to separate response from schedule creation and schedule responsibilities + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); + } + catch (ScheduleInvalidTransitionException ex) { - string errorMessage = "Schedule is already created."; - Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), errorMessage, exception); - this.State.AddActivityLog("Create", "Failed", new FailureDetails + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = errorMessage, - Type = "InvalidOperation", + Reason = ex.Message, + Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure the schedule is not already created.", + }); + } + catch (ScheduleClientValidationException ex) + { + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = ex.Message, + Type = ScheduleOperationFailureType.ValidationError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure request is valid.", + }); + } + catch (Exception ex) + { + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); + this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = "Failed to create schedule", + Type = ScheduleOperationFailureType.InternalError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Please contact support.", }); - throw exception; } - - this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); - this.TryStatusTransition(ScheduleStatus.Active); - - this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog("Create", "Success"); - - // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately - // or later to separate response from schedule creation and schedule responsibilities - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } /// @@ -269,15 +297,21 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) } } - void TryStatusTransition(ScheduleStatus to) + bool CanTransitionTo(string operationName, ScheduleStatus targetStatus) { - // Check if transition is valid HashSet validTargetStates; - ScheduleStatus from = this.State.Status; + ScheduleStatus currentStatus = this.State.Status; + + return ScheduleTransitions.TryGetValidTransitions(operationName, currentStatus, out validTargetStates) && + validTargetStates.Contains(targetStatus); + } - if (!ScheduleTransitions.TryGetValidTransitions(from, out validTargetStates) || !validTargetStates.Contains(to)) + void TryStatusTransition(string operationName, ScheduleStatus to) + { + if (!this.CanTransitionTo(operationName, to)) { - throw new InvalidOperationException($"Invalid state transition: Cannot transition from {from} to {to}"); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.TryStatusTransition), $"Invalid state transition from {this.State.Status} to {to}"); + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration!.ScheduleId, this.State.Status, to); } this.State.Status = to; diff --git a/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs new file mode 100644 index 00000000..263e7902 --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when client-side validation fails for schedule operations. +/// +public class ScheduleClientValidationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed validation. + /// The validation error message. + public ScheduleClientValidationException(string scheduleId, string message) + : base($"Validation failed for schedule '{scheduleId}': {message}") + { + this.ScheduleId = scheduleId; + } + + /// + /// Gets the ID of the schedule that failed validation. + /// + public string ScheduleId { get; } +} diff --git a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs new file mode 100644 index 00000000..74457b9d --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when an invalid state transition is attempted on a schedule. +/// +public class ScheduleInvalidTransitionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule on which the invalid transition was attempted. + /// The current status of the schedule. + /// The target status that was invalid. + public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus fromStatus, ScheduleStatus toStatus) + : base($"Invalid state transition attempted for schedule '{scheduleId}': Cannot transition from {fromStatus} to {toStatus}.") + { + this.ScheduleId = scheduleId; + this.FromStatus = fromStatus; + this.ToStatus = toStatus; + } + + /// + /// Gets the ID of the schedule that encountered the invalid transition. + /// + public string ScheduleId { get; } + + /// + /// Gets the status the schedule was transitioning from. + /// + public ScheduleStatus FromStatus { get; } + + /// + /// Gets the invalid target status that was attempted. + /// + public ScheduleStatus ToStatus { get; } +} diff --git a/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs b/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs new file mode 100644 index 00000000..702600e9 --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents the type of failure that occurred during a schedule operation. +/// +public enum ScheduleOperationFailureType +{ + /// + /// The operation failed due to an invalid operation being attempted. + /// + InvalidOperation, + + /// + /// The operation failed due to an invalid state transition. + /// + InvalidStateTransition, + + /// + /// The operation failed due to validation errors. + /// + ValidationError, + + /// + /// The operation failed due to an internal server error. + /// + InternalError, +} diff --git a/src/ScheduledTasks/Models/ScheduleOperationStatus.cs b/src/ScheduledTasks/Models/ScheduleOperationStatus.cs new file mode 100644 index 00000000..ef5e649c --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleOperationStatus.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents the current status of a schedule operation. +/// +public enum ScheduleOperationStatus +{ + /// + /// The operation completed successfully. + /// + Succeeded, + + /// + /// The operation failed. + /// + Failed +} diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index d82e09f8..6c8c3b58 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -41,6 +41,11 @@ class ScheduleState /// public IReadOnlyCollection ActivityLogs => this.activityLogs.ToList().AsReadOnly(); + /// + /// Gets the last activity log for this schedule. + /// + public ScheduleActivityLog? LastActivityLog => this.ActivityLogs.LastOrDefault(); + /// /// Refreshes the execution token to invalidate pending schedule operations. /// @@ -72,5 +77,7 @@ public void AddActivityLog(string operation, string status, FailureDetails? fail { this.activityLogs.Dequeue(); } + + this.LastActivityLog = log; } } diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index 0f53b259..94e0c191 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -22,9 +22,4 @@ public enum ScheduleStatus /// Schedule is paused. /// Paused, - - /// - /// Schedule is being deleted. - /// - Failed, } diff --git a/src/ScheduledTasks/Models/ScheduleTransitions.cs b/src/ScheduledTasks/Models/ScheduleTransitions.cs index cc936461..f74c409a 100644 --- a/src/ScheduledTasks/Models/ScheduleTransitions.cs +++ b/src/ScheduledTasks/Models/ScheduleTransitions.cs @@ -8,26 +8,54 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// static class ScheduleTransitions { - /// - /// Maps schedule states to their valid target states. - /// - static readonly Dictionary> ValidTransitions = + // define valid transitions for create schedule + static readonly Dictionary> CreateScheduleStatusTransitions = new Dictionary> { { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, + }; + + // define valid transitions for update schedule + static readonly Dictionary> UpdateScheduleStatusTransitions = + new Dictionary> + { + { ScheduleStatus.Active, new HashSet { ScheduleStatus.Active } }, + { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Paused } }, + }; + + // define valid transitions for pause schedule + static readonly Dictionary> PauseScheduleStatusTransitions = + new Dictionary> + { { ScheduleStatus.Active, new HashSet { ScheduleStatus.Paused } }, + }; + + // define valid transitions for resume schedule + static readonly Dictionary> ResumeScheduleStatusTransitions = + new Dictionary> + { { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, }; /// - /// Attempts to get the valid target states for a given schedule state. + /// Attempts to get the valid target states for a given schedule state and operation. /// + /// The name of the operation being performed. /// The current schedule state. /// When this method returns, contains the valid target states if found; otherwise, an empty set. - /// True if valid transitions exist for the given state; otherwise, false. - public static bool TryGetValidTransitions(ScheduleStatus from, out HashSet validTargetStates) + /// True if valid transitions exist for the given state and operation; otherwise, false. + public static bool TryGetValidTransitions(string operationName, ScheduleStatus from, out HashSet validTargetStates) { - bool exists = ValidTransitions.TryGetValue(from, out HashSet? states); + Dictionary> transitionMap = operationName switch + { + nameof(Schedule.CreateSchedule) => CreateScheduleStatusTransitions, + nameof(Schedule.UpdateSchedule) => UpdateScheduleStatusTransitions, + nameof(Schedule.PauseSchedule) => PauseScheduleStatusTransitions, + nameof(Schedule.ResumeSchedule) => ResumeScheduleStatusTransitions, + _ => new Dictionary>(), + }; + + bool exists = transitionMap.TryGetValue(from, out HashSet? states); validTargetStates = states ?? new HashSet(); return exists; } From 9e9fb6a44b42be80000b0b0feb75e9e5f5486d49 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:01:08 -0800 Subject: [PATCH 073/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index bd07c40f..3b0a9a23 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -32,11 +32,14 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc { if (!this.CanTransitionTo(nameof(this.CreateSchedule), ScheduleStatus.Active)) { - throw new ScheduleInvalidTransitionException(scheduleCreationOptions.ScheduleId, this.State.Status, ScheduleStatus.Active); + throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId, this.State.Status, ScheduleStatus.Active); } // CreateSchedule is allowed, we shall throw exception if any following step failed to inform caller - Verify.NotNull(scheduleCreationOptions, nameof(scheduleCreationOptions)); + if (scheduleCreationOptions == null) + { + throw new ScheduleClientValidationException(null, "Schedule creation options cannot be null"); + } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); @@ -92,7 +95,16 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc /// Thrown when the schedule is not created. public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions scheduleUpdateOptions) { - Verify.NotNull(scheduleUpdateOptions, nameof(scheduleUpdateOptions)); + if (!this.CanTransitionTo(nameof(this.UpdateSchedule), ScheduleStatus.Active)) + { + throw new ScheduleInvalidTransitionException(scheduleUpdateOptions?.ScheduleId, this.State.Status, this.State.Status); + } + + if (scheduleUpdateOptions == null) + { + throw new ScheduleClientValidationException(null, "Schedule update options cannot be null"); + } + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); HashSet updatedScheduleConfigFields = this.State.ScheduleConfiguration.Update(scheduleUpdateOptions); From 1fb11490c15cf66c5a38bc56e323028fba880223 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:06:40 -0800 Subject: [PATCH 074/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 104 ++++++++++++++++++-------- 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 3b0a9a23..d2b20fcd 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -95,49 +95,87 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc /// Thrown when the schedule is not created. public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions scheduleUpdateOptions) { - if (!this.CanTransitionTo(nameof(this.UpdateSchedule), ScheduleStatus.Active)) + try { - throw new ScheduleInvalidTransitionException(scheduleUpdateOptions?.ScheduleId, this.State.Status, this.State.Status); - } + if (!this.CanTransitionTo(nameof(this.UpdateSchedule), ScheduleStatus.Active)) + { + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, this.State.Status); + } - if (scheduleUpdateOptions == null) - { - throw new ScheduleClientValidationException(null, "Schedule update options cannot be null"); - } + if (scheduleUpdateOptions == null) + { + throw new ScheduleClientValidationException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, "Schedule update options cannot be null"); + } - Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - HashSet updatedScheduleConfigFields = this.State.ScheduleConfiguration.Update(scheduleUpdateOptions); - if (updatedScheduleConfigFields.Count == 0) - { - // no need to interrupt and update current schedule run as there is no change in the schedule config - this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration.ScheduleId, nameof(this.UpdateSchedule), "Schedule configuration is up to date."); - return; - } + HashSet updatedScheduleConfigFields = this.State.ScheduleConfiguration.Update(scheduleUpdateOptions); + if (updatedScheduleConfigFields.Count == 0) + { + // no need to interrupt and update current schedule run as there is no change in the schedule config + this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration.ScheduleId, nameof(this.UpdateSchedule), "Schedule configuration is up to date."); + return; + } - // after schedule config is updated, perform post-config-update logic separately - foreach (string updatedScheduleConfigField in updatedScheduleConfigFields) - { - switch (updatedScheduleConfigField) + // after schedule config is updated, perform post-config-update logic separately + foreach (string updatedScheduleConfigField in updatedScheduleConfigFields) { - case nameof(this.State.ScheduleConfiguration.StartAt): - case nameof(this.State.ScheduleConfiguration.Interval): - this.State.NextRunAt = null; - break; - - // TODO: add other fields's callback logic after config update if any - default: - break; + switch (updatedScheduleConfigField) + { + case nameof(this.State.ScheduleConfiguration.StartAt): + case nameof(this.State.ScheduleConfiguration.Interval): + this.State.NextRunAt = null; + break; + + // TODO: add other fields's callback logic after config update if any + default: + break; + } } - } - this.State.RefreshScheduleRunExecutionToken(); + this.State.RefreshScheduleRunExecutionToken(); - this.logger.UpdatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.logger.UpdatedSchedule(this.State.ScheduleConfiguration.ScheduleId); - // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately - // or later to separate response from schedule creation and schedule responsibilities - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); + // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately + // or later to separate response from schedule creation and schedule responsibilities + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); + + this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Succeeded.ToString()); + } + catch (ScheduleInvalidTransitionException ex) + { + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = ex.Message, + Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure the schedule is in a valid state for update.", + }); + } + catch (ScheduleClientValidationException ex) + { + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = ex.Message, + Type = ScheduleOperationFailureType.ValidationError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure update request is valid.", + }); + } + catch (Exception ex) + { + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); + this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = "Failed to update schedule", + Type = ScheduleOperationFailureType.InternalError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Please contact support.", + }); + } } /// From 6ab593bfa917682aa9d2dc90c515e408573dcda0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:09:34 -0800 Subject: [PATCH 075/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 34 +++++++++++---------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index d2b20fcd..2c4fe3b9 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -97,7 +97,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche { try { - if (!this.CanTransitionTo(nameof(this.UpdateSchedule), ScheduleStatus.Active)) + if (!this.CanTransitionTo(nameof(this.UpdateSchedule), this.State.Status)) { throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, this.State.Status); } @@ -183,28 +183,22 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche /// public void PauseSchedule(TaskEntityContext context) { - Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - if (this.State.Status != ScheduleStatus.Active) - { - string errorMessage = "Schedule must be in Active state to pause."; - Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.PauseSchedule), errorMessage, exception); - this.State.AddActivityLog("Pause", "Failed", new FailureDetails + try { + if (!this.CanTransitionTo(nameof(this.PauseSchedule), ScheduleStatus.Paused)) { - Reason = errorMessage, - Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow, - }); - throw exception; - } + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Paused); + } - // Transition to Paused state - this.TryStatusTransition(ScheduleStatus.Paused); - this.State.NextRunAt = null; - this.State.RefreshScheduleRunExecutionToken(); + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + + // Transition to Paused state + this.TryStatusTransition(ScheduleStatus.Paused); + this.State.NextRunAt = null; + this.State.RefreshScheduleRunExecutionToken(); - this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog("Pause", "Success"); + this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog("Pause", "Success"); + } catch (Exception ex) {} } /// From eb9853d8625922117ea1ffc12f5e16284717f6c3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:12:47 -0800 Subject: [PATCH 076/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 93 ++++++++++++++++++++------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 2c4fe3b9..727e997c 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -6,6 +6,9 @@ namespace Microsoft.DurableTask.ScheduledTasks; +// TODO: Support other schedule option properties like cron expression, max occurrence, etc. + + /// /// Entity that manages the state and execution of a scheduled task. /// @@ -197,8 +200,29 @@ public void PauseSchedule(TaskEntityContext context) this.State.RefreshScheduleRunExecutionToken(); this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog("Pause", "Success"); - } catch (Exception ex) {} + this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Succeeded.ToString()); + } catch (ScheduleInvalidTransitionException ex) + { + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = ex.Message, + Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure the schedule is in a valid state for pause.", + }); + } + catch (Exception ex) + { + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); + this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = "Failed to pause schedule", + Type = ScheduleOperationFailureType.InternalError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Please contact support.", + }); + } } /// @@ -208,34 +232,57 @@ public void PauseSchedule(TaskEntityContext context) /// Thrown when the schedule is not paused. public void ResumeSchedule(TaskEntityContext context) { - Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - if (this.State.Status != ScheduleStatus.Paused) + try { + if (!this.CanTransitionTo(nameof(this.ResumeSchedule), ScheduleStatus.Active)) + { + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active); + } + + Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + + this.TryStatusTransition(ScheduleStatus.Active); + this.State.NextRunAt = null; + this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); + this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Succeeded.ToString()); + + // compute next run based on startat and interval + context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); + } + catch (ScheduleInvalidTransitionException ex) { - string errorMessage = "Schedule must be in Paused state to resume."; - Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.ResumeSchedule), errorMessage, exception); - this.State.AddActivityLog("Resume", "Failed", new FailureDetails + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = errorMessage, - Type = "InvalidOperation", + Reason = ex.Message, + Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure the schedule is in a valid state for resume.", + }); + } + catch (ScheduleClientValidationException ex) + { + this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = ex.Message, + Type = ScheduleOperationFailureType.ValidationError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure request is valid.", + }); + } + catch (Exception ex) + { + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); + this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = "Failed to resume schedule", + Type = ScheduleOperationFailureType.InternalError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Please contact support.", }); - throw exception; } - - this.TryStatusTransition(ScheduleStatus.Active); - this.State.NextRunAt = null; - this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog("Resume", "Success"); - - // compute next run based on startat and interval - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - // TODO: Verify use built int entity delete operation to delete schedule - - // TODO: Support other schedule option properties like cron expression, max occurrence, etc. - /// /// Runs the schedule based on the defined configuration. /// From beb40cfe3f61a7ab4fb3e00f2314f8bf951c7212 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:13:25 -0800 Subject: [PATCH 077/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 727e997c..5f063896 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -8,7 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; // TODO: Support other schedule option properties like cron expression, max occurrence, etc. - /// /// Entity that manages the state and execution of a scheduled task. /// @@ -186,7 +185,8 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche /// public void PauseSchedule(TaskEntityContext context) { - try { + try + { if (!this.CanTransitionTo(nameof(this.PauseSchedule), ScheduleStatus.Paused)) { throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Paused); @@ -195,13 +195,14 @@ public void PauseSchedule(TaskEntityContext context) Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); // Transition to Paused state - this.TryStatusTransition(ScheduleStatus.Paused); + this.TryStatusTransition(nameof(this.PauseSchedule), ScheduleStatus.Paused); this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Succeeded.ToString()); - } catch (ScheduleInvalidTransitionException ex) + } + catch (ScheduleInvalidTransitionException ex) { this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails @@ -232,7 +233,8 @@ public void PauseSchedule(TaskEntityContext context) /// Thrown when the schedule is not paused. public void ResumeSchedule(TaskEntityContext context) { - try { + try + { if (!this.CanTransitionTo(nameof(this.ResumeSchedule), ScheduleStatus.Active)) { throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active); @@ -240,14 +242,14 @@ public void ResumeSchedule(TaskEntityContext context) Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.TryStatusTransition(ScheduleStatus.Active); + this.TryStatusTransition(nameof(this.ResumeSchedule), ScheduleStatus.Active); this.State.NextRunAt = null; this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Succeeded.ToString()); // compute next run based on startat and interval context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); - } + } catch (ScheduleInvalidTransitionException ex) { this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); From 1879dcdc96ae76ecce3d43274e33f3ce33bd87d8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:24:11 -0800 Subject: [PATCH 078/203] save --- src/ScheduledTasks/Client/ScheduleWaiter.cs | 6 ++++-- .../ScheduleOperationFailedException.cs | 16 ++++++++++++++-- src/ScheduledTasks/Models/ScheduleState.cs | 7 ------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index 3a14168c..a5d2cd13 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -115,9 +115,11 @@ async Task WaitForStatusAsync( return description; } - if (description.Status == ScheduleStatus.Failed) + // check latest operation log status + ScheduleActivityLog? latestActivityLog = description.ActivityLogs.LastOrDefault(); + if (latestActivityLog != null && latestActivityLog.Status == ScheduleOperationStatus.Failed.ToString()) { - throw new ScheduleOperationFailedException(description); + throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails); } } catch (ScheduleStillBeingProvisionedException) diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs index 8f0b8033..069129f0 100644 --- a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs +++ b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs @@ -29,13 +29,25 @@ public ScheduleOperationFailedException(ScheduleDescription schedule, Exception this.Schedule = schedule; } + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed. + /// The operation that failed. + /// The status of the failed operation. + /// Details about the failure. + public ScheduleOperationFailedException(string scheduleId, string operation, string status, FailureDetails failureDetails) + : base($"Operation '{operation}' failed for schedule '{scheduleId}' with status '{status}'. Details: {failureDetails}") + { + } + /// /// Gets the schedule that failed. /// - public ScheduleDescription Schedule { get; } + public ScheduleDescription? Schedule { get; } /// /// Gets the ID of the schedule that failed. /// - public string ScheduleId => this.Schedule.ScheduleId; + public string ScheduleId => this.Schedule?.ScheduleId ?? this.ScheduleId; } diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 6c8c3b58..d82e09f8 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -41,11 +41,6 @@ class ScheduleState /// public IReadOnlyCollection ActivityLogs => this.activityLogs.ToList().AsReadOnly(); - /// - /// Gets the last activity log for this schedule. - /// - public ScheduleActivityLog? LastActivityLog => this.ActivityLogs.LastOrDefault(); - /// /// Refreshes the execution token to invalidate pending schedule operations. /// @@ -77,7 +72,5 @@ public void AddActivityLog(string operation, string status, FailureDetails? fail { this.activityLogs.Dequeue(); } - - this.LastActivityLog = log; } } From 4985b04404b48224f6a704169da4b96208583108 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:26:37 -0800 Subject: [PATCH 079/203] save --- src/ScheduledTasks/Client/ScheduleWaiter.cs | 2 +- .../Exception/ScheduleOperationFailedException.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index a5d2cd13..c443386c 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -119,7 +119,7 @@ async Task WaitForStatusAsync( ScheduleActivityLog? latestActivityLog = description.ActivityLogs.LastOrDefault(); if (latestActivityLog != null && latestActivityLog.Status == ScheduleOperationStatus.Failed.ToString()) { - throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails); + throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails ?? null); } } catch (ScheduleStillBeingProvisionedException) diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs index 069129f0..da9e50e9 100644 --- a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs +++ b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs @@ -36,7 +36,7 @@ public ScheduleOperationFailedException(ScheduleDescription schedule, Exception /// The operation that failed. /// The status of the failed operation. /// Details about the failure. - public ScheduleOperationFailedException(string scheduleId, string operation, string status, FailureDetails failureDetails) + public ScheduleOperationFailedException(string scheduleId, string operation, string status, FailureDetails? failureDetails) : base($"Operation '{operation}' failed for schedule '{scheduleId}' with status '{status}'. Details: {failureDetails}") { } From 28bebd5396b87d40c7efd1ab1e8761f1e54006ce Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 01:06:52 -0800 Subject: [PATCH 080/203] save --- samples/ScheduleDemo/Program.cs | 60 +++++++++++-------- src/ScheduledTasks/Client/IScheduleHandle.cs | 7 +++ .../Client/IScheduledTaskClient.cs | 7 --- src/ScheduledTasks/Client/ScheduleHandle.cs | 15 ++++- src/ScheduledTasks/Client/ScheduleWaiter.cs | 2 +- .../Client/ScheduledTaskClient.cs | 14 ----- src/ScheduledTasks/Entity/Schedule.cs | 41 +++---------- src/ScheduledTasks/Models/ScheduleState.cs | 2 +- 8 files changed, 67 insertions(+), 81 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index e41ca5c8..89626e11 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -25,7 +25,7 @@ // Add a demo orchestration that will be triggered by the schedule r.AddOrchestratorFunc("DemoOrchestration", async context => { - const string stockSymbol = "MSFT"; // Hardcoded to Microsoft stock + string stockSymbol = context.GetInput() ?? "APPL"; // Default to MSFT if no input provided var logger = context.CreateReplaySafeLogger("DemoOrchestration"); logger.LogInformation("Getting stock price for: {symbol}", stockSymbol); @@ -87,43 +87,55 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule2", + ScheduleId = "demo-schedule4", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, - OrchestrationInput = "This is a scheduled message!" + OrchestrationInput = "MSFT" }; + // Get schedule handle + IScheduleHandle scheduleHandle = scheduledTaskClient.GetScheduleHandle(scheduleOptions.ScheduleId); + + // Create the schedule - IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + Console.WriteLine("Creating schedule..."); + IScheduleWaiter waiter = await scheduleHandle.CreateAsync(scheduleOptions); + ScheduleDescription scheduleDescription = await waiter.WaitUntilActiveAsync(); + // print the schedule description + Console.WriteLine(scheduleDescription); + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); // // Pause the schedule - // Console.WriteLine("\nPausing schedule..."); - IScheduleWaiter waiter = await scheduleHandle.PauseAsync(); - await waiter.WaitUntilPausedAsync(); + Console.WriteLine("\nPausing schedule..."); + IScheduleWaiter pauseWaiter = await scheduleHandle.PauseAsync(); + scheduleDescription = await pauseWaiter.WaitUntilPausedAsync(); + Console.WriteLine(scheduleDescription); + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); - // var pausedSchedule = await scheduleHandle.DescribeAsync(); - // Console.WriteLine($"Schedule status after pause: {pausedSchedule.Status}"); - // // Resume the schedule - // Console.WriteLine("\nResuming schedule..."); - // await scheduleHandle.ResumeAsync(); + // Resume the schedule + Console.WriteLine("\nResuming schedule..."); + IScheduleWaiter resumeWaiter = await scheduleHandle.ResumeAsync(); - // var resumedSchedule = await scheduleHandle.DescribeAsync(); - // Console.WriteLine($"Schedule status after resume: {resumedSchedule.Status}"); - // Console.WriteLine($"Next run at: {resumedSchedule.NextRunAt}"); + scheduleDescription = await resumeWaiter.WaitUntilActiveAsync(); + Console.WriteLine(scheduleDescription); - // Console.WriteLine("\nPress any key to delete the schedule and exit..."); - // Console.ReadKey(); + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); - // // intentionally call schedule to trigger exceptions - // await scheduleHandle.ResumeAsync(); - // await scheduleHandle.ResumeAsync(); + Console.WriteLine("\nPress any key to delete the schedule and exit..."); + Console.ReadKey(); - //await Task.Delay(TimeSpan.FromSeconds(120)); - // // Delete the schedule - // await scheduleHandle.DeleteAsync(); - // Console.WriteLine("Schedule deleted."); + // Delete the schedule + IScheduleWaiter deleteWaiter = await scheduleHandle.DeleteAsync(); + bool deleted = await deleteWaiter.WaitUntilDeletedAsync(); + Console.WriteLine(deleted ? "Schedule deleted." : "Schedule not deleted."); } catch (Exception ex) { diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 60e5b2fe..e8a1fd64 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -13,6 +13,13 @@ public interface IScheduleHandle /// string ScheduleId { get; } + /// + /// Creates this schedule with the specified configuration. + /// + /// The options for creating the schedule. + /// A task that completes when the schedule is created. + Task CreateAsync(ScheduleCreationOptions creationOptions); + /// /// Retrieves the current details of this schedule. /// diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 0349be80..7ca2e7be 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -21,11 +21,4 @@ public interface IScheduledTaskClient /// Whether to include full activity logs in the returned schedules. /// A list of schedule descriptions. Task> ListSchedulesAsync(bool includeFullActivityLogs); - - /// - /// Creates a new schedule with the specified configuration. - /// - /// The configuration options for creating the schedule. - /// The ID of the newly created schedule. - Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 1565b6e6..5092c7e4 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -88,6 +88,19 @@ public async Task DescribeAsync(bool includeFullActivityLog }; } + /// + public async Task CreateAsync(ScheduleCreationOptions creationOptions) + { + this.logger.ClientCreatingSchedule(creationOptions); + Check.NotNull(creationOptions, nameof(creationOptions)); + + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), creationOptions.ScheduleId); + + await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), creationOptions); + + return new ScheduleWaiter(this); + } + /// public async Task PauseAsync() { @@ -111,7 +124,7 @@ public async Task ResumeAsync() public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) { this.logger.ClientUpdatingSchedule(this.ScheduleId); - + Check.NotNull(updateOptions, nameof(updateOptions)); await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); return new ScheduleWaiter(this); } diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index c443386c..e1c6d8f1 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -122,7 +122,7 @@ async Task WaitForStatusAsync( throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails ?? null); } } - catch (ScheduleStillBeingProvisionedException) + catch (Exception ex) when (ex is ScheduleStillBeingProvisionedException || ex is ScheduleNotFoundException) { if (desiredStatus != ScheduleStatus.Active) { diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 2feebe52..9b916071 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -3,7 +3,6 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; -using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -36,19 +35,6 @@ public IScheduleHandle GetScheduleHandle(string scheduleId) return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); } - /// - public async Task CreateScheduleAsync(ScheduleCreationOptions scheduleConfigCreateOptions) - { - this.logger.ClientCreatingSchedule(scheduleConfigCreateOptions); - Check.NotNull(scheduleConfigCreateOptions, nameof(scheduleConfigCreateOptions)); - - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleConfigCreateOptions.ScheduleId); - - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), scheduleConfigCreateOptions); - - return new ScheduleHandle(this.durableTaskClient, scheduleConfigCreateOptions.ScheduleId, this.logger); - } - /// public async Task> ListSchedulesAsync(bool includeFullActivityLogs = false) { diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 5f063896..01bfb253 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -293,30 +293,12 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { - Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - if (this.State.ScheduleConfiguration.Interval == null) - { - string errorMessage = "Schedule interval must be specified."; - Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); - this.State.AddActivityLog("Run", "Failed", new FailureDetails - { - Reason = errorMessage, - Type = "InvalidConfiguration", - OccurredAt = DateTimeOffset.UtcNow, - }); - throw exception; - } + ScheduleConfiguration scheduleConfig = Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + TimeSpan interval = scheduleConfig.Interval ?? throw new InvalidOperationException("Schedule interval must be specified."); if (executionToken != this.State.ExecutionToken) { - this.logger.ScheduleRunCancelled(this.State.ScheduleConfiguration.ScheduleId, executionToken); - this.State.AddActivityLog("Run", "Cancelled", new FailureDetails - { - Reason = "Execution token mismatch", - Type = "TokenExpired", - OccurredAt = DateTimeOffset.UtcNow, - }); + this.logger.ScheduleRunCancelled(scheduleConfig.ScheduleId, executionToken); return; } @@ -324,13 +306,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { string errorMessage = "Schedule must be in Active status to run."; Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); - this.State.AddActivityLog("Run", "Failed", new FailureDetails - { - Reason = errorMessage, - Type = "InvalidOperation", - OccurredAt = DateTimeOffset.UtcNow, - }); + this.logger.ScheduleOperationError(scheduleConfig.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); throw exception; } @@ -344,16 +320,16 @@ public void RunSchedule(TaskEntityContext context, string executionToken) // else, it has run before, we cant run at startat, need to compute next run at based on last run at + num of intervals between last runtime and now plus 1 if (!this.State.LastRunAt.HasValue) { - this.State.NextRunAt = this.State.ScheduleConfiguration.StartAt; + this.State.NextRunAt = scheduleConfig.StartAt; } else { // Calculate number of intervals between last run and now TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - this.State.LastRunAt.Value; - int intervalsElapsed = (int)(timeSinceLastRun.Ticks / this.State.ScheduleConfiguration.Interval.Value.Ticks); + int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); // Compute the next run time - this.State.NextRunAt = this.State.LastRunAt.Value + TimeSpan.FromTicks(this.State.ScheduleConfiguration.Interval.Value.Ticks * (intervalsElapsed + 1)); + this.State.NextRunAt = this.State.LastRunAt.Value + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); } } @@ -364,10 +340,9 @@ public void RunSchedule(TaskEntityContext context, string executionToken) this.State.NextRunAt = currentTime; this.StartOrchestrationIfNotRunning(context); this.State.LastRunAt = this.State.NextRunAt; - this.State.NextRunAt = this.State.LastRunAt.Value + this.State.ScheduleConfiguration.Interval.Value; + this.State.NextRunAt = this.State.LastRunAt.Value + interval; } - // this.logger.CompletedScheduleRun(this.State.ScheduleConfiguration.ScheduleId); context.SignalEntity( new EntityInstanceId( nameof(Schedule), diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index d82e09f8..7103fe9e 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -8,7 +8,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleState { - const int MaxActivityLogItems = 100; + const int MaxActivityLogItems = 10; readonly Queue activityLogs = new(); /// From 067bb1912582c864291640e070f9ed45c30586f8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 02:09:25 -0800 Subject: [PATCH 081/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 9 ++++----- src/ScheduledTasks/Client/ScheduleWaiter.cs | 4 ++-- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 1 - src/ScheduledTasks/Logging/Client/Logs.cs | 6 ------ 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 5092c7e4..94a01fe0 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -47,7 +47,6 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge public async Task DescribeAsync(bool includeFullActivityLogs = false) { Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - this.logger.ClientDescribingSchedule(this.ScheduleId); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); EntityMetadata? metadata = @@ -58,10 +57,10 @@ public async Task DescribeAsync(bool includeFullActivityLog } ScheduleState state = metadata.State; - if (state.Status == ScheduleStatus.Uninitialized) - { - throw new ScheduleStillBeingProvisionedException(this.ScheduleId); - } + // if (state.Status == ScheduleStatus.Uninitialized) + // { + // throw new ScheduleStillBeingProvisionedException(this.ScheduleId); + // } ScheduleConfiguration? config = state.ScheduleConfiguration; diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index e1c6d8f1..2bbb3910 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -10,8 +10,8 @@ public class ScheduleWaiter : IScheduleWaiter { readonly IScheduleHandle scheduleHandle; readonly TimeSpan defaultPollingInterval = TimeSpan.FromSeconds(5); - readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(5); - readonly TimeSpan defaultMaxPollingInterval = TimeSpan.FromSeconds(30); + readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(2); + readonly TimeSpan defaultMaxPollingInterval = TimeSpan.FromSeconds(20); /// /// Initializes a new instance of the class. diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 9b916071..aff05400 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -38,7 +38,6 @@ public IScheduleHandle GetScheduleHandle(string scheduleId) /// public async Task> ListSchedulesAsync(bool includeFullActivityLogs = false) { - this.logger.ClientListingSchedules(); EntityQuery query = new EntityQuery { InstanceIdStartsWith = nameof(Schedule), // Automatically ensures correct formatting diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 86b3f60a..8f3aad03 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -20,12 +20,6 @@ static partial class Logs [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Client: Getting schedule handle for schedule '{scheduleId}'")] public static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Client: Listing initialized schedules")] - public static partial void ClientListingSchedules(this ILogger logger); - - [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Client: Describing schedule '{scheduleId}'")] - public static partial void ClientDescribingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Client: Pausing schedule '{scheduleId}'")] public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); From c3c6ed0d153a68fe6de98b45eded901fdfcaef8f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 02:18:21 -0800 Subject: [PATCH 082/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 10 +++++----- src/ScheduledTasks/Client/ScheduleWaiter.cs | 10 +++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 94a01fe0..c8b36fec 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -97,7 +97,7 @@ public async Task CreateAsync(ScheduleCreationOptions creationO await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), creationOptions); - return new ScheduleWaiter(this); + return new ScheduleWaiter(this, nameof(Schedule.CreateSchedule)); } /// @@ -106,7 +106,7 @@ public async Task PauseAsync() this.logger.ClientPausingSchedule(this.ScheduleId); await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.PauseSchedule)); - return new ScheduleWaiter(this); + return new ScheduleWaiter(this, nameof(Schedule.PauseSchedule)); } /// @@ -116,7 +116,7 @@ public async Task ResumeAsync() await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.ResumeSchedule)); - return new ScheduleWaiter(this); + return new ScheduleWaiter(this, nameof(Schedule.ResumeSchedule)); } /// @@ -125,7 +125,7 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptio this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); - return new ScheduleWaiter(this); + return new ScheduleWaiter(this, nameof(Schedule.UpdateSchedule)); } /// @@ -134,6 +134,6 @@ public async Task DeleteAsync() this.logger.ClientDeletingSchedule(this.ScheduleId); await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete"); - return new ScheduleWaiter(this); + return new ScheduleWaiter(this, nameof(Schedule.DeleteSchedule)); } } diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index 2bbb3910..11df364a 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -13,13 +13,17 @@ public class ScheduleWaiter : IScheduleWaiter readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(2); readonly TimeSpan defaultMaxPollingInterval = TimeSpan.FromSeconds(20); + readonly string operationName; + /// /// Initializes a new instance of the class. /// /// The schedule handle to wait on. - public ScheduleWaiter(IScheduleHandle scheduleHandle) + /// + public ScheduleWaiter(IScheduleHandle scheduleHandle, string operationName) { this.scheduleHandle = scheduleHandle ?? throw new ArgumentNullException(nameof(scheduleHandle)); + this.operationName = operationName ?? throw new ArgumentNullException(nameof(operationName)); } /// @@ -122,9 +126,9 @@ async Task WaitForStatusAsync( throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails ?? null); } } - catch (Exception ex) when (ex is ScheduleStillBeingProvisionedException || ex is ScheduleNotFoundException) + catch (Exception ex) when (ex is ScheduleNotFoundException) { - if (desiredStatus != ScheduleStatus.Active) + if (this.operationName == nameof(Schedule.CreateSchedule) && desiredStatus != ScheduleStatus.Active) { throw; } From 2dbf58a517ea6df3e32d8723a17f5fba7a09b5fb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 02:23:31 -0800 Subject: [PATCH 083/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 4 ++-- src/ScheduledTasks/Entity/Schedule.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index c8b36fec..89b51a24 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -57,11 +57,11 @@ public async Task DescribeAsync(bool includeFullActivityLog } ScheduleState state = metadata.State; + // if (state.Status == ScheduleStatus.Uninitialized) // { // throw new ScheduleStillBeingProvisionedException(this.ScheduleId); // } - ScheduleConfiguration? config = state.ScheduleConfiguration; IReadOnlyCollection activityLogs = @@ -134,6 +134,6 @@ public async Task DeleteAsync() this.logger.ClientDeletingSchedule(this.ScheduleId); await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete"); - return new ScheduleWaiter(this, nameof(Schedule.DeleteSchedule)); + return new ScheduleWaiter(this, "delete"); } } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 01bfb253..985a6e71 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -34,7 +34,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc { if (!this.CanTransitionTo(nameof(this.CreateSchedule), ScheduleStatus.Active)) { - throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId, this.State.Status, ScheduleStatus.Active); + throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active); } // CreateSchedule is allowed, we shall throw exception if any following step failed to inform caller @@ -183,6 +183,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche /// /// Pauses the schedule. /// + /// The task entity context. public void PauseSchedule(TaskEntityContext context) { try From d6dae32e536952dab6a9c45fef0e3a5e32bff81c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 08:17:33 -0800 Subject: [PATCH 084/203] save --- samples/ScheduleDemo/Program.cs | 19 ++++++----- src/ScheduledTasks/Client/ScheduleWaiter.cs | 2 +- src/ScheduledTasks/Entity/Schedule.cs | 34 +++++++++---------- .../ScheduleInvalidTransitionException.cs | 11 ++++-- .../Models/ScheduleDescription.cs | 9 ++++- src/ScheduledTasks/Models/ScheduleState.cs | 11 +++--- 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 89626e11..56c5df5a 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -25,7 +25,7 @@ // Add a demo orchestration that will be triggered by the schedule r.AddOrchestratorFunc("DemoOrchestration", async context => { - string stockSymbol = context.GetInput() ?? "APPL"; // Default to MSFT if no input provided + string stockSymbol = "MSFT"; // Default to MSFT if no input provided var logger = context.CreateReplaySafeLogger("DemoOrchestration"); logger.LogInformation("Getting stock price for: {symbol}", stockSymbol); @@ -87,7 +87,7 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule4", + ScheduleId = "demo-schedule8", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" @@ -102,17 +102,17 @@ IScheduleWaiter waiter = await scheduleHandle.CreateAsync(scheduleOptions); ScheduleDescription scheduleDescription = await waiter.WaitUntilActiveAsync(); // print the schedule description - Console.WriteLine(scheduleDescription); + Console.WriteLine(scheduleDescription.ToJsonString(true)); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine(""); - // // Pause the schedule + // Pause the schedule Console.WriteLine("\nPausing schedule..."); IScheduleWaiter pauseWaiter = await scheduleHandle.PauseAsync(); scheduleDescription = await pauseWaiter.WaitUntilPausedAsync(); - Console.WriteLine(scheduleDescription); + Console.WriteLine(scheduleDescription.ToJsonString(true)); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine(""); @@ -123,14 +123,15 @@ IScheduleWaiter resumeWaiter = await scheduleHandle.ResumeAsync(); scheduleDescription = await resumeWaiter.WaitUntilActiveAsync(); - Console.WriteLine(scheduleDescription); + Console.WriteLine(scheduleDescription.ToJsonString(true)); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine(""); - Console.WriteLine("\nPress any key to delete the schedule and exit..."); - Console.ReadKey(); + await Task.Delay(200000); + //Console.WriteLine("\nPress any key to delete the schedule and exit..."); + //Console.ReadKey(); // Delete the schedule IScheduleWaiter deleteWaiter = await scheduleHandle.DeleteAsync(); @@ -139,7 +140,7 @@ } catch (Exception ex) { - Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine($"One of your schedule operations failed, please fix and rerun: {ex.Message}"); } await host.StopAsync(); \ No newline at end of file diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs index 11df364a..f3ccc5bb 100644 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ b/src/ScheduledTasks/Client/ScheduleWaiter.cs @@ -19,7 +19,7 @@ public class ScheduleWaiter : IScheduleWaiter /// Initializes a new instance of the class. /// /// The schedule handle to wait on. - /// + /// operation name. public ScheduleWaiter(IScheduleHandle scheduleHandle, string operationName) { this.scheduleHandle = scheduleHandle ?? throw new ArgumentNullException(nameof(scheduleHandle)); diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 985a6e71..c1eb59d0 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -34,13 +34,13 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc { if (!this.CanTransitionTo(nameof(this.CreateSchedule), ScheduleStatus.Active)) { - throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active); + throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active, nameof(this.CreateSchedule)); } // CreateSchedule is allowed, we shall throw exception if any following step failed to inform caller if (scheduleCreationOptions == null) { - throw new ScheduleClientValidationException(null, "Schedule creation options cannot be null"); + throw new ScheduleClientValidationException(string.Empty, "Schedule creation options cannot be null"); } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); @@ -55,7 +55,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (ScheduleInvalidTransitionException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -66,7 +66,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (ScheduleClientValidationException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -77,7 +77,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); + // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to create schedule", @@ -101,7 +101,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche { if (!this.CanTransitionTo(nameof(this.UpdateSchedule), this.State.Status)) { - throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, this.State.Status); + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, this.State.Status, nameof(this.UpdateSchedule)); } if (scheduleUpdateOptions == null) @@ -147,7 +147,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (ScheduleInvalidTransitionException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -158,7 +158,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (ScheduleClientValidationException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -169,7 +169,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); + // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to update schedule", @@ -190,7 +190,7 @@ public void PauseSchedule(TaskEntityContext context) { if (!this.CanTransitionTo(nameof(this.PauseSchedule), ScheduleStatus.Paused)) { - throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Paused); + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Paused, nameof(this.PauseSchedule)); } Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); @@ -205,7 +205,7 @@ public void PauseSchedule(TaskEntityContext context) } catch (ScheduleInvalidTransitionException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -216,7 +216,7 @@ public void PauseSchedule(TaskEntityContext context) } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); + // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to pause schedule", @@ -238,7 +238,7 @@ public void ResumeSchedule(TaskEntityContext context) { if (!this.CanTransitionTo(nameof(this.ResumeSchedule), ScheduleStatus.Active)) { - throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active); + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active, nameof(this.ResumeSchedule)); } Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); @@ -253,7 +253,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (ScheduleInvalidTransitionException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -264,7 +264,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (ScheduleClientValidationException ex) { - this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = ex.Message, @@ -275,7 +275,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); + // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to resume schedule", @@ -380,7 +380,7 @@ void TryStatusTransition(string operationName, ScheduleStatus to) if (!this.CanTransitionTo(operationName, to)) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.TryStatusTransition), $"Invalid state transition from {this.State.Status} to {to}"); - throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration!.ScheduleId, this.State.Status, to); + throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration!.ScheduleId, this.State.Status, to, operationName); } this.State.Status = to; diff --git a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs index 74457b9d..2ff1224b 100644 --- a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs +++ b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs @@ -14,12 +14,14 @@ public class ScheduleInvalidTransitionException : Exception /// The ID of the schedule on which the invalid transition was attempted. /// The current status of the schedule. /// The target status that was invalid. - public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus fromStatus, ScheduleStatus toStatus) - : base($"Invalid state transition attempted for schedule '{scheduleId}': Cannot transition from {fromStatus} to {toStatus}.") + /// The name of the operation that was attempted. + public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus fromStatus, ScheduleStatus toStatus, string operationName) + : base($"Invalid state transition attempted for schedule '{scheduleId}': Cannot transition from {fromStatus} to {toStatus} during {operationName} operation.") { this.ScheduleId = scheduleId; this.FromStatus = fromStatus; this.ToStatus = toStatus; + this.OperationName = operationName; } /// @@ -36,4 +38,9 @@ public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus from /// Gets the invalid target status that was attempted. /// public ScheduleStatus ToStatus { get; } + + /// + /// Gets the name of the operation that was attempted. + /// + public string OperationName { get; } } diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index a0b4d7b1..50d635a0 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -86,6 +86,13 @@ public record ScheduleDescription /// /// Returns a JSON string representation of the schedule description. /// + /// If true, formats the JSON with indentation for readability. /// A JSON string containing the schedule details. - public string ToJsonString() => System.Text.Json.JsonSerializer.Serialize(this); + public string ToJsonString(bool pretty = false) + { + System.Text.Json.JsonSerializerOptions options = pretty + ? new System.Text.Json.JsonSerializerOptions { WriteIndented = true } + : new System.Text.Json.JsonSerializerOptions(); + return System.Text.Json.JsonSerializer.Serialize(this, options); + } } diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 7103fe9e..e62d4c16 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -9,7 +9,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; class ScheduleState { const int MaxActivityLogItems = 10; - readonly Queue activityLogs = new(); + + public Queue? ActivityLogs { get; set; } = new Queue(); /// /// Gets or sets the current status of the schedule. @@ -39,7 +40,7 @@ class ScheduleState /// /// Gets the activity logs for this schedule. /// - public IReadOnlyCollection ActivityLogs => this.activityLogs.ToList().AsReadOnly(); + // public IReadOnlyCollection ActivityLogs => this.ActivityLogs1.ToList().AsReadOnly(); /// /// Refreshes the execution token to invalidate pending schedule operations. @@ -65,12 +66,12 @@ public void AddActivityLog(string operation, string status, FailureDetails? fail FailureDetails = failureDetails, }; - this.activityLogs.Enqueue(log); + this.ActivityLogs.Enqueue(log); // Keep only the most recent MaxActivityLogItems - while (this.activityLogs.Count > MaxActivityLogItems) + while (this.ActivityLogs.Count > MaxActivityLogItems) { - this.activityLogs.Dequeue(); + this.ActivityLogs.Dequeue(); } } } From 41603fe15bb90d852678c007ca54d68cc89513e9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 08:38:56 -0800 Subject: [PATCH 085/203] save --- samples/ScheduleDemo/Program.cs | 2 +- src/Abstractions/Converters/JsonDataConverter.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 56c5df5a..e5da5ea2 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -87,7 +87,7 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule8", + ScheduleId = "demo-schedule9", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" diff --git a/src/Abstractions/Converters/JsonDataConverter.cs b/src/Abstractions/Converters/JsonDataConverter.cs index 7e4d1467..eeca67a3 100644 --- a/src/Abstractions/Converters/JsonDataConverter.cs +++ b/src/Abstractions/Converters/JsonDataConverter.cs @@ -35,8 +35,6 @@ public JsonDataConverter(JsonSerializerOptions? options = null) /// public override string? Serialize(object? value) { - // Console.WriteLine("Serializing value: " + value); - // Console.WriteLine("After serializaed value: " + JsonSerializer.Serialize(value, this.options)); return value != null ? JsonSerializer.Serialize(value, this.options) : null; } From 00f5660f0dd5fb645cd6f3fb944b73947b62e1cc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 08:40:33 -0800 Subject: [PATCH 086/203] fix --- src/Worker/AzureManaged/Worker.AzureManaged.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index 508c3ad5..4e7aefd6 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -10,6 +10,7 @@ + From 53c7518dd28a1156c5ce703c7b84548bb16f5be7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:44:37 -0800 Subject: [PATCH 087/203] cleanup --- samples/ScheduleDemo/Program.cs | 6 +-- src/ScheduledTasks/Client/WaitOptions.cs | 4 +- src/ScheduledTasks/Entity/Schedule.cs | 47 ++++++++++++------- .../Models/ScheduleActivityLog.cs | 2 +- .../Models/ScheduleDescription.cs | 2 +- .../Models/ScheduleOperationStatus.cs | 2 +- src/ScheduledTasks/Models/ScheduleState.cs | 10 ++-- 7 files changed, 40 insertions(+), 33 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index e5da5ea2..82a348dc 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -84,10 +84,11 @@ try { + // TODO: ORCHname // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") { - ScheduleId = "demo-schedule9", + ScheduleId = "demo-schedule12", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" @@ -96,7 +97,6 @@ // Get schedule handle IScheduleHandle scheduleHandle = scheduledTaskClient.GetScheduleHandle(scheduleOptions.ScheduleId); - // Create the schedule Console.WriteLine("Creating schedule..."); IScheduleWaiter waiter = await scheduleHandle.CreateAsync(scheduleOptions); @@ -129,7 +129,7 @@ Console.WriteLine(""); Console.WriteLine(""); - await Task.Delay(200000); + await Task.Delay(2000000); //Console.WriteLine("\nPress any key to delete the schedule and exit..."); //Console.ReadKey(); diff --git a/src/ScheduledTasks/Client/WaitOptions.cs b/src/ScheduledTasks/Client/WaitOptions.cs index 02cc02fc..c3d40594 100644 --- a/src/ScheduledTasks/Client/WaitOptions.cs +++ b/src/ScheduledTasks/Client/WaitOptions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; - namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -23,7 +21,7 @@ public record WaitOptions public TimeSpan? PollingInterval { get; init; } /// - /// Gets whether to use exponential backoff for polling intervals. + /// Gets a value indicating whether to use exponential backoff for polling intervals. /// When enabled, the polling interval will increase exponentially between retries. /// public bool UseExponentialBackoff { get; init; } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index c1eb59d0..bfc88479 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -53,23 +53,23 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc // or later to separate response from schedule creation and schedule responsibilities context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - catch (ScheduleInvalidTransitionException ex) + catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleInvalidTransitionEx.Message, Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure the schedule is not already created.", }); } - catch (ScheduleClientValidationException ex) + catch (ScheduleClientValidationException scheduleClientValidationEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleClientValidationEx.Message, Type = ScheduleOperationFailureType.ValidationError.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure request is valid.", @@ -77,7 +77,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (Exception ex) { - // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to create schedule", @@ -145,23 +145,23 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Succeeded.ToString()); } - catch (ScheduleInvalidTransitionException ex) + catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleInvalidTransitionEx.Message, Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure the schedule is in a valid state for update.", }); } - catch (ScheduleClientValidationException ex) + catch (ScheduleClientValidationException scheduleClientValidationEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleClientValidationEx.Message, Type = ScheduleOperationFailureType.ValidationError.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure update request is valid.", @@ -169,7 +169,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (Exception ex) { - // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to update schedule", @@ -203,20 +203,31 @@ public void PauseSchedule(TaskEntityContext context) this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Succeeded.ToString()); } - catch (ScheduleInvalidTransitionException ex) + catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleInvalidTransitionEx.Message, Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure the schedule is in a valid state for pause.", }); } + catch (ScheduleClientValidationException scheduleClientValidationEx) + { + // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); + this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails + { + Reason = scheduleClientValidationEx.Message, + Type = ScheduleOperationFailureType.ValidationError.ToString(), + OccurredAt = DateTimeOffset.UtcNow, + SuggestedFix = "Ensure request is valid.", + }); + } catch (Exception ex) { - // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to pause schedule", @@ -251,23 +262,23 @@ public void ResumeSchedule(TaskEntityContext context) // compute next run based on startat and interval context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - catch (ScheduleInvalidTransitionException ex) + catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleInvalidTransitionEx.Message, Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure the schedule is in a valid state for resume.", }); } - catch (ScheduleClientValidationException ex) + catch (ScheduleClientValidationException scheduleClientValidationEx) { // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { - Reason = ex.Message, + Reason = scheduleClientValidationEx.Message, Type = ScheduleOperationFailureType.ValidationError.ToString(), OccurredAt = DateTimeOffset.UtcNow, SuggestedFix = "Ensure request is valid.", @@ -275,7 +286,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (Exception ex) { - // this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = "Failed to resume schedule", diff --git a/src/ScheduledTasks/Models/ScheduleActivityLog.cs b/src/ScheduledTasks/Models/ScheduleActivityLog.cs index a4478614..8969df8f 100644 --- a/src/ScheduledTasks/Models/ScheduleActivityLog.cs +++ b/src/ScheduledTasks/Models/ScheduleActivityLog.cs @@ -53,4 +53,4 @@ public record FailureDetails /// Gets the suggested fix for the failure. /// public string? SuggestedFix { get; init; } -} \ No newline at end of file +} diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 50d635a0..7ae75e8b 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -90,7 +90,7 @@ public record ScheduleDescription /// A JSON string containing the schedule details. public string ToJsonString(bool pretty = false) { - System.Text.Json.JsonSerializerOptions options = pretty + System.Text.Json.JsonSerializerOptions options = pretty ? new System.Text.Json.JsonSerializerOptions { WriteIndented = true } : new System.Text.Json.JsonSerializerOptions(); return System.Text.Json.JsonSerializer.Serialize(this, options); diff --git a/src/ScheduledTasks/Models/ScheduleOperationStatus.cs b/src/ScheduledTasks/Models/ScheduleOperationStatus.cs index ef5e649c..8f91d98a 100644 --- a/src/ScheduledTasks/Models/ScheduleOperationStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleOperationStatus.cs @@ -16,5 +16,5 @@ public enum ScheduleOperationStatus /// /// The operation failed. /// - Failed + Failed, } diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index e62d4c16..dda2be21 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -10,7 +10,10 @@ class ScheduleState { const int MaxActivityLogItems = 10; - public Queue? ActivityLogs { get; set; } = new Queue(); + /// + /// Gets or sets the queue of activity logs for this schedule, maintaining the most recent entries. + /// + public Queue ActivityLogs { get; set; } = new Queue(); /// /// Gets or sets the current status of the schedule. @@ -37,11 +40,6 @@ class ScheduleState /// public ScheduleConfiguration? ScheduleConfiguration { get; set; } - /// - /// Gets the activity logs for this schedule. - /// - // public IReadOnlyCollection ActivityLogs => this.ActivityLogs1.ToList().AsReadOnly(); - /// /// Refreshes the execution token to invalidate pending schedule operations. /// From fde15ac41bbee40e7590d135545741d1115c4748 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:03:25 -0800 Subject: [PATCH 088/203] demo1 --- samples/ScheduleDemo/Program.cs | 5 +++-- .../Models/ScheduleCreationOptions.cs | 17 ++++++----------- .../Models/ScheduleDescription.cs | 13 +++++++++---- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 82a348dc..93f163e8 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -86,9 +86,10 @@ { // TODO: ORCHname // Create schedule options that runs every 30 seconds - ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("DemoOrchestration") + ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions { - ScheduleId = "demo-schedule12", + OrchestrationName = "DemoOrchestration", + ScheduleId = "demo-schedule13", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index d328ab2b..63985cb5 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -13,21 +13,16 @@ public record ScheduleCreationOptions /// TimeSpan? interval; - /// - /// Initializes a new instance of the class. - /// - /// The name of the orchestration function to schedule. - /// Thrown when is null or empty. - public ScheduleCreationOptions(string orchestrationName) - { - Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); - this.OrchestrationName = orchestrationName; - } + string orchestrationName = string.Empty; /// /// Gets the name of the orchestration function to schedule. /// - public string OrchestrationName { get; init; } + public string OrchestrationName + { + get => this.orchestrationName; + init => this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } /// /// Gets the ID of the schedule, if not provided, default to a new GUID. diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 7ae75e8b..effc5a2e 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json; + namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -90,9 +92,12 @@ public record ScheduleDescription /// A JSON string containing the schedule details. public string ToJsonString(bool pretty = false) { - System.Text.Json.JsonSerializerOptions options = pretty - ? new System.Text.Json.JsonSerializerOptions { WriteIndented = true } - : new System.Text.Json.JsonSerializerOptions(); - return System.Text.Json.JsonSerializer.Serialize(this, options); +#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances + JsonSerializerOptions options = new JsonSerializerOptions + { + WriteIndented = pretty, + }; +#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances + return System.Text.Json.JsonSerializer.Serialize(this, options); } } From d0602150764a0b5c88a0ba16f287baf4fe0be01c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:17:55 -0800 Subject: [PATCH 089/203] take orch inst id from creationoptions if provided --- src/ScheduledTasks/Entity/Schedule.cs | 15 ++++++++++++--- .../Models/ScheduleConfiguration.cs | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index bfc88479..eec3b3ea 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -368,12 +368,21 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) { try { - ScheduleConfiguration? config = this.State.ScheduleConfiguration; - context.ScheduleNewOrchestration(new TaskName(config!.OrchestrationName), config!.OrchestrationInput, new StartOrchestrationOptions(config!.OrchestrationInstanceId)); + StartOrchestrationOptions? startOrchestrationOptions = string.IsNullOrEmpty(this.State.ScheduleConfiguration?.OrchestrationInstanceId) + ? null + : new StartOrchestrationOptions(this.State.ScheduleConfiguration.OrchestrationInstanceId); + context.ScheduleNewOrchestration( + new TaskName(this.State.ScheduleConfiguration!.OrchestrationName), + this.State.ScheduleConfiguration.OrchestrationInput, + startOrchestrationOptions); } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.StartOrchestrationIfNotRunning), "Failed to start orchestration", ex); + this.logger.ScheduleOperationError( + this.State.ScheduleConfiguration!.ScheduleId, + nameof(this.StartOrchestrationIfNotRunning), + "Failed to start orchestration", + ex); } } diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 0db4e525..ce8ac540 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -47,7 +47,7 @@ public string OrchestrationName /// /// Gets or sets the instance ID of the orchestration function. /// - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + public string? OrchestrationInstanceId { get; set; } /// /// Gets or sets the start time of the schedule. From d9619738ee88bf87e35299486e52cd1a385c36fd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:25:52 -0800 Subject: [PATCH 090/203] save --- samples/ScheduleDemo/Program.cs | 2 +- src/ScheduledTasks/Models/ScheduleCreationOptions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 93f163e8..172fa2d4 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -89,7 +89,7 @@ ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions { OrchestrationName = "DemoOrchestration", - ScheduleId = "demo-schedule13", + ScheduleId = "demo-schedule14", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 63985cb5..bebc554b 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -35,9 +35,9 @@ public string OrchestrationName public string? OrchestrationInput { get; init; } /// - /// Gets the instance ID of the orchestration function, if not provided, default to a new GUID. + /// Gets the instance ID of the orchestration function. /// - public string OrchestrationInstanceId { get; init; } = Guid.NewGuid().ToString("N"); + public string? OrchestrationInstanceId { get; init; } /// /// Gets the start time of the schedule. From 556e8f4296fc1248f4b61979a6ffdb1d82f89933 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:55:29 -0800 Subject: [PATCH 091/203] remove unsupported for now --- samples/ScheduleDemo/Program.cs | 25 ++++--- src/ScheduledTasks/Client/ScheduleHandle.cs | 2 - .../Client/ScheduledTaskClient.cs | 2 - .../Models/ScheduleConfiguration.cs | 24 ------- .../ScheduleConfigurationCreateOptions.cs | 70 +++++++++++++++++++ .../ScheduleConfigurationUpdateOptions.cs | 54 ++++++++++++++ .../Models/ScheduleCreationOptions.cs | 10 --- .../Models/ScheduleDescription.cs | 10 --- .../Models/ScheduleUpdateOptions.cs | 10 --- 9 files changed, 140 insertions(+), 67 deletions(-) create mode 100644 src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs create mode 100644 src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 172fa2d4..0c692757 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -23,24 +23,23 @@ builder.AddTasks(r => { // Add a demo orchestration that will be triggered by the schedule - r.AddOrchestratorFunc("DemoOrchestration", async context => + r.AddOrchestratorFunc("DemoOrchestration", async (context, symbol) => { - string stockSymbol = "MSFT"; // Default to MSFT if no input provided var logger = context.CreateReplaySafeLogger("DemoOrchestration"); - logger.LogInformation("Getting stock price for: {symbol}", stockSymbol); + logger.LogInformation("Getting stock price for: {symbol}", symbol); try { // Get current stock price - decimal currentPrice = await context.CallActivityAsync("GetStockPrice", stockSymbol); + decimal currentPrice = await context.CallActivityAsync("GetStockPrice", symbol); - logger.LogInformation("Current price for {symbol} is ${price:F2}", stockSymbol, currentPrice); + logger.LogInformation("Current price for {symbol} is ${price:F2}", symbol, currentPrice); - return $"Stock {stockSymbol} price: ${currentPrice:F2} at {DateTime.UtcNow}"; + return $"Stock {symbol} price: ${currentPrice:F2} at {DateTime.UtcNow}"; } catch (Exception ex) { - logger.LogError(ex, "Error processing stock price for {symbol}", stockSymbol); + logger.LogError(ex, "Error processing stock price for {symbol}", symbol); throw; } }); @@ -84,12 +83,20 @@ try { - // TODO: ORCHname + // list all schedules + var schedules = await scheduledTaskClient.ListSchedulesAsync(false); + foreach (var schedule in schedules) + { + var handle = scheduledTaskClient.GetScheduleHandle(schedule.ScheduleId); + await handle.DeleteAsync(); + Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); + } + // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions { OrchestrationName = "DemoOrchestration", - ScheduleId = "demo-schedule14", + ScheduleId = "demo-schedule101", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 89b51a24..890540a8 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -76,8 +76,6 @@ public async Task DescribeAsync(bool includeFullActivityLog StartAt = config?.StartAt, EndAt = config?.EndAt, Interval = config?.Interval, - CronExpression = config?.CronExpression, - MaxOccurrence = config?.MaxOccurrence ?? 0, StartImmediatelyIfLate = config?.StartImmediatelyIfLate, Status = state.Status, ExecutionToken = state.ExecutionToken, diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index aff05400..8aede822 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -64,8 +64,6 @@ public async Task> ListSchedulesAsync(bool incl StartAt = config.StartAt, EndAt = config.EndAt, Interval = config.Interval, - CronExpression = config.CronExpression, - MaxOccurrence = config.MaxOccurrence, StartImmediatelyIfLate = config.StartImmediatelyIfLate, Status = metadata.State.Status, ExecutionToken = metadata.State.ExecutionToken, diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index ce8ac540..09176a7e 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -86,16 +86,6 @@ public TimeSpan? Interval } } - /// - /// Gets or sets the cron expression for the schedule. - /// - public string? CronExpression { get; set; } - - /// - /// Gets or sets the maximum number of times the schedule should run. - /// - public int MaxOccurrence { get; set; } - /// /// Gets or sets whether the schedule should start immediately if it's late. /// @@ -115,8 +105,6 @@ public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions cr StartAt = createOptions.StartAt, EndAt = createOptions.EndAt, Interval = createOptions.Interval, - CronExpression = createOptions.CronExpression, - MaxOccurrence = createOptions.MaxOccurrence, StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate, }; } @@ -167,18 +155,6 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) updatedFields.Add(nameof(this.Interval)); } - if (!string.IsNullOrEmpty(updateOptions.CronExpression)) - { - this.CronExpression = updateOptions.CronExpression; - updatedFields.Add(nameof(this.CronExpression)); - } - - if (updateOptions.MaxOccurrence != 0) - { - this.MaxOccurrence = updateOptions.MaxOccurrence; - updatedFields.Add(nameof(this.MaxOccurrence)); - } - if (updateOptions.StartImmediatelyIfLate.HasValue) { this.StartImmediatelyIfLate = updateOptions.StartImmediatelyIfLate.Value; diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs new file mode 100644 index 00000000..b543f6e6 --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Configuration for a scheduled task. +/// +public class ScheduleConfigurationCreateOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + public ScheduleConfigurationCreateOptions(string orchestrationName, string scheduleId) + { + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + } + + string orchestrationName; + + public string OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); + } + } + + public string ScheduleId { get; init; } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public bool? StartImmediatelyIfLate { get; set; } +} diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs new file mode 100644 index 00000000..6c8344d6 --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +public class ScheduleConfigurationUpdateOptions +{ + string? orchestrationName; + + public string? OrchestrationName + { + get => this.orchestrationName; + set + { + this.orchestrationName = value; + } + } + + public string? OrchestrationInput { get; set; } + + public string? OrchestrationInstanceId { get; set; } + + public DateTimeOffset? StartAt { get; set; } + + public DateTimeOffset? EndAt { get; set; } + + TimeSpan? interval; + + public TimeSpan? Interval + { + get => this.interval; + set + { + if (!value.HasValue) + { + return; + } + + if (value.Value <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(value)); + } + + if (value.Value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); + } + + this.interval = value; + } + } + + public bool? StartImmediatelyIfLate { get; set; } +} diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index bebc554b..a23f3080 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -74,16 +74,6 @@ public TimeSpan? Interval } } - /// - /// Gets the cron expression for the schedule. - /// - public string? CronExpression { get; init; } - - /// - /// Gets the maximum number of occurrences for the schedule. - /// - public int MaxOccurrence { get; init; } - /// /// Gets a value indicating whether to start the schedule immediately if it is late. /// diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index effc5a2e..7575f18b 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -45,16 +45,6 @@ public record ScheduleDescription /// public TimeSpan? Interval { get; init; } - /// - /// Gets the optional cron expression for the schedule. - /// - public string? CronExpression { get; init; } - - /// - /// Gets the maximum number of times the schedule should run. - /// - public int MaxOccurrence { get; init; } - /// /// Gets whether the schedule should run immediately if started late. /// diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 69e94db1..5e9fcc25 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -60,16 +60,6 @@ public TimeSpan? Interval } } - /// - /// Gets or initializes the cron expression for the schedule. - /// - public string? CronExpression { get; init; } - - /// - /// Gets or initializes the maximum number of times the schedule should run. - /// - public int MaxOccurrence { get; init; } - /// /// Gets or initializes whether the schedule should start immediately if it's late. /// From 51cc78a2cee6740dbbccaa3e610ee1a782569e7c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:05:58 -0800 Subject: [PATCH 092/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index eec3b3ea..67c63c63 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -322,6 +322,14 @@ public void RunSchedule(TaskEntityContext context, string executionToken) throw exception; } + // if endat is set and time now is past endat, do not run + if (scheduleConfig.EndAt.HasValue && DateTimeOffset.UtcNow > scheduleConfig.EndAt.Value) + { + this.logger.ScheduleRunCancelled(scheduleConfig.ScheduleId, executionToken); + this.State.NextRunAt = null; + return; + } + // run schedule based on next run at // need to enforce the constraint here NextRunAt truly represents the next run at // if next run at is null, this means schedule is changed, we compute the next run at based on startat and update From fb31819843363a98991777bea26c2c1c2f6d1fab Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:12:46 -0800 Subject: [PATCH 093/203] save --- src/ScheduledTasks/Logging/Client/Logs.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 8f3aad03..7319dc67 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -11,33 +11,32 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. /// -// TODO: Do we really need all of these? static partial class Logs { - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Client: Creating schedule with options: {scheduleConfigCreateOptions}")] + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] public static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Client: Getting schedule handle for schedule '{scheduleId}'")] + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Getting schedule handle for schedule '{scheduleId}'")] public static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Client: Pausing schedule '{scheduleId}'")] + [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Client: Resuming schedule '{scheduleId}'")] + [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] public static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Client: Updating schedule '{scheduleId}'")] + [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}'")] public static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Client: Deleting schedule '{scheduleId}'")] + [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Client: {message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientInfo(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 10, Level = LogLevel.Warning, Message = "Client Warning: {message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 10, Level = LogLevel.Warning, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientWarning(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "Client Error: {message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientError(this ILogger logger, string message, string scheduleId); } From f58003404fcedac2d8c326a2dcf5cb9686cf7b85 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:14:58 -0800 Subject: [PATCH 094/203] save --- src/ScheduledTasks/Logging/Entity/Logs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Entity/Logs.cs index a0354842..a0cb8540 100644 --- a/src/ScheduledTasks/Logging/Entity/Logs.cs +++ b/src/ScheduledTasks/Logging/Entity/Logs.cs @@ -13,8 +13,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// static partial class Logs { - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Schedule is being created with options: {scheduleConfigurationCreateOptions}")] - public static partial void CreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigurationCreateOptions); + [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being created")] + public static partial void CreatingSchedule(this ILogger logger, string scheduleId); [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is created")] public static partial void CreatedSchedule(this ILogger logger, string scheduleId); From 4bebb190f0c797a023d4f07bb99e66a2b34d9fef Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:39:42 -0800 Subject: [PATCH 095/203] comment --- samples/ScheduleDemo/Program.cs | 1 - .../ScheduleConfigurationCreateOptions.cs | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index 0c692757..fc143781 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -27,7 +27,6 @@ { var logger = context.CreateReplaySafeLogger("DemoOrchestration"); logger.LogInformation("Getting stock price for: {symbol}", symbol); - try { // Get current stock price diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs index b543f6e6..aa6bb884 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs @@ -4,44 +4,62 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Configuration for a scheduled task. +/// Configuration options for creating a scheduled task. /// public class ScheduleConfigurationCreateOptions { + string orchestrationName; + /// /// Initializes a new instance of the class. /// - /// - /// + /// The name of the orchestration to schedule. + /// Optional ID for the schedule. If not provided, a new GUID will be generated. public ScheduleConfigurationCreateOptions(string orchestrationName, string scheduleId) { this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); } - string orchestrationName; - + /// + /// Gets or sets the name of the orchestration to schedule. + /// public string OrchestrationName { get => this.orchestrationName; - set - { - this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } + set => this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); } + /// + /// Gets the unique identifier for this schedule. + /// public string ScheduleId { get; init; } + /// + /// Gets or sets the input data to pass to the orchestration. + /// public string? OrchestrationInput { get; set; } + /// + /// Gets or sets the instance ID for the orchestration. Defaults to a new GUID. + /// public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); + /// + /// Gets or sets when the schedule should start. + /// public DateTimeOffset? StartAt { get; set; } + /// + /// Gets or sets when the schedule should end. + /// public DateTimeOffset? EndAt { get; set; } TimeSpan? interval; + /// + /// Gets or sets the time interval between schedule executions. Must be at least 1 second. + /// public TimeSpan? Interval { get => this.interval; @@ -66,5 +84,8 @@ public TimeSpan? Interval } } + /// + /// Gets or sets whether to start immediately if the schedule is already late. + /// public bool? StartImmediatelyIfLate { get; set; } } From 7bffc261792e50752d19e4997497dead22128872 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:45:21 -0800 Subject: [PATCH 096/203] save --- .../ScheduleConfigurationCreateOptions.cs | 8 +++---- .../ScheduleConfigurationUpdateOptions.cs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs index aa6bb884..ebf358c8 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs @@ -8,8 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleConfigurationCreateOptions { - string orchestrationName; - /// /// Initializes a new instance of the class. /// @@ -55,8 +53,6 @@ public string OrchestrationName /// public DateTimeOffset? EndAt { get; set; } - TimeSpan? interval; - /// /// Gets or sets the time interval between schedule executions. Must be at least 1 second. /// @@ -88,4 +84,8 @@ public TimeSpan? Interval /// Gets or sets whether to start immediately if the schedule is already late. /// public bool? StartImmediatelyIfLate { get; set; } + + string orchestrationName; + + TimeSpan? interval; } diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs index 6c8344d6..4211734f 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs @@ -3,10 +3,16 @@ namespace Microsoft.DurableTask.ScheduledTasks; +/// +/// Configuration options for updating a scheduled task. +/// public class ScheduleConfigurationUpdateOptions { string? orchestrationName; + /// + /// Gets or sets the name of the orchestration to schedule. + /// public string? OrchestrationName { get => this.orchestrationName; @@ -16,16 +22,31 @@ public string? OrchestrationName } } + /// + /// Gets or sets the input data to pass to the orchestration. + /// public string? OrchestrationInput { get; set; } + /// + /// Gets or sets the instance ID for the orchestration. + /// public string? OrchestrationInstanceId { get; set; } + /// + /// Gets or sets when the schedule should start. + /// public DateTimeOffset? StartAt { get; set; } + /// + /// Gets or sets when the schedule should end. + /// public DateTimeOffset? EndAt { get; set; } TimeSpan? interval; + /// + /// Gets or sets the time interval between schedule executions. Must be at least 1 second. + /// public TimeSpan? Interval { get => this.interval; @@ -50,5 +71,8 @@ public TimeSpan? Interval } } + /// + /// Gets or sets whether to start immediately if the schedule is already late. + /// public bool? StartImmediatelyIfLate { get; set; } } From e13b25db943a103e0cca2229ce1431a6fee3d865 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:48:28 -0800 Subject: [PATCH 097/203] style --- .../Models/ScheduleConfigurationCreateOptions.cs | 7 +++---- .../Models/ScheduleConfigurationUpdateOptions.cs | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs index ebf358c8..38becc40 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs @@ -8,6 +8,9 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// public class ScheduleConfigurationCreateOptions { + string orchestrationName; + TimeSpan? interval; + /// /// Initializes a new instance of the class. /// @@ -84,8 +87,4 @@ public TimeSpan? Interval /// Gets or sets whether to start immediately if the schedule is already late. /// public bool? StartImmediatelyIfLate { get; set; } - - string orchestrationName; - - TimeSpan? interval; } diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs index 4211734f..fca78a21 100644 --- a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs @@ -9,6 +9,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; public class ScheduleConfigurationUpdateOptions { string? orchestrationName; + TimeSpan? interval; /// /// Gets or sets the name of the orchestration to schedule. @@ -42,8 +43,6 @@ public string? OrchestrationName /// public DateTimeOffset? EndAt { get; set; } - TimeSpan? interval; - /// /// Gets or sets the time interval between schedule executions. Must be at least 1 second. /// From 9048e151f4df62a1794f1b966a283352a87121d5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:01:03 -0800 Subject: [PATCH 098/203] update eventids --- src/ScheduledTasks/Logging/Client/Logs.cs | 18 ++++++------ src/ScheduledTasks/Logging/Entity/Logs.cs | 34 +++++++++++------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 7319dc67..55f66204 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -13,30 +13,30 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// static partial class Logs { - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] + [LoggerMessage(EventId = 80, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] public static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Getting schedule handle for schedule '{scheduleId}'")] + [LoggerMessage(EventId = 81, Level = LogLevel.Information, Message = "Getting schedule handle for schedule '{scheduleId}'")] public static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] + [LoggerMessage(EventId = 82, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] + [LoggerMessage(EventId = 83, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] public static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}'")] + [LoggerMessage(EventId = 84, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}'")] public static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] + [LoggerMessage(EventId = 85, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 86, Level = LogLevel.Information, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientInfo(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 10, Level = LogLevel.Warning, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 87, Level = LogLevel.Warning, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientWarning(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 11, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 88, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientError(this ILogger logger, string message, string scheduleId); } diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Entity/Logs.cs index a0cb8540..cad2269c 100644 --- a/src/ScheduledTasks/Logging/Entity/Logs.cs +++ b/src/ScheduledTasks/Logging/Entity/Logs.cs @@ -13,51 +13,51 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// static partial class Logs { - [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being created")] + [LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being created")] public static partial void CreatingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is created")] + [LoggerMessage(EventId = 101, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is created")] public static partial void CreatedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 3, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being updated with options: {scheduleConfigurationUpdateOptions}")] - public static partial void UpdatingSchedule(this ILogger logger, string scheduleId, ScheduleUpdateOptions scheduleConfigurationUpdateOptions); + [LoggerMessage(EventId = 102, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being updated")] + public static partial void UpdatingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 4, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is updated")] + [LoggerMessage(EventId = 103, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is updated")] public static partial void UpdatedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being paused")] + [LoggerMessage(EventId = 104, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being paused")] public static partial void PausingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 6, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is paused")] + [LoggerMessage(EventId = 105, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is paused")] public static partial void PausedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 7, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being resumed")] + [LoggerMessage(EventId = 106, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being resumed")] public static partial void ResumingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 8, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is resumed")] + [LoggerMessage(EventId = 107, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is resumed")] public static partial void ResumedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 9, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is running")] + [LoggerMessage(EventId = 108, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is running")] public static partial void RunningSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 10, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is executed")] + [LoggerMessage(EventId = 109, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is executed")] public static partial void CompletedScheduleRun(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 11, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being deleted")] + [LoggerMessage(EventId = 110, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being deleted")] public static partial void DeletingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 12, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is deleted")] + [LoggerMessage(EventId = 111, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is deleted")] public static partial void DeletedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 13, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] + [LoggerMessage(EventId = 112, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] public static partial void ScheduleOperationInfo(this ILogger logger, string scheduleId, string operationName, string infoMessage); - [LoggerMessage(EventId = 14, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] + [LoggerMessage(EventId = 113, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] public static partial void ScheduleOperationWarning(this ILogger logger, string scheduleId, string operationName, string warningMessage); - [LoggerMessage(EventId = 15, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] + [LoggerMessage(EventId = 114, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] public static partial void ScheduleOperationError(this ILogger logger, string scheduleId, string operationName, string errorMessage, Exception? exception = null); - [LoggerMessage(EventId = 16, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] + [LoggerMessage(EventId = 115, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] public static partial void ScheduleRunCancelled(this ILogger logger, string scheduleId, string executionToken); } From 820007208c9d6a2efa1134754788d1d35d8d972a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:13:15 -0800 Subject: [PATCH 099/203] startimmediatelyifalte --- src/ScheduledTasks/Entity/Schedule.cs | 2 +- src/ScheduledTasks/Models/ScheduleConfiguration.cs | 2 +- src/ScheduledTasks/Models/ScheduleCreationOptions.cs | 4 ++-- src/ScheduledTasks/Models/ScheduleUpdateOptions.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 67c63c63..907b68c8 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -340,7 +340,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) // else, it has run before, we cant run at startat, need to compute next run at based on last run at + num of intervals between last runtime and now plus 1 if (!this.State.LastRunAt.HasValue) { - this.State.NextRunAt = scheduleConfig.StartAt; + this.State.NextRunAt = scheduleConfig.StartImmediatelyIfLate == true ? DateTimeOffset.UtcNow : scheduleConfig.StartAt; } else { diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 09176a7e..d7ffea56 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -89,7 +89,7 @@ public TimeSpan? Interval /// /// Gets or sets whether the schedule should start immediately if it's late. /// - public bool? StartImmediatelyIfLate { get; set; } + public bool? StartImmediatelyIfLate { get; set; } = false; /// /// Creates a new configuration from the provided creation options. diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index a23f3080..9824587e 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -75,7 +75,7 @@ public TimeSpan? Interval } /// - /// Gets a value indicating whether to start the schedule immediately if it is late. + /// Gets a value indicating whether to start the schedule immediately if it is late. Default is false. /// - public bool? StartImmediatelyIfLate { get; init; } + public bool? StartImmediatelyIfLate { get; init; } = false; } diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 5e9fcc25..63144737 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -61,7 +61,7 @@ public TimeSpan? Interval } /// - /// Gets or initializes whether the schedule should start immediately if it's late. + /// Gets or initializes whether the schedule should start immediately if it's late. Default is false. /// - public bool? StartImmediatelyIfLate { get; init; } + public bool? StartImmediatelyIfLate { get; init; } = false; } From 0f88a0b4237cb2f8a7096e16f765424c3f729e4f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:17:30 -0800 Subject: [PATCH 100/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 907b68c8..7b1a9f3e 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -340,7 +340,14 @@ public void RunSchedule(TaskEntityContext context, string executionToken) // else, it has run before, we cant run at startat, need to compute next run at based on last run at + num of intervals between last runtime and now plus 1 if (!this.State.LastRunAt.HasValue) { - this.State.NextRunAt = scheduleConfig.StartImmediatelyIfLate == true ? DateTimeOffset.UtcNow : scheduleConfig.StartAt; + if (scheduleConfig.StartImmediatelyIfLate == true && scheduleConfig.StartAt.HasValue && DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) + { + this.State.NextRunAt = DateTimeOffset.UtcNow; + } + else + { + this.State.NextRunAt = scheduleConfig.StartAt; + } } else { From 75fb8e3bd0c9e68a89996e0749ca1bc9f95bc596 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:39:02 -0800 Subject: [PATCH 101/203] refactor --- src/ScheduledTasks/Entity/Schedule.cs | 77 ++++++++++++++++----------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 7b1a9f3e..45c69320 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -21,6 +21,33 @@ class Schedule(ILogger logger) : TaskEntity { readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + static DateTimeOffset? ComputeInitialRunTime(ScheduleConfiguration scheduleConfig) + { + if (scheduleConfig.StartImmediatelyIfLate == true && + scheduleConfig.StartAt.HasValue && + DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) + { + return DateTimeOffset.UtcNow; + } + + return scheduleConfig.StartAt ?? DateTimeOffset.UtcNow; // Default to now if StartAt not defined + } + + static DateTimeOffset ComputeNextRunTime(ScheduleConfiguration scheduleConfig, DateTimeOffset lastRunAt) + { + if (!scheduleConfig.Interval.HasValue) + { + throw new InvalidOperationException("Interval must be set to compute next run time."); + } + + // Calculate number of intervals between last run and now + TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - lastRunAt; + int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); + + // Compute and return the next run time + return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); + } + /// /// Creates a new schedule with the specified configuration. /// @@ -330,39 +357,11 @@ public void RunSchedule(TaskEntityContext context, string executionToken) return; } - // run schedule based on next run at - // need to enforce the constraint here NextRunAt truly represents the next run at - // if next run at is null, this means schedule is changed, we compute the next run at based on startat and update - // else if next run at is set, then we run at next run at - if (!this.State.NextRunAt.HasValue) - { - // check whats last run at time, if not set, meaning it has not run once, we run at startat - // else, it has run before, we cant run at startat, need to compute next run at based on last run at + num of intervals between last runtime and now plus 1 - if (!this.State.LastRunAt.HasValue) - { - if (scheduleConfig.StartImmediatelyIfLate == true && scheduleConfig.StartAt.HasValue && DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) - { - this.State.NextRunAt = DateTimeOffset.UtcNow; - } - else - { - this.State.NextRunAt = scheduleConfig.StartAt; - } - } - else - { - // Calculate number of intervals between last run and now - TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - this.State.LastRunAt.Value; - int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); - - // Compute the next run time - this.State.NextRunAt = this.State.LastRunAt.Value + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); - } - } + this.DetermineNextRunTime(scheduleConfig); DateTimeOffset currentTime = DateTimeOffset.UtcNow; - if (!this.State.NextRunAt.HasValue || this.State.NextRunAt!.Value <= currentTime) + if (this.State.NextRunAt!.Value <= currentTime) { this.State.NextRunAt = currentTime; this.StartOrchestrationIfNotRunning(context); @@ -420,4 +419,22 @@ void TryStatusTransition(string operationName, ScheduleStatus to) this.State.Status = to; } + + void DetermineNextRunTime(ScheduleConfiguration scheduleConfig) + { + if (this.State.NextRunAt.HasValue) + { + return; // NextRunAt already set, no need to compute + } + + // If LastRunAt is not set, determine if we should start immediately or at StartAt + if (!this.State.LastRunAt.HasValue) + { + this.State.NextRunAt = ComputeInitialRunTime(scheduleConfig); + } + else + { + this.State.NextRunAt = ComputeNextRunTime(scheduleConfig, this.State.LastRunAt.Value); + } + } } From 56d9443b92a35b75cc4dd13f618e6d25427fc20d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:40:52 -0800 Subject: [PATCH 102/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 45c69320..40d6cfa7 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -21,33 +21,6 @@ class Schedule(ILogger logger) : TaskEntity { readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); - static DateTimeOffset? ComputeInitialRunTime(ScheduleConfiguration scheduleConfig) - { - if (scheduleConfig.StartImmediatelyIfLate == true && - scheduleConfig.StartAt.HasValue && - DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) - { - return DateTimeOffset.UtcNow; - } - - return scheduleConfig.StartAt ?? DateTimeOffset.UtcNow; // Default to now if StartAt not defined - } - - static DateTimeOffset ComputeNextRunTime(ScheduleConfiguration scheduleConfig, DateTimeOffset lastRunAt) - { - if (!scheduleConfig.Interval.HasValue) - { - throw new InvalidOperationException("Interval must be set to compute next run time."); - } - - // Calculate number of intervals between last run and now - TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - lastRunAt; - int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); - - // Compute and return the next run time - return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); - } - /// /// Creates a new schedule with the specified configuration. /// @@ -378,6 +351,33 @@ public void RunSchedule(TaskEntityContext context, string executionToken) new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); } + static DateTimeOffset? ComputeInitialRunTime(ScheduleConfiguration scheduleConfig) + { + if (scheduleConfig.StartImmediatelyIfLate == true && + scheduleConfig.StartAt.HasValue && + DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) + { + return DateTimeOffset.UtcNow; + } + + return scheduleConfig.StartAt ?? DateTimeOffset.UtcNow; // Default to now if StartAt not defined + } + + static DateTimeOffset ComputeNextRunTime(ScheduleConfiguration scheduleConfig, DateTimeOffset lastRunAt) + { + if (!scheduleConfig.Interval.HasValue) + { + throw new InvalidOperationException("Interval must be set to compute next run time."); + } + + // Calculate number of intervals between last run and now + TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - lastRunAt; + int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); + + // Compute and return the next run time + return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); + } + void StartOrchestrationIfNotRunning(TaskEntityContext context) { try From e16ab9ce35c077b06c273675b66dcc3b555030d8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:55:31 -0800 Subject: [PATCH 103/203] before waiter refactor --- src/ScheduledTasks/Entity/Schedule.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 40d6cfa7..e2e18d90 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -37,7 +37,6 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc throw new ScheduleInvalidTransitionException(scheduleCreationOptions?.ScheduleId ?? string.Empty, this.State.Status, ScheduleStatus.Active, nameof(this.CreateSchedule)); } - // CreateSchedule is allowed, we shall throw exception if any following step failed to inform caller if (scheduleCreationOptions == null) { throw new ScheduleClientValidationException(string.Empty, "Schedule creation options cannot be null"); @@ -55,7 +54,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(scheduleCreationOptions?.ScheduleId ?? string.Empty, nameof(this.CreateSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleInvalidTransitionEx.Message, @@ -66,7 +65,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (ScheduleClientValidationException scheduleClientValidationEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.CreateSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(scheduleCreationOptions?.ScheduleId ?? string.Empty, nameof(this.CreateSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleClientValidationEx.Message, @@ -147,7 +146,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleInvalidTransitionEx.Message, @@ -158,7 +157,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche } catch (ScheduleClientValidationException scheduleClientValidationEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.UpdateSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleClientValidationEx.Message, @@ -205,7 +204,7 @@ public void PauseSchedule(TaskEntityContext context) } catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleInvalidTransitionEx.Message, @@ -216,7 +215,7 @@ public void PauseSchedule(TaskEntityContext context) } catch (ScheduleClientValidationException scheduleClientValidationEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.PauseSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleClientValidationEx.Message, @@ -264,7 +263,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleInvalidTransitionEx.Message, @@ -275,7 +274,7 @@ public void ResumeSchedule(TaskEntityContext context) } catch (ScheduleClientValidationException scheduleClientValidationEx) { - // this.logger.ScheduleOperationError(ex.ScheduleId, nameof(this.ResumeSchedule), ex.Message, ex); + this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails { Reason = scheduleClientValidationEx.Message, From 78e4966a0106e515870420088c1d26eeb2653c5c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:18:10 -0800 Subject: [PATCH 104/203] logging rule scheduledemo added --- samples/ScheduleDemo/appsettings.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 samples/ScheduleDemo/appsettings.json diff --git a/samples/ScheduleDemo/appsettings.json b/samples/ScheduleDemo/appsettings.json new file mode 100644 index 00000000..b7ce5f11 --- /dev/null +++ b/samples/ScheduleDemo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "ScheduleDemo": "Debug", + "DemoOrchestration": "Debug" + } + } +} \ No newline at end of file From 5e29246408964d8b4594cc0b568c7f6642c41d28 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:16:11 -0800 Subject: [PATCH 105/203] sample fb --- .../ScheduleDemo/Activities/GetStockPrice.cs | 16 +++++++++ .../Orchestrators/StockPriceOrchestrator.cs | 29 ++++++++++++++++ samples/ScheduleDemo/Program.cs | 34 ++++--------------- 3 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 samples/ScheduleDemo/Activities/GetStockPrice.cs create mode 100644 samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs diff --git a/samples/ScheduleDemo/Activities/GetStockPrice.cs b/samples/ScheduleDemo/Activities/GetStockPrice.cs new file mode 100644 index 00000000..b9e96b3a --- /dev/null +++ b/samples/ScheduleDemo/Activities/GetStockPrice.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace ScheduleDemo.Activities; + +[DurableTask] +public class GetStockPrice : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string symbol) + { + // Mock implementation - would normally call stock API + return Task.FromResult(100.00m); + } +} diff --git a/samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs b/samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs new file mode 100644 index 00000000..27852813 --- /dev/null +++ b/samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; +using Microsoft.Extensions.Logging; + +[DurableTask] +public class StockPriceOrchestrator : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string symbol) + { + var logger = context.CreateReplaySafeLogger("DemoOrchestration"); + logger.LogInformation("Getting stock price for: {symbol}", symbol); + try + { + // Get current stock price + decimal currentPrice = await context.CallActivityAsync("GetStockPrice", symbol); + + logger.LogInformation("Current price for {symbol} is ${price:F2}", symbol, currentPrice); + + return $"Stock {symbol} price: ${currentPrice:F2} at {DateTime.UtcNow}"; + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing stock price for {symbol}", symbol); + throw; + } + } +} \ No newline at end of file diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index fc143781..ed5eed9b 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using ScheduleDemo.Activities; + // Create the host builder IHost host = Host.CreateDefaultBuilder(args) @@ -22,33 +24,11 @@ // Add the Schedule entity and demo orchestration builder.AddTasks(r => { - // Add a demo orchestration that will be triggered by the schedule - r.AddOrchestratorFunc("DemoOrchestration", async (context, symbol) => - { - var logger = context.CreateReplaySafeLogger("DemoOrchestration"); - logger.LogInformation("Getting stock price for: {symbol}", symbol); - try - { - // Get current stock price - decimal currentPrice = await context.CallActivityAsync("GetStockPrice", symbol); - - logger.LogInformation("Current price for {symbol} is ${price:F2}", symbol, currentPrice); - - return $"Stock {symbol} price: ${currentPrice:F2} at {DateTime.UtcNow}"; - } - catch (Exception ex) - { - logger.LogError(ex, "Error processing stock price for {symbol}", symbol); - throw; - } - }); + // Add the orchestrator class + r.AddOrchestrator(); // Add required activities - r.AddActivityFunc("GetStockPrice", (context, symbol) => - { - // Mock implementation - would normally call stock API - return 100.00m; - }); + r.AddActivity(); }); // Enable scheduled tasks support @@ -94,7 +74,7 @@ // Create schedule options that runs every 30 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions { - OrchestrationName = "DemoOrchestration", + OrchestrationName = nameof(StockPriceOrchestrator), ScheduleId = "demo-schedule101", Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, @@ -136,7 +116,7 @@ Console.WriteLine(""); Console.WriteLine(""); - await Task.Delay(2000000); + await Task.Delay(TimeSpan.FromMinutes(30)); //Console.WriteLine("\nPress any key to delete the schedule and exit..."); //Console.ReadKey(); From 6d66b5ad4bb83dac6de9c8045e206159fe7ed693 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:27:44 -0800 Subject: [PATCH 106/203] fb --- samples/ScheduleDemo/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index ed5eed9b..f7e8c693 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -71,7 +71,7 @@ Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } - // Create schedule options that runs every 30 seconds + // Create schedule options that runs every 4 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions { OrchestrationName = nameof(StockPriceOrchestrator), From d93986f4b7a40f1fe194efbd1ed62bbeaedb7b7f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 08:58:51 -0800 Subject: [PATCH 107/203] param --- src/ScheduledTasks/Entity/Schedule.cs | 2 +- .../Models/ScheduleConfiguration.cs | 6 ++--- .../Models/ScheduleCreationOptions.cs | 23 ++++++++----------- .../Models/ScheduleUpdateOptions.cs | 4 ++-- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index e2e18d90..893d20b7 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -352,7 +352,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) static DateTimeOffset? ComputeInitialRunTime(ScheduleConfiguration scheduleConfig) { - if (scheduleConfig.StartImmediatelyIfLate == true && + if (scheduleConfig.StartImmediatelyIfLate && scheduleConfig.StartAt.HasValue && DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) { diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index d7ffea56..6c2c721b 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -87,9 +87,9 @@ public TimeSpan? Interval } /// - /// Gets or sets whether the schedule should start immediately if it's late. + /// Gets or sets a value indicating whether gets or sets whether the schedule should start immediately if it's late. /// - public bool? StartImmediatelyIfLate { get; set; } = false; + public bool StartImmediatelyIfLate { get; set; } /// /// Creates a new configuration from the provided creation options. @@ -125,7 +125,7 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) updatedFields.Add(nameof(this.OrchestrationName)); } - if (updateOptions.OrchestrationInput == null) + if (!string.IsNullOrEmpty(updateOptions.OrchestrationInput)) { this.OrchestrationInput = updateOptions.OrchestrationInput; updatedFields.Add(nameof(this.OrchestrationInput)); diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 9824587e..a31783b7 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -11,7 +11,7 @@ public record ScheduleCreationOptions /// /// The interval of the schedule. /// - TimeSpan? interval; + TimeSpan interval; string orchestrationName = string.Empty; @@ -27,7 +27,7 @@ public string OrchestrationName /// /// Gets the ID of the schedule, if not provided, default to a new GUID. /// - public string ScheduleId { get; init; } = Guid.NewGuid().ToString("N"); + public string ScheduleId { get; init; }; /// /// Gets the input to the orchestration function. @@ -52,22 +52,19 @@ public string OrchestrationName /// /// Gets the interval of the schedule. /// - public TimeSpan? Interval + public TimeSpan Interval { get => this.interval; init { - if (value.HasValue) + if (value <= TimeSpan.Zero) { - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } + throw new ArgumentException("Interval must be positive", nameof(value)); + } - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } + if (value.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(value)); } this.interval = value; @@ -77,5 +74,5 @@ public TimeSpan? Interval /// /// Gets a value indicating whether to start the schedule immediately if it is late. Default is false. /// - public bool? StartImmediatelyIfLate { get; init; } = false; + public bool StartImmediatelyIfLate { get; init; } } diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 63144737..5e9fcc25 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -61,7 +61,7 @@ public TimeSpan? Interval } /// - /// Gets or initializes whether the schedule should start immediately if it's late. Default is false. + /// Gets or initializes whether the schedule should start immediately if it's late. /// - public bool? StartImmediatelyIfLate { get; init; } = false; + public bool? StartImmediatelyIfLate { get; init; } } From 8086607ff264c3bace0c56be007b6693dac6f03d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:50:41 -0800 Subject: [PATCH 108/203] creatopt update --- samples/ScheduleDemo/Program.cs | 5 +- .../Models/ScheduleCreationOptions.cs | 55 +++++++++---------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index f7e8c693..e2e8517d 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -72,11 +72,8 @@ } // Create schedule options that runs every 4 seconds - ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions + ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) { - OrchestrationName = nameof(StockPriceOrchestrator), - ScheduleId = "demo-schedule101", - Interval = TimeSpan.FromSeconds(4), StartAt = DateTimeOffset.UtcNow, OrchestrationInput = "MSFT" }; diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index a31783b7..86b8f319 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -9,25 +9,37 @@ namespace Microsoft.DurableTask.ScheduledTasks; public record ScheduleCreationOptions { /// - /// The interval of the schedule. + /// Initializes a new instance of the class. /// - TimeSpan interval; + /// The ID of the schedule, or null to generate one. + /// The name of the orchestration to schedule. + /// The time interval between schedule executions. Must be at least 1 second and cannot be negative. + public ScheduleCreationOptions(string scheduleId, string orchestrationName, TimeSpan interval) + { + this.ScheduleId = Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.OrchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + if (interval <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(interval)); + } + + if (interval.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(interval)); + } - string orchestrationName = string.Empty; + this.Interval = interval; + } /// /// Gets the name of the orchestration function to schedule. /// - public string OrchestrationName - { - get => this.orchestrationName; - init => this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } + public string OrchestrationName { get; } /// - /// Gets the ID of the schedule, if not provided, default to a new GUID. + /// Gets the ID of the schedule. /// - public string ScheduleId { get; init; }; + public string ScheduleId { get; } /// /// Gets the input to the orchestration function. @@ -40,36 +52,19 @@ public string OrchestrationName public string? OrchestrationInstanceId { get; init; } /// - /// Gets the start time of the schedule. + /// Gets the start time of the schedule. If not provided, default to the current time. /// public DateTimeOffset? StartAt { get; init; } /// - /// Gets the end time of the schedule. + /// Gets the end time of the schedule. If not provided, schedule will run indefinitely. /// public DateTimeOffset? EndAt { get; init; } /// /// Gets the interval of the schedule. /// - public TimeSpan Interval - { - get => this.interval; - init - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } - - this.interval = value; - } - } + public TimeSpan Interval { get; } /// /// Gets a value indicating whether to start the schedule immediately if it is late. Default is false. From 47d9af0a3e24b2211ed3d9d44e9fc58ad1a1d127 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:08:06 -0800 Subject: [PATCH 109/203] update scheduleconfig --- src/ScheduledTasks/Entity/Schedule.cs | 11 ++-- .../Models/ScheduleConfiguration.cs | 51 +++++++++++-------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 893d20b7..f96ec671 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -305,7 +305,7 @@ public void ResumeSchedule(TaskEntityContext context) public void RunSchedule(TaskEntityContext context, string executionToken) { ScheduleConfiguration scheduleConfig = Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - TimeSpan interval = scheduleConfig.Interval ?? throw new InvalidOperationException("Schedule interval must be specified."); + TimeSpan interval = scheduleConfig.Interval; if (executionToken != this.State.ExecutionToken) { @@ -364,17 +364,12 @@ public void RunSchedule(TaskEntityContext context, string executionToken) static DateTimeOffset ComputeNextRunTime(ScheduleConfiguration scheduleConfig, DateTimeOffset lastRunAt) { - if (!scheduleConfig.Interval.HasValue) - { - throw new InvalidOperationException("Interval must be set to compute next run time."); - } - // Calculate number of intervals between last run and now TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - lastRunAt; - int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Value.Ticks); + int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Ticks); // Compute and return the next run time - return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Value.Ticks * (intervalsElapsed + 1)); + return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Ticks * (intervalsElapsed + 1)); } void StartOrchestrationIfNotRunning(TaskEntityContext context) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 6c2c721b..82c4a8f1 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -9,35 +9,46 @@ namespace Microsoft.DurableTask.ScheduledTasks; class ScheduleConfiguration { string orchestrationName; - TimeSpan? interval; + TimeSpan interval; /// /// Initializes a new instance of the class. /// + /// The ID of the schedule. /// The name of the orchestration to schedule. - /// The ID of the schedule, or null to generate one. - public ScheduleConfiguration(string orchestrationName, string scheduleId) + /// The interval between schedule executions. Must be positive and at least 1 second. +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public ScheduleConfiguration(string scheduleId, string orchestrationName, TimeSpan interval) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { - this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); - this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); + this.ScheduleId = Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + this.OrchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + if (interval <= TimeSpan.Zero) + { + throw new ArgumentException("Interval must be positive", nameof(interval)); + } + + if (interval.TotalSeconds < 1) + { + throw new ArgumentException("Interval must be at least 1 second", nameof(interval)); + } + + this.Interval = interval; } /// - /// Gets or sets the name of the orchestration function to schedule. + /// Gets or Sets the name of the orchestration function to schedule. /// public string OrchestrationName { get => this.orchestrationName; - set - { - this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } + set => this.orchestrationName = Check.NotNullOrEmpty(value, nameof(this.OrchestrationName)); } /// /// Gets the ID of the schedule. /// - public string ScheduleId { get; init; } + public string ScheduleId { get; } /// /// Gets or sets the input to the orchestration function. @@ -62,22 +73,17 @@ public string OrchestrationName /// /// Gets or sets the interval between schedule executions. /// - public TimeSpan? Interval + public TimeSpan Interval { get => this.interval; set { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) + if (value <= TimeSpan.Zero) { throw new ArgumentException("Interval must be positive", nameof(value)); } - if (value.Value.TotalSeconds < 1) + if (value.TotalSeconds < 1) { throw new ArgumentException("Interval must be at least 1 second", nameof(value)); } @@ -98,13 +104,14 @@ public TimeSpan? Interval /// A new schedule configuration. public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions createOptions) { - return new ScheduleConfiguration(createOptions.OrchestrationName, createOptions.ScheduleId) + Check.NotNull(createOptions, nameof(createOptions)); + + return new ScheduleConfiguration(createOptions.ScheduleId, createOptions.OrchestrationName, createOptions.Interval) { OrchestrationInput = createOptions.OrchestrationInput, OrchestrationInstanceId = createOptions.OrchestrationInstanceId, StartAt = createOptions.StartAt, EndAt = createOptions.EndAt, - Interval = createOptions.Interval, StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate, }; } @@ -151,7 +158,7 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) if (updateOptions.Interval.HasValue) { - this.Interval = updateOptions.Interval; + this.Interval = updateOptions.Interval.Value; updatedFields.Add(nameof(this.Interval)); } From aa2ee924ef4580ba8cde8bee7703f8dd1c35e342 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 10:12:15 -0800 Subject: [PATCH 110/203] save --- src/ScheduledTasks/Client/ScheduleHandle.cs | 1 + src/ScheduledTasks/Client/ScheduledTaskClient.cs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 890540a8..2bbb95d1 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -86,6 +86,7 @@ public async Task DescribeAsync(bool includeFullActivityLog } /// + /// TODO: Check not already exists once updating to poll free public async Task CreateAsync(ScheduleCreationOptions creationOptions) { this.logger.ClientCreatingSchedule(creationOptions); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 8aede822..5541e673 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -7,8 +7,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: validation - /// /// Client for managing scheduled tasks in a Durable Task application. /// From 80ddc60b2ed57925c31e8dc3c910838f76638b94 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:15:12 -0800 Subject: [PATCH 111/203] ischeduletaskclient interface --- .../Client/IScheduledTaskClient.cs | 16 ++++++-- src/ScheduledTasks/Client/ScheduleHandle.cs | 4 -- src/ScheduledTasks/Models/ScheduleQuery.cs | 40 +++++++++++++++++++ src/ScheduledTasks/ScheduledTasks.csproj | 1 - 4 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 src/ScheduledTasks/Models/ScheduleQuery.cs diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 7ca2e7be..2e2e6752 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -16,9 +16,17 @@ public interface IScheduledTaskClient IScheduleHandle GetScheduleHandle(string scheduleId); /// - /// Gets a list of all initialized schedules. + /// Gets a pageable list of schedules matching the specified filter criteria. /// - /// Whether to include full activity logs in the returned schedules. - /// A list of schedule descriptions. - Task> ListSchedulesAsync(bool includeFullActivityLogs); + /// Optional filter criteria for the schedules. If null, returns all schedules. + /// A pageable list of schedule descriptions. + Task> ListSchedulesAsync(ScheduleQuery? filter = null); + + /// + /// Creates a new schedule with the specified configuration. + /// + /// The options for creating the schedule. + /// Optional cancellation token. + /// A handle to the created schedule. + Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 2bbb95d1..631f9e27 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -58,10 +58,6 @@ public async Task DescribeAsync(bool includeFullActivityLog ScheduleState state = metadata.State; - // if (state.Status == ScheduleStatus.Uninitialized) - // { - // throw new ScheduleStillBeingProvisionedException(this.ScheduleId); - // } ScheduleConfiguration? config = state.ScheduleConfiguration; IReadOnlyCollection activityLogs = diff --git a/src/ScheduledTasks/Models/ScheduleQuery.cs b/src/ScheduledTasks/Models/ScheduleQuery.cs new file mode 100644 index 00000000..805fe48f --- /dev/null +++ b/src/ScheduledTasks/Models/ScheduleQuery.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents query parameters for filtering schedules. +/// +public record ScheduleQuery +{ + /// + /// Gets or sets a value indicating whether to include full activity logs in the returned schedules. + /// + public bool IncludeFullActivityLogs { get; init; } + + /// + /// Gets or sets a filter for the schedule status. + /// + public ScheduleStatus? Status { get; init; } + + /// + /// Gets or sets a prefix to filter schedule IDs. + /// + public string? ScheduleIdPrefix { get; init; } + + /// + /// Gets or sets a filter for schedules created after this time. + /// + public DateTimeOffset? CreatedAfter { get; init; } + + /// + /// Gets or sets a filter for schedules created before this time. + /// + public DateTimeOffset? CreatedBefore { get; init; } + + /// + /// Gets or sets the maximum number of schedules to return. + /// + public int? MaxItemCount { get; init; } +} \ No newline at end of file diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj index dd4008c0..a6f8f871 100644 --- a/src/ScheduledTasks/ScheduledTasks.csproj +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -8,7 +8,6 @@ - From 2ebb3ee0919ddea20bef79c4271af0293f8f1c27 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 12:18:31 -0800 Subject: [PATCH 112/203] save --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 9 +++++++-- src/ScheduledTasks/Logging/Client/Logs.cs | 3 --- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 5541e673..2cb94a00 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -26,15 +26,20 @@ public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) this.logger = Check.NotNull(logger, nameof(logger)); } + /// + public Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + /// public IScheduleHandle GetScheduleHandle(string scheduleId) { - this.logger.ClientGettingScheduleHandle(scheduleId); return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); } /// - public async Task> ListSchedulesAsync(bool includeFullActivityLogs = false) + public async Task> ListSchedulesAsync(ScheduleQuery? filter = null) { EntityQuery query = new EntityQuery { diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 55f66204..73569537 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -16,9 +16,6 @@ static partial class Logs [LoggerMessage(EventId = 80, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] public static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); - [LoggerMessage(EventId = 81, Level = LogLevel.Information, Message = "Getting schedule handle for schedule '{scheduleId}'")] - public static partial void ClientGettingScheduleHandle(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 82, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); From bd6a9a204e1b06968e3a2676d21709269b54f61a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:07:25 -0800 Subject: [PATCH 113/203] listschedule update --- .../Client/ScheduledTaskClient.cs | 100 ++++++++++++------ src/ScheduledTasks/Models/ScheduleQuery.cs | 33 ++++-- src/ScheduledTasks/Models/ScheduleState.cs | 10 ++ src/ScheduledTasks/Models/ScheduleStatus.cs | 2 + 4 files changed, 103 insertions(+), 42 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 2cb94a00..7734903d 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -33,50 +33,84 @@ public Task CreateScheduleAsync(ScheduleCreationOptions creation } /// - public IScheduleHandle GetScheduleHandle(string scheduleId) - { - return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); - } + public IScheduleHandle GetScheduleHandle(string scheduleId) => new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); /// - public async Task> ListSchedulesAsync(ScheduleQuery? filter = null) + public Task> ListSchedulesAsync(ScheduleQuery? filter = null) { + // TODO: map to entity query last modified from/to filters EntityQuery query = new EntityQuery { - InstanceIdStartsWith = nameof(Schedule), // Automatically ensures correct formatting - IncludeState = true, + InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), + IncludeState = filter?.ReturnIdsOnly ?? true, + PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, + ContinuationToken = filter?.ContinuationToken, }; - List schedules = new List(); - - await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + // Create an async pageable using the Pageable.Create helper + return Task.FromResult(Pageable.Create(async (continuationToken, pageSize, cancellation) => { - if (metadata.State.Status != ScheduleStatus.Uninitialized) + try { - ScheduleConfiguration config = metadata.State.ScheduleConfiguration!; - - IReadOnlyCollection activityLogs = - includeFullActivityLogs ? metadata.State.ActivityLogs : metadata.State.ActivityLogs.TakeLast(1).ToArray(); + List schedules = new List(); - schedules.Add(new ScheduleDescription + await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) { - ScheduleId = metadata.Id.Key, - OrchestrationName = config.OrchestrationName, - OrchestrationInput = config.OrchestrationInput, - OrchestrationInstanceId = config.OrchestrationInstanceId, - StartAt = config.StartAt, - EndAt = config.EndAt, - Interval = config.Interval, - StartImmediatelyIfLate = config.StartImmediatelyIfLate, - Status = metadata.State.Status, - ExecutionToken = metadata.State.ExecutionToken, - LastRunAt = metadata.State.LastRunAt, - NextRunAt = metadata.State.NextRunAt, - ActivityLogs = activityLogs, - }); - } - } + ScheduleState state = metadata.State; + if (state.Status == ScheduleStatus.Uninitialized) + { + continue; + } + + // Skip if status filter is specified and doesn't match + if (filter?.Status.HasValue == true && state.Status != filter.Status.Value) + { + continue; + } + + // Skip if created time filter is specified and doesn't match + if (filter?.CreatedFrom.HasValue == true && state.ScheduleCreatedAt <= filter.CreatedFrom) + { + continue; + } - return schedules; + if (filter?.CreatedTo.HasValue == true && state.ScheduleCreatedAt >= filter.CreatedTo) + { + continue; + } + + ScheduleConfiguration config = state.ScheduleConfiguration!; + + IReadOnlyCollection activityLogs = + filter?.IncludeFullActivityLogs == true ? + state.ActivityLogs : + state.ActivityLogs.TakeLast(1).ToArray(); + + schedules.Add(new ScheduleDescription + { + ScheduleId = metadata.Id.Key, + OrchestrationName = config.OrchestrationName, + OrchestrationInput = config.OrchestrationInput, + OrchestrationInstanceId = config.OrchestrationInstanceId, + StartAt = config.StartAt, + EndAt = config.EndAt, + Interval = config.Interval, + StartImmediatelyIfLate = config.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + ActivityLogs = activityLogs, + }); + } + + return new Page(schedules, continuationToken); + } + catch (OperationCanceledException e) + { + throw new OperationCanceledException( + $"The {nameof(this.ListSchedulesAsync)} operation was canceled.", e, e.CancellationToken); + } + })); } } diff --git a/src/ScheduledTasks/Models/ScheduleQuery.cs b/src/ScheduledTasks/Models/ScheduleQuery.cs index 805fe48f..e61b1c6f 100644 --- a/src/ScheduledTasks/Models/ScheduleQuery.cs +++ b/src/ScheduledTasks/Models/ScheduleQuery.cs @@ -9,32 +9,47 @@ namespace Microsoft.DurableTask.ScheduledTasks; public record ScheduleQuery { /// - /// Gets or sets a value indicating whether to include full activity logs in the returned schedules. + /// The default page size when not supplied. + /// + public const int DefaultPageSize = 100; + + /// + /// Gets a value indicating whether to include full activity logs in the returned schedules. /// public bool IncludeFullActivityLogs { get; init; } /// - /// Gets or sets a filter for the schedule status. + /// Gets the filter for the schedule status. /// public ScheduleStatus? Status { get; init; } /// - /// Gets or sets a prefix to filter schedule IDs. + /// Gets the prefix to filter schedule IDs. /// public string? ScheduleIdPrefix { get; init; } /// - /// Gets or sets a filter for schedules created after this time. + /// Gets the filter for schedules created after this time. + /// + public DateTimeOffset? CreatedFrom { get; init; } + + /// + /// Gets the filter for schedules created before this time. + /// + public DateTimeOffset? CreatedTo { get; init; } + + /// + /// Gets a value indicating whether to return only schedule IDs without additional details. /// - public DateTimeOffset? CreatedAfter { get; init; } + public bool ReturnIdsOnly { get; init; } /// - /// Gets or sets a filter for schedules created before this time. + /// Gets the maximum number of schedules to return per page. /// - public DateTimeOffset? CreatedBefore { get; init; } + public int? PageSize { get; init; } /// - /// Gets or sets the maximum number of schedules to return. + /// Gets the continuation token for retrieving the next page of results. /// - public int? MaxItemCount { get; init; } + public string? ContinuationToken { get; init; } } \ No newline at end of file diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index dda2be21..6c6c005b 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -35,6 +35,16 @@ class ScheduleState /// public DateTimeOffset? NextRunAt { get; set; } + /// + /// Gets or sets the time when this schedule was created. + /// + public DateTimeOffset? ScheduleCreatedAt { get; set; } + + /// + /// Gets or sets the time when this schedule was last modified. + /// + public DateTimeOffset? ScheduleLastModifiedAt { get; set; } + /// /// Gets or sets the schedule configuration. /// diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index 94e0c191..ac1f3a8e 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -3,6 +3,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; +// TODO: Find whether it is possible to remove uninitialized, have to ensure atomicity of creation if possible + /// /// Represents the current status of a schedule. /// From d20923b39700442d394853a29f3e02d3c35d5e90 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:19:25 -0800 Subject: [PATCH 114/203] remove create --- src/ScheduledTasks/Client/IScheduleHandle.cs | 7 ------- src/ScheduledTasks/Client/ScheduleHandle.cs | 14 -------------- 2 files changed, 21 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index e8a1fd64..60e5b2fe 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -13,13 +13,6 @@ public interface IScheduleHandle /// string ScheduleId { get; } - /// - /// Creates this schedule with the specified configuration. - /// - /// The options for creating the schedule. - /// A task that completes when the schedule is created. - Task CreateAsync(ScheduleCreationOptions creationOptions); - /// /// Retrieves the current details of this schedule. /// diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 631f9e27..923f5bce 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -81,20 +81,6 @@ public async Task DescribeAsync(bool includeFullActivityLog }; } - /// - /// TODO: Check not already exists once updating to poll free - public async Task CreateAsync(ScheduleCreationOptions creationOptions) - { - this.logger.ClientCreatingSchedule(creationOptions); - Check.NotNull(creationOptions, nameof(creationOptions)); - - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), creationOptions.ScheduleId); - - await this.durableTaskClient.Entities.SignalEntityAsync(entityId, nameof(Schedule.CreateSchedule), creationOptions); - - return new ScheduleWaiter(this, nameof(Schedule.CreateSchedule)); - } - /// public async Task PauseAsync() { From 45c12583f21f636f8366ce063c3885fd310185c6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:39:09 -0800 Subject: [PATCH 115/203] createschedule --- .../Client/ScheduledTaskClient.cs | 26 +++++++++++++++-- .../ExecuteScheduleOperationOrchestrator.cs | 29 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 7734903d..064d7907 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -3,6 +3,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -27,9 +28,30 @@ public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) } /// - public Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) { - throw new NotImplementedException(); + Check.NotNull(creationOptions, nameof(creationOptions)); + + string scheduleId = creationOptions.ScheduleId; + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + + try + { + // Call the orchestrator to create the schedule + ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); + await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Return a handle to the schedule + return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + } + catch (Exception ex) + { + this.logger.ClientError($"Failed to create schedule: {ex.Message}", scheduleId); + throw; + } } /// diff --git a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs new file mode 100644 index 00000000..fed585d9 --- /dev/null +++ b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs @@ -0,0 +1,29 @@ +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; + +[DurableTask("ExecuteScheduleOperation")] +public class ExecuteScheduleOperationOrchestrator : TaskOrchestrator +{ + public override async Task RunTask(TaskOrchestrationContext context, ScheduleOperationRequest input) + { + var logger = context.CreateReplaySafeLogger(); + logger.LogInformation("Starting schedule operation {Operation} for entity {EntityId}", input.OperationName, input.EntityId); + + try + { + var result = await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); + return result; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to execute schedule operation {Operation} for entity {EntityId}", input.OperationName, input.EntityId); + throw; + } + } +} + +public record ScheduleOperationRequest(EntityInstanceId EntityId, string OperationName, object? Input = null); \ No newline at end of file From 2603859ab442400eb9da27cd0e861807886d6ec6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 19:59:46 -0800 Subject: [PATCH 116/203] save --- .../Exception/ScheduleCreationException.cs | 27 +++++++++++++ .../ExecuteScheduleOperationOrchestrator.cs | 40 ++++++++++--------- 2 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 src/ScheduledTasks/Exception/ScheduleCreationException.cs diff --git a/src/ScheduledTasks/Exception/ScheduleCreationException.cs b/src/ScheduledTasks/Exception/ScheduleCreationException.cs new file mode 100644 index 00000000..8e814f23 --- /dev/null +++ b/src/ScheduledTasks/Exception/ScheduleCreationException.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Exception thrown when schedule creation fails. +/// +public class ScheduleCreationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed to create. + /// The error message. + /// The inner exception. + public ScheduleCreationException(string scheduleId, string message, Exception? innerException = null) + : base($"Failed to create schedule '{scheduleId}': {message}", innerException) + { + this.ScheduleId = scheduleId; + } + + /// + /// Gets the ID of the schedule that failed to create. + /// + public string ScheduleId { get; } +} \ No newline at end of file diff --git a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs index fed585d9..40fc93ef 100644 --- a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs +++ b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs @@ -1,29 +1,31 @@ -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client.Entities; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using Microsoft.DurableTask.Entities; -using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; +// TODO: logging +// TODO: May need separate orchs, result is obj now + +/// +/// Orchestrator that executes operations on schedule entities. +/// Calls the specified operation on the target entity and returns the result. +/// [DurableTask("ExecuteScheduleOperation")] -public class ExecuteScheduleOperationOrchestrator : TaskOrchestrator +public class ExecuteScheduleOperationOrchestrator : TaskOrchestrator { - public override async Task RunTask(TaskOrchestrationContext context, ScheduleOperationRequest input) + /// + public override async Task RunAsync(TaskOrchestrationContext context, ScheduleOperationRequest input) { - var logger = context.CreateReplaySafeLogger(); - logger.LogInformation("Starting schedule operation {Operation} for entity {EntityId}", input.OperationName, input.EntityId); - - try - { - var result = await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); - return result; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to execute schedule operation {Operation} for entity {EntityId}", input.OperationName, input.EntityId); - throw; - } + return await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); } } -public record ScheduleOperationRequest(EntityInstanceId EntityId, string OperationName, object? Input = null); \ No newline at end of file +/// +/// Request for executing a schedule operation. +/// +/// The ID of the entity to execute the operation on. +/// The name of the operation to execute. +/// Optional input for the operation. +public record ScheduleOperationRequest(EntityInstanceId EntityId, string OperationName, object? Input = null); From f20dc86bfd655a9ff47409846b894a09829f1f92 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:02:43 -0800 Subject: [PATCH 117/203] createsche --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 064d7907..2f27bd08 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -39,11 +39,19 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr { // Call the orchestrator to create the schedule ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); - await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), request, cancellation); + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, false, cancellation); + + if (state.RuntimeStatus == OrchestrationRuntimeStatus.Failed && state.FailureDetails != null) + { + throw new ScheduleCreationException(scheduleId, state.FailureDetails.ErrorMessage); + } + // Return a handle to the schedule return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); } From 330a935cf15d587d9e7d9149a7e50588e31d6933 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:34:17 -0800 Subject: [PATCH 118/203] create step 2 --- .../Client/IScheduledTaskClient.cs | 9 +++ .../Client/ScheduledTaskClient.cs | 72 ++++++++++++++----- 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 2e2e6752..a17e4e71 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -15,6 +15,15 @@ public interface IScheduledTaskClient /// A handle to manage the schedule. IScheduleHandle GetScheduleHandle(string scheduleId); + /// + /// Gets a schedule description by its ID. + /// + /// The ID of the schedule to retrieve. + /// Whether to include full activity logs in the returned schedule details. + /// Optional cancellation token. + /// The schedule description if found, null otherwise. + Task GetScheduleAsync(string scheduleId, bool includeFullActivityLogs = false, CancellationToken cancellation = default); + /// /// Gets a pageable list of schedules matching the specified filter criteria. /// diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 2f27bd08..a9a4b494 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -35,31 +35,67 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr string scheduleId = creationOptions.ScheduleId; EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - try + // Check if schedule already exists + ScheduleDescription? existingSchedule = await this.GetScheduleAsync(scheduleId, cancellation: cancellation); + if (existingSchedule != null) { - // Call the orchestrator to create the schedule - ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); + throw new ScheduleAlreadyExistException(scheduleId); + } - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, false, cancellation); + // Call the orchestrator to create the schedule + ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); - if (state.RuntimeStatus == OrchestrationRuntimeStatus.Failed && state.FailureDetails != null) - { - throw new ScheduleCreationException(scheduleId, state.FailureDetails.ErrorMessage); - } + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, false, cancellation); - // Return a handle to the schedule - return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new ScheduleCreationException(scheduleId, state.FailureDetails?.ErrorMessage ?? string.Empty); } - catch (Exception ex) + + // Return a handle to the schedule + return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + } + + /// + public async Task GetScheduleAsync(string scheduleId, bool includeFullActivityLogs = false, CancellationToken cancellation = default) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); + + if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) { - this.logger.ClientError($"Failed to create schedule: {ex.Message}", scheduleId); - throw; + return null; } + + ScheduleState state = metadata.State; + ScheduleConfiguration? config = state.ScheduleConfiguration; + + IReadOnlyCollection activityLogs = + includeFullActivityLogs ? state.ActivityLogs : state.ActivityLogs.TakeLast(1).ToArray(); + + return new ScheduleDescription + { + ScheduleId = scheduleId, + OrchestrationName = config?.OrchestrationName, + OrchestrationInput = config?.OrchestrationInput, + OrchestrationInstanceId = config?.OrchestrationInstanceId, + StartAt = config?.StartAt, + EndAt = config?.EndAt, + Interval = config?.Interval, + StartImmediatelyIfLate = config?.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + ActivityLogs = activityLogs, + }; } /// From 810bb80cdee9433aee4038bb455b71f4d8cd5d1a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:40:18 -0800 Subject: [PATCH 119/203] create step 3 --- .../Client/ScheduledTaskClient.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index a9a4b494..53ff1ce4 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -36,8 +36,8 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); // Check if schedule already exists - ScheduleDescription? existingSchedule = await this.GetScheduleAsync(scheduleId, cancellation: cancellation); - if (existingSchedule != null) + bool scheduleExists = await this.CheckScheduleExists(scheduleId, cancellation); + if (scheduleExists) { throw new ScheduleAlreadyExistException(scheduleId); } @@ -179,4 +179,20 @@ public Task> ListSchedulesAsync(ScheduleQuery } })); } + + /// + /// Checks if a schedule with the specified ID exists. + /// + /// The ID of the schedule to check. + /// Optional cancellation token. + /// True if the schedule exists, false otherwise. + async Task CheckScheduleExists(string scheduleId, CancellationToken cancellation = default) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, false, cancellation); + + return metadata != null; + } } From 7e53bee5411e54de3bae7cdad3b000efb6db001a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:50:30 -0800 Subject: [PATCH 120/203] list --- .../Client/IScheduledTaskClient.cs | 8 +++++ .../Client/ScheduledTaskClient.cs | 36 ++++++++++++++++++- src/ScheduledTasks/Models/ScheduleQuery.cs | 7 +--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index a17e4e71..efe958cc 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -31,6 +31,14 @@ public interface IScheduledTaskClient /// A pageable list of schedule descriptions. Task> ListSchedulesAsync(ScheduleQuery? filter = null); + /// + /// Gets a pageable list of schedule IDs matching the specified filter criteria. + /// This is a more efficient version of ListSchedulesAsync when only the IDs are needed. + /// + /// Optional filter criteria for the schedules. If null, returns all schedule IDs. + /// A pageable list of schedule IDs. + Task> ListScheduleIdsAsync(ScheduleQuery? filter = null); + /// /// Creates a new schedule with the specified configuration. /// diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 53ff1ce4..86ab3aa2 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -108,7 +108,7 @@ public Task> ListSchedulesAsync(ScheduleQuery EntityQuery query = new EntityQuery { InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), - IncludeState = filter?.ReturnIdsOnly ?? true, + IncludeState = true, PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, ContinuationToken = filter?.ContinuationToken, }; @@ -180,6 +180,40 @@ public Task> ListSchedulesAsync(ScheduleQuery })); } + /// + public Task> ListScheduleIdsAsync(ScheduleQuery? filter = null) + { + EntityQuery query = new EntityQuery + { + InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), + IncludeState = false, // We don't need the state since we only want IDs + PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, + ContinuationToken = filter?.ContinuationToken, + }; + + // Create an async pageable using the Pageable.Create helper + return Task.FromResult(Pageable.Create(async (continuationToken, pageSize, cancellation) => + { + try + { + List scheduleIds = new List(); + + await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + { + // Extract just the schedule ID from the entity ID + scheduleIds.Add(metadata.Id.Key); + } + + return new Page(scheduleIds, continuationToken); + } + catch (OperationCanceledException e) + { + throw new OperationCanceledException( + $"The {nameof(this.ListScheduleIdsAsync)} operation was canceled.", e, e.CancellationToken); + } + })); + } + /// /// Checks if a schedule with the specified ID exists. /// diff --git a/src/ScheduledTasks/Models/ScheduleQuery.cs b/src/ScheduledTasks/Models/ScheduleQuery.cs index e61b1c6f..6ffb91a7 100644 --- a/src/ScheduledTasks/Models/ScheduleQuery.cs +++ b/src/ScheduledTasks/Models/ScheduleQuery.cs @@ -38,11 +38,6 @@ public record ScheduleQuery /// public DateTimeOffset? CreatedTo { get; init; } - /// - /// Gets a value indicating whether to return only schedule IDs without additional details. - /// - public bool ReturnIdsOnly { get; init; } - /// /// Gets the maximum number of schedules to return per page. /// @@ -52,4 +47,4 @@ public record ScheduleQuery /// Gets the continuation token for retrieving the next page of results. /// public string? ContinuationToken { get; init; } -} \ No newline at end of file +} From 3a814809381b5c3f4a497988e6f9a108ffd88222 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:52:52 -0800 Subject: [PATCH 121/203] save --- src/ScheduledTasks/Client/IScheduledTaskClient.cs | 2 +- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index efe958cc..04dbf576 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -37,7 +37,7 @@ public interface IScheduledTaskClient /// /// Optional filter criteria for the schedules. If null, returns all schedule IDs. /// A pageable list of schedule IDs. - Task> ListScheduleIdsAsync(ScheduleQuery? filter = null); + Task> ListScheduleAsync(ScheduleQuery? filter = null); /// /// Creates a new schedule with the specified configuration. diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 86ab3aa2..b91dc9eb 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -181,7 +181,7 @@ public Task> ListSchedulesAsync(ScheduleQuery } /// - public Task> ListScheduleIdsAsync(ScheduleQuery? filter = null) + public Task> ListScheduleAsync(ScheduleQuery? filter = null) { EntityQuery query = new EntityQuery { @@ -209,7 +209,7 @@ public Task> ListScheduleIdsAsync(ScheduleQuery? filter = catch (OperationCanceledException e) { throw new OperationCanceledException( - $"The {nameof(this.ListScheduleIdsAsync)} operation was canceled.", e, e.CancellationToken); + $"The {nameof(this.ListScheduleAsync)} operation was canceled.", e, e.CancellationToken); } })); } From bcadfc2947f9070cd47dde1157d48339dff2a1fc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:43:27 -0800 Subject: [PATCH 122/203] poll free part 1 --- samples/ScheduleDemo/Program.cs | 60 ++++--- samples/ScheduleDemo/appsettings.json | 2 +- src/ScheduledTasks/Client/IScheduleHandle.cs | 32 ++-- src/ScheduledTasks/Client/IScheduleWaiter.cs | 40 ----- .../Client/IScheduledTaskClient.cs | 5 +- src/ScheduledTasks/Client/ScheduleHandle.cs | 29 ++-- src/ScheduledTasks/Client/ScheduleWaiter.cs | 156 ------------------ .../Client/ScheduledTaskClient.cs | 22 +-- src/ScheduledTasks/Client/WaitOptions.cs | 42 ----- src/ScheduledTasks/Entity/Schedule.cs | 122 +------------- .../ScheduleOperationFailedException.cs | 12 -- .../DurableTaskSchedulerWorkerExtensions.cs | 6 +- .../Models/ScheduleActivityLog.cs | 56 ------- .../Models/ScheduleDescription.cs | 5 - src/ScheduledTasks/Models/ScheduleQuery.cs | 5 - src/ScheduledTasks/Models/ScheduleState.cs | 32 ---- src/ScheduledTasks/Models/ScheduleStatus.cs | 2 - .../ExecuteScheduleOperationOrchestrator.cs | 5 +- 18 files changed, 83 insertions(+), 550 deletions(-) delete mode 100644 src/ScheduledTasks/Client/IScheduleWaiter.cs delete mode 100644 src/ScheduledTasks/Client/ScheduleWaiter.cs delete mode 100644 src/ScheduledTasks/Client/WaitOptions.cs delete mode 100644 src/ScheduledTasks/Models/ScheduleActivityLog.cs diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index e2e8517d..d93314c5 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.AzureManaged; using Microsoft.DurableTask.ScheduledTasks; @@ -63,14 +64,38 @@ try { // list all schedules - var schedules = await scheduledTaskClient.ListSchedulesAsync(false); - foreach (var schedule in schedules) + // Define the initial query with the desired page size + ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; + + // Retrieve the pageable collection of schedule IDs + AsyncPageable schedules = await scheduledTaskClient.ListScheduleIdsAsync(query); + + // Initialize the continuation token + string? continuationToken = null; + await foreach (Page page in schedules.AsPages(continuationToken)) { - var handle = scheduledTaskClient.GetScheduleHandle(schedule.ScheduleId); - await handle.DeleteAsync(); - Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); + foreach (string scheduleId in page.Values) + { + // Obtain the schedule handle for the current scheduleId + IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); + + // Delete the schedule + await handle.DeleteAsync(); + + Console.WriteLine($"Deleted schedule {scheduleId}"); + } + + // Update the continuation token for the next iteration + continuationToken = page.ContinuationToken; + + // If there's no continuation token, we've reached the end of the collection + if (continuationToken == null) + { + break; + } } + // Create schedule options that runs every 4 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) { @@ -78,13 +103,12 @@ OrchestrationInput = "MSFT" }; - // Get schedule handle - IScheduleHandle scheduleHandle = scheduledTaskClient.GetScheduleHandle(scheduleOptions.ScheduleId); + // Create the schedule and get a handle to it + ScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + + // Get the schedule description + ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); - // Create the schedule - Console.WriteLine("Creating schedule..."); - IScheduleWaiter waiter = await scheduleHandle.CreateAsync(scheduleOptions); - ScheduleDescription scheduleDescription = await waiter.WaitUntilActiveAsync(); // print the schedule description Console.WriteLine(scheduleDescription.ToJsonString(true)); @@ -94,8 +118,8 @@ // Pause the schedule Console.WriteLine("\nPausing schedule..."); - IScheduleWaiter pauseWaiter = await scheduleHandle.PauseAsync(); - scheduleDescription = await pauseWaiter.WaitUntilPausedAsync(); + await scheduleHandle.PauseAsync(); + scheduleDescription = await scheduleHandle.DescribeAsync(); Console.WriteLine(scheduleDescription.ToJsonString(true)); Console.WriteLine(""); Console.WriteLine(""); @@ -104,9 +128,8 @@ // Resume the schedule Console.WriteLine("\nResuming schedule..."); - IScheduleWaiter resumeWaiter = await scheduleHandle.ResumeAsync(); - - scheduleDescription = await resumeWaiter.WaitUntilActiveAsync(); + await scheduleHandle.ResumeAsync(); + scheduleDescription = await scheduleHandle.DescribeAsync(); Console.WriteLine(scheduleDescription.ToJsonString(true)); Console.WriteLine(""); @@ -116,11 +139,6 @@ await Task.Delay(TimeSpan.FromMinutes(30)); //Console.WriteLine("\nPress any key to delete the schedule and exit..."); //Console.ReadKey(); - - // Delete the schedule - IScheduleWaiter deleteWaiter = await scheduleHandle.DeleteAsync(); - bool deleted = await deleteWaiter.WaitUntilDeletedAsync(); - Console.WriteLine(deleted ? "Schedule deleted." : "Schedule not deleted."); } catch (Exception ex) { diff --git a/samples/ScheduleDemo/appsettings.json b/samples/ScheduleDemo/appsettings.json index b7ce5f11..fa3d5ee7 100644 --- a/samples/ScheduleDemo/appsettings.json +++ b/samples/ScheduleDemo/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Warning", + "Default": "Debug", "Microsoft": "Warning", "ScheduleDemo": "Debug", "DemoOrchestration": "Debug" diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs index 60e5b2fe..9bfc4092 100644 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ b/src/ScheduledTasks/Client/IScheduleHandle.cs @@ -16,32 +16,36 @@ public interface IScheduleHandle /// /// Retrieves the current details of this schedule. /// - /// Whether to include full activity logs in the returned schedule details. + /// A cancellation token that can be used to cancel the operation. /// The schedule details. - Task DescribeAsync(bool includeFullActivityLogs = false); + Task DescribeAsync(CancellationToken cancellation = default); /// - /// Deletes this schedule. + /// Deletes this schedule. The schedule will stop executing and be removed from the system. /// - /// A task that completes when the schedule is deleted. - Task DeleteAsync(); + /// A cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been deleted. + Task DeleteAsync(CancellationToken cancellation = default); /// - /// Pauses this schedule. + /// Pauses this schedule. The schedule will stop executing but remain in the system. /// - /// A task that completes when the schedule is paused. - Task PauseAsync(); + /// A cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been paused. + Task PauseAsync(CancellationToken cancellation = default); /// - /// Resumes this schedule. + /// Resumes this schedule. The schedule will continue executing from where it was paused. /// - /// A task that completes when the schedule is resumed. - Task ResumeAsync(); + /// A cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been resumed. + Task ResumeAsync(CancellationToken cancellation = default); /// - /// Updates this schedule with new configuration. + /// Updates this schedule with new configuration. The schedule will continue executing with the new configuration. /// /// The options for updating the schedule configuration. - /// A task that completes when the schedule is updated. - Task UpdateAsync(ScheduleUpdateOptions updateOptions); + /// A cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been updated. + Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/IScheduleWaiter.cs b/src/ScheduledTasks/Client/IScheduleWaiter.cs deleted file mode 100644 index cb51c91e..00000000 --- a/src/ScheduledTasks/Client/IScheduleWaiter.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Provides waiter functionality for schedule state transitions. -/// -public interface IScheduleWaiter -{ - /// - /// Waits until the schedule is paused. - /// - /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. - /// Optional cancellation token. - /// The schedule description once paused. - Task WaitUntilPausedAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default); - - /// - /// Waits until the schedule is active. - /// - /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. - /// Optional cancellation token. - /// The schedule description once active. - Task WaitUntilActiveAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default); - - /// - /// Waits until the schedule is deleted. - /// - /// Optional wait options to configure timeout, polling intervals and backoff strategy. If not provided, default polling mechanism will be used. - /// Optional cancellation token. - /// True if the schedule was deleted, false otherwise. - Task WaitUntilDeletedAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default); -} diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 04dbf576..761e5745 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -19,10 +19,9 @@ public interface IScheduledTaskClient /// Gets a schedule description by its ID. /// /// The ID of the schedule to retrieve. - /// Whether to include full activity logs in the returned schedule details. /// Optional cancellation token. /// The schedule description if found, null otherwise. - Task GetScheduleAsync(string scheduleId, bool includeFullActivityLogs = false, CancellationToken cancellation = default); + Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default); /// /// Gets a pageable list of schedules matching the specified filter criteria. @@ -37,7 +36,7 @@ public interface IScheduledTaskClient /// /// Optional filter criteria for the schedules. If null, returns all schedule IDs. /// A pageable list of schedule IDs. - Task> ListScheduleAsync(ScheduleQuery? filter = null); + Task> ListScheduleIdsAsync(ScheduleQuery? filter = null); /// /// Creates a new schedule with the specified configuration. diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 923f5bce..fbe7a53b 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -44,13 +44,13 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge public EntityInstanceId EntityId { get; } /// - public async Task DescribeAsync(bool includeFullActivityLogs = false) + public async Task DescribeAsync(CancellationToken cancellation = default) { Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); EntityMetadata? metadata = - await this.durableTaskClient.Entities.GetEntityAsync(entityId); + await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation: cancellation); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); @@ -60,9 +60,6 @@ public async Task DescribeAsync(bool includeFullActivityLog ScheduleConfiguration? config = state.ScheduleConfiguration; - IReadOnlyCollection activityLogs = - includeFullActivityLogs ? state.ActivityLogs : state.ActivityLogs.TakeLast(1).ToArray(); - return new ScheduleDescription { ScheduleId = this.ScheduleId, @@ -77,44 +74,38 @@ public async Task DescribeAsync(bool includeFullActivityLog ExecutionToken = state.ExecutionToken, LastRunAt = state.LastRunAt, NextRunAt = state.NextRunAt, - ActivityLogs = activityLogs, }; } /// - public async Task PauseAsync() + public async Task PauseAsync(CancellationToken cancellation = default) { this.logger.ClientPausingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.PauseSchedule)); - return new ScheduleWaiter(this, nameof(Schedule.PauseSchedule)); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.PauseSchedule), cancellation: cancellation); } /// - public async Task ResumeAsync() + public async Task ResumeAsync(CancellationToken cancellation = default) { this.logger.ClientResumingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.ResumeSchedule)); - - return new ScheduleWaiter(this, nameof(Schedule.ResumeSchedule)); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.ResumeSchedule), cancellation: cancellation); } /// - public async Task UpdateAsync(ScheduleUpdateOptions updateOptions) + public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default) { this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); - return new ScheduleWaiter(this, nameof(Schedule.UpdateSchedule)); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions, cancellation: cancellation); } /// - public async Task DeleteAsync() + public async Task DeleteAsync(CancellationToken cancellation = default) { this.logger.ClientDeletingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete"); - return new ScheduleWaiter(this, "delete"); + await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete", cancellation: cancellation); } } diff --git a/src/ScheduledTasks/Client/ScheduleWaiter.cs b/src/ScheduledTasks/Client/ScheduleWaiter.cs deleted file mode 100644 index f3ccc5bb..00000000 --- a/src/ScheduledTasks/Client/ScheduleWaiter.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Provides waiter functionality for schedule state transitions. -/// -public class ScheduleWaiter : IScheduleWaiter -{ - readonly IScheduleHandle scheduleHandle; - readonly TimeSpan defaultPollingInterval = TimeSpan.FromSeconds(5); - readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(2); - readonly TimeSpan defaultMaxPollingInterval = TimeSpan.FromSeconds(20); - - readonly string operationName; - - /// - /// Initializes a new instance of the class. - /// - /// The schedule handle to wait on. - /// operation name. - public ScheduleWaiter(IScheduleHandle scheduleHandle, string operationName) - { - this.scheduleHandle = scheduleHandle ?? throw new ArgumentNullException(nameof(scheduleHandle)); - this.operationName = operationName ?? throw new ArgumentNullException(nameof(operationName)); - } - - /// - public Task WaitUntilPausedAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.WaitForStatusAsync(ScheduleStatus.Paused, options, cancellationToken); - } - - /// - public Task WaitUntilActiveAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default) - { - return this.WaitForStatusAsync(ScheduleStatus.Active, options, cancellationToken); - } - - /// - public async Task WaitUntilDeletedAsync( - WaitOptions? options = null, - CancellationToken cancellationToken = default) - { - TimeSpan timeout = options?.Timeout ?? this.defaultTimeout; - TimeSpan pollingInterval = options?.PollingInterval ?? this.defaultPollingInterval; - TimeSpan maxPollingInterval = options?.MaxPollingInterval ?? this.defaultMaxPollingInterval; - bool useExponentialBackoff = options?.UseExponentialBackoff ?? false; - double backoffMultiplier = options?.BackoffMultiplier ?? 2.0; - - using CancellationTokenSource timeoutCts = new CancellationTokenSource(timeout); - using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); - - TimeSpan currentPollingInterval = pollingInterval; - - try - { - while (!linkedCts.Token.IsCancellationRequested) - { - try - { - await this.scheduleHandle.DescribeAsync(); - - // Calculate next polling interval with exponential backoff if enabled - if (useExponentialBackoff) - { - currentPollingInterval = TimeSpan.FromTicks(Math.Min( - currentPollingInterval.Ticks * (long)backoffMultiplier, - maxPollingInterval.Ticks)); - } - - await Task.Delay(currentPollingInterval, linkedCts.Token); - } - catch (ScheduleNotFoundException) - { - return true; - } - } - - linkedCts.Token.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to be deleted"); - } - catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) - { - throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to be deleted"); - } - } - - async Task WaitForStatusAsync( - ScheduleStatus desiredStatus, - WaitOptions? options = null, - CancellationToken cancellationToken = default) - { - TimeSpan timeout = options?.Timeout ?? this.defaultTimeout; - TimeSpan pollingInterval = options?.PollingInterval ?? this.defaultPollingInterval; - TimeSpan maxPollingInterval = options?.MaxPollingInterval ?? this.defaultMaxPollingInterval; - bool useExponentialBackoff = options?.UseExponentialBackoff ?? false; - double backoffMultiplier = options?.BackoffMultiplier ?? 2.0; - - using CancellationTokenSource timeoutCts = new CancellationTokenSource(timeout); - using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); - - TimeSpan currentPollingInterval = pollingInterval; - - try - { - while (!linkedCts.Token.IsCancellationRequested) - { - try - { - ScheduleDescription description = await this.scheduleHandle.DescribeAsync(); - if (description.Status == desiredStatus) - { - return description; - } - - // check latest operation log status - ScheduleActivityLog? latestActivityLog = description.ActivityLogs.LastOrDefault(); - if (latestActivityLog != null && latestActivityLog.Status == ScheduleOperationStatus.Failed.ToString()) - { - throw new ScheduleOperationFailedException(description.ScheduleId, latestActivityLog.Operation, latestActivityLog.Status, latestActivityLog.FailureDetails ?? null); - } - } - catch (Exception ex) when (ex is ScheduleNotFoundException) - { - if (this.operationName == nameof(Schedule.CreateSchedule) && desiredStatus != ScheduleStatus.Active) - { - throw; - } - } - - // Calculate next polling interval with exponential backoff if enabled - if (useExponentialBackoff) - { - currentPollingInterval = TimeSpan.FromTicks(Math.Min( - currentPollingInterval.Ticks * (long)backoffMultiplier, - maxPollingInterval.Ticks)); - } - - await Task.Delay(currentPollingInterval, linkedCts.Token); - } - - linkedCts.Token.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to reach status {desiredStatus}"); - } - catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) - { - throw new TimeoutException($"Timed out waiting for schedule {this.scheduleHandle.ScheduleId} to reach status {desiredStatus}"); - } - } -} diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index b91dc9eb..f1d35e2a 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -50,7 +50,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr cancellation); // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, false, cancellation); + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) { @@ -62,7 +62,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr } /// - public async Task GetScheduleAsync(string scheduleId, bool includeFullActivityLogs = false, CancellationToken cancellation = default) + public async Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); @@ -77,9 +77,6 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr ScheduleState state = metadata.State; ScheduleConfiguration? config = state.ScheduleConfiguration; - IReadOnlyCollection activityLogs = - includeFullActivityLogs ? state.ActivityLogs : state.ActivityLogs.TakeLast(1).ToArray(); - return new ScheduleDescription { ScheduleId = scheduleId, @@ -94,7 +91,6 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr ExecutionToken = state.ExecutionToken, LastRunAt = state.LastRunAt, NextRunAt = state.NextRunAt, - ActivityLogs = activityLogs, }; } @@ -123,10 +119,6 @@ public Task> ListSchedulesAsync(ScheduleQuery await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) { ScheduleState state = metadata.State; - if (state.Status == ScheduleStatus.Uninitialized) - { - continue; - } // Skip if status filter is specified and doesn't match if (filter?.Status.HasValue == true && state.Status != filter.Status.Value) @@ -147,11 +139,6 @@ public Task> ListSchedulesAsync(ScheduleQuery ScheduleConfiguration config = state.ScheduleConfiguration!; - IReadOnlyCollection activityLogs = - filter?.IncludeFullActivityLogs == true ? - state.ActivityLogs : - state.ActivityLogs.TakeLast(1).ToArray(); - schedules.Add(new ScheduleDescription { ScheduleId = metadata.Id.Key, @@ -166,7 +153,6 @@ public Task> ListSchedulesAsync(ScheduleQuery ExecutionToken = state.ExecutionToken, LastRunAt = state.LastRunAt, NextRunAt = state.NextRunAt, - ActivityLogs = activityLogs, }); } @@ -181,7 +167,7 @@ public Task> ListSchedulesAsync(ScheduleQuery } /// - public Task> ListScheduleAsync(ScheduleQuery? filter = null) + public Task> ListScheduleIdsAsync(ScheduleQuery? filter = null) { EntityQuery query = new EntityQuery { @@ -209,7 +195,7 @@ public Task> ListScheduleAsync(ScheduleQuery? filter = nul catch (OperationCanceledException e) { throw new OperationCanceledException( - $"The {nameof(this.ListScheduleAsync)} operation was canceled.", e, e.CancellationToken); + $"The {nameof(this.ListScheduleIdsAsync)} operation was canceled.", e, e.CancellationToken); } })); } diff --git a/src/ScheduledTasks/Client/WaitOptions.cs b/src/ScheduledTasks/Client/WaitOptions.cs deleted file mode 100644 index c3d40594..00000000 --- a/src/ScheduledTasks/Client/WaitOptions.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Options for configuring wait behavior when waiting for schedule state transitions. -/// -public record WaitOptions -{ - /// - /// Gets the timeout duration for the wait operation. - /// If not specified, defaults to 5 minutes. - /// - public TimeSpan? Timeout { get; init; } - - /// - /// Gets the initial polling interval between status checks. - /// If not specified, defaults to 5 seconds. - /// - public TimeSpan? PollingInterval { get; init; } - - /// - /// Gets a value indicating whether to use exponential backoff for polling intervals. - /// When enabled, the polling interval will increase exponentially between retries. - /// - public bool UseExponentialBackoff { get; init; } - - /// - /// Gets the maximum polling interval when using exponential backoff. - /// Only applicable when UseExponentialBackoff is true. - /// If not specified, defaults to 30 seconds. - /// - public TimeSpan? MaxPollingInterval { get; init; } - - /// - /// Gets the exponential backoff multiplier. - /// Only applicable when UseExponentialBackoff is true. - /// If not specified, defaults to 2.0. - /// - public double BackoffMultiplier { get; init; } = 2.0; -} diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index f96ec671..82b10e1b 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -46,44 +46,15 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Succeeded.ToString()); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) - { - this.logger.ScheduleOperationError(scheduleCreationOptions?.ScheduleId ?? string.Empty, nameof(this.CreateSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); - this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleInvalidTransitionEx.Message, - Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure the schedule is not already created.", - }); - } - catch (ScheduleClientValidationException scheduleClientValidationEx) - { - this.logger.ScheduleOperationError(scheduleCreationOptions?.ScheduleId ?? string.Empty, nameof(this.CreateSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); - this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleClientValidationEx.Message, - Type = ScheduleOperationFailureType.ValidationError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure request is valid.", - }); - } catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); - this.State.AddActivityLog(nameof(this.CreateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = "Failed to create schedule", - Type = ScheduleOperationFailureType.InternalError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Please contact support.", - }); + throw; } } @@ -141,41 +112,10 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately // or later to separate response from schedule creation and schedule responsibilities context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); - - this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Succeeded.ToString()); - } - catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); - this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleInvalidTransitionEx.Message, - Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure the schedule is in a valid state for update.", - }); - } - catch (ScheduleClientValidationException scheduleClientValidationEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); - this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleClientValidationEx.Message, - Type = ScheduleOperationFailureType.ValidationError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure update request is valid.", - }); } catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); - this.State.AddActivityLog(nameof(this.UpdateSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = "Failed to update schedule", - Type = ScheduleOperationFailureType.InternalError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Please contact support.", - }); } } @@ -200,40 +140,10 @@ public void PauseSchedule(TaskEntityContext context) this.State.RefreshScheduleRunExecutionToken(); this.logger.PausedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Succeeded.ToString()); - } - catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); - this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleInvalidTransitionEx.Message, - Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure the schedule is in a valid state for pause.", - }); - } - catch (ScheduleClientValidationException scheduleClientValidationEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); - this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleClientValidationEx.Message, - Type = ScheduleOperationFailureType.ValidationError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure request is valid.", - }); } catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); - this.State.AddActivityLog(nameof(this.PauseSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = "Failed to pause schedule", - Type = ScheduleOperationFailureType.InternalError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Please contact support.", - }); } } @@ -256,43 +166,13 @@ public void ResumeSchedule(TaskEntityContext context) this.TryStatusTransition(nameof(this.ResumeSchedule), ScheduleStatus.Active); this.State.NextRunAt = null; this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); - this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Succeeded.ToString()); // compute next run based on startat and interval context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } - catch (ScheduleInvalidTransitionException scheduleInvalidTransitionEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), scheduleInvalidTransitionEx.Message, scheduleInvalidTransitionEx); - this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleInvalidTransitionEx.Message, - Type = ScheduleOperationFailureType.InvalidStateTransition.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure the schedule is in a valid state for resume.", - }); - } - catch (ScheduleClientValidationException scheduleClientValidationEx) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), scheduleClientValidationEx.Message, scheduleClientValidationEx); - this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = scheduleClientValidationEx.Message, - Type = ScheduleOperationFailureType.ValidationError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Ensure request is valid.", - }); - } catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); - this.State.AddActivityLog(nameof(this.ResumeSchedule), ScheduleOperationStatus.Failed.ToString(), new FailureDetails - { - Reason = "Failed to resume schedule", - Type = ScheduleOperationFailureType.InternalError.ToString(), - OccurredAt = DateTimeOffset.UtcNow, - SuggestedFix = "Please contact support.", - }); } } diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs index da9e50e9..d3107d45 100644 --- a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs +++ b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs @@ -29,18 +29,6 @@ public ScheduleOperationFailedException(ScheduleDescription schedule, Exception this.Schedule = schedule; } - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that failed. - /// The operation that failed. - /// The status of the failed operation. - /// Details about the failure. - public ScheduleOperationFailedException(string scheduleId, string operation, string status, FailureDetails? failureDetails) - : base($"Operation '{operation}' failed for schedule '{scheduleId}' with status '{status}'. Details: {failureDetails}") - { - } - /// /// Gets the schedule that failed. /// diff --git a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs index b7253d1a..8cb803c9 100644 --- a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs @@ -16,6 +16,10 @@ public static class DurableTaskSchedulerWorkerExtensions /// The worker builder to add scheduled task support to. public static void EnableScheduledTasksSupport(this IDurableTaskWorkerBuilder builder) { - builder.AddTasks(r => r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp))); + builder.AddTasks(r => + { + r.AddEntity(nameof(Schedule), sp => ActivatorUtilities.CreateInstance(sp)); + r.AddOrchestrator(); + }); } } diff --git a/src/ScheduledTasks/Models/ScheduleActivityLog.cs b/src/ScheduledTasks/Models/ScheduleActivityLog.cs deleted file mode 100644 index 8969df8f..00000000 --- a/src/ScheduledTasks/Models/ScheduleActivityLog.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Represents a log entry for schedule activity. -/// -public record ScheduleActivityLog -{ - /// - /// Gets the operation performed. - /// - public string Operation { get; init; } = string.Empty; - - /// - /// Gets the status of the operation. - /// - public string Status { get; init; } = string.Empty; - - /// - /// Gets the timestamp when the operation occurred. - /// - public DateTimeOffset Timestamp { get; init; } - - /// - /// Gets the failure details if the operation failed. - /// - public FailureDetails? FailureDetails { get; init; } -} - -/// -/// Represents details about a failure that occurred during schedule execution. -/// -public record FailureDetails -{ - /// - /// Gets the reason for the failure. - /// - public string Reason { get; init; } = string.Empty; - - /// - /// Gets the type of failure. - /// - public string Type { get; init; } = string.Empty; - - /// - /// Gets when the failure occurred. - /// - public DateTimeOffset OccurredAt { get; init; } - - /// - /// Gets the suggested fix for the failure. - /// - public string? SuggestedFix { get; init; } -} diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 7575f18b..1712500c 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -70,11 +70,6 @@ public record ScheduleDescription /// public DateTimeOffset? NextRunAt { get; init; } - /// - /// Gets the activity logs for this schedule. - /// - public IReadOnlyCollection ActivityLogs { get; init; } = Array.Empty(); - /// /// Returns a JSON string representation of the schedule description. /// diff --git a/src/ScheduledTasks/Models/ScheduleQuery.cs b/src/ScheduledTasks/Models/ScheduleQuery.cs index 6ffb91a7..17d07d42 100644 --- a/src/ScheduledTasks/Models/ScheduleQuery.cs +++ b/src/ScheduledTasks/Models/ScheduleQuery.cs @@ -13,11 +13,6 @@ public record ScheduleQuery /// public const int DefaultPageSize = 100; - /// - /// Gets a value indicating whether to include full activity logs in the returned schedules. - /// - public bool IncludeFullActivityLogs { get; init; } - /// /// Gets the filter for the schedule status. /// diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 6c6c005b..7007c762 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -8,13 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// class ScheduleState { - const int MaxActivityLogItems = 10; - - /// - /// Gets or sets the queue of activity logs for this schedule, maintaining the most recent entries. - /// - public Queue ActivityLogs { get; set; } = new Queue(); - /// /// Gets or sets the current status of the schedule. /// @@ -57,29 +50,4 @@ public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } - - /// - /// Adds an activity log entry to the schedule's history. - /// - /// The operation being performed. - /// The status of the operation. - /// Optional failure details if the operation failed. - public void AddActivityLog(string operation, string status, FailureDetails? failureDetails = null) - { - ScheduleActivityLog log = new ScheduleActivityLog - { - Operation = operation, - Status = status, - Timestamp = DateTimeOffset.UtcNow, - FailureDetails = failureDetails, - }; - - this.ActivityLogs.Enqueue(log); - - // Keep only the most recent MaxActivityLogItems - while (this.ActivityLogs.Count > MaxActivityLogItems) - { - this.ActivityLogs.Dequeue(); - } - } } diff --git a/src/ScheduledTasks/Models/ScheduleStatus.cs b/src/ScheduledTasks/Models/ScheduleStatus.cs index ac1f3a8e..94e0c191 100644 --- a/src/ScheduledTasks/Models/ScheduleStatus.cs +++ b/src/ScheduledTasks/Models/ScheduleStatus.cs @@ -3,8 +3,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: Find whether it is possible to remove uninitialized, have to ensure atomicity of creation if possible - /// /// Represents the current status of a schedule. /// diff --git a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs index 40fc93ef..70a63658 100644 --- a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs +++ b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs @@ -12,13 +12,14 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// Orchestrator that executes operations on schedule entities. /// Calls the specified operation on the target entity and returns the result. /// -[DurableTask("ExecuteScheduleOperation")] +[DurableTask] public class ExecuteScheduleOperationOrchestrator : TaskOrchestrator { /// public override async Task RunAsync(TaskOrchestrationContext context, ScheduleOperationRequest input) { - return await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); + var res = await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); + return res; } } From 5f00e2dffd0d5b0fca761570cbb17c819ba92c34 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:49:43 -0800 Subject: [PATCH 123/203] poll free part 2 --- src/ScheduledTasks/Client/ScheduleHandle.cs | 57 +++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index fbe7a53b..65e22f61 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -82,7 +82,19 @@ public async Task PauseAsync(CancellationToken cancellation = default) { this.logger.ClientPausingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.PauseSchedule), cancellation: cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.PauseSchedule)); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to pause schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } } /// @@ -90,7 +102,19 @@ public async Task ResumeAsync(CancellationToken cancellation = default) { this.logger.ClientResumingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.ResumeSchedule), cancellation: cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.ResumeSchedule)); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to resume schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } } /// @@ -98,7 +122,20 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationT { this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions, cancellation: cancellation); + + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to update schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } } /// @@ -106,6 +143,18 @@ public async Task DeleteAsync(CancellationToken cancellation = default) { this.logger.ClientDeletingSchedule(this.ScheduleId); - await this.durableTaskClient.Entities.SignalEntityAsync(this.EntityId, "delete", cancellation: cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, "delete"); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to delete schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } } } From 2041f6b2382e82ed51003c9b5dc47ae6d1380c55 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:56:29 -0800 Subject: [PATCH 124/203] poll free working --- .../Orchestrations/ExecuteScheduleOperationOrchestrator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs index 70a63658..0f2baa0b 100644 --- a/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs +++ b/src/ScheduledTasks/Orchestrations/ExecuteScheduleOperationOrchestrator.cs @@ -18,8 +18,7 @@ public class ExecuteScheduleOperationOrchestrator : TaskOrchestrator public override async Task RunAsync(TaskOrchestrationContext context, ScheduleOperationRequest input) { - var res = await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); - return res; + return await context.Entities.CallEntityAsync(input.EntityId, input.OperationName, input.Input); } } From 9bfcf1a225132c1f19d4381bd872681e510756b7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 02:31:40 -0800 Subject: [PATCH 125/203] clean up --- src/ScheduledTasks/Client/ScheduleHandle.cs | 217 ++++++++++++------ .../Client/ScheduledTaskClient.cs | 142 ++++++++---- .../Exception/ScheduleCreationException.cs | 27 --- .../Exception/ScheduleInternalException.cs | 38 --- .../ScheduleOperationFailedException.cs | 41 ---- .../ScheduleStillBeingProvisionedException.cs | 36 --- src/ScheduledTasks/Logging/Client/Logs.cs | 2 +- .../Models/ScheduleOperationFailureType.cs | 30 --- .../Models/ScheduleOperationStatus.cs | 20 -- 9 files changed, 241 insertions(+), 312 deletions(-) delete mode 100644 src/ScheduledTasks/Exception/ScheduleCreationException.cs delete mode 100644 src/ScheduledTasks/Exception/ScheduleInternalException.cs delete mode 100644 src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs delete mode 100644 src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs delete mode 100644 src/ScheduledTasks/Models/ScheduleOperationFailureType.cs delete mode 100644 src/ScheduledTasks/Models/ScheduleOperationStatus.cs diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 65e22f61..30b9c048 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -8,8 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: Validaiton - /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// @@ -46,115 +44,190 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge /// public async Task DescribeAsync(CancellationToken cancellation = default) { - Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); - EntityMetadata? metadata = - await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation: cancellation); - if (metadata == null) + try { - throw new ScheduleNotFoundException(this.ScheduleId); + Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); + + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); + EntityMetadata? metadata = + await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation: cancellation); + if (metadata == null) + { + throw new ScheduleNotFoundException(this.ScheduleId); + } + + ScheduleState state = metadata.State; + + ScheduleConfiguration? config = state.ScheduleConfiguration; + + return new ScheduleDescription + { + ScheduleId = this.ScheduleId, + OrchestrationName = config?.OrchestrationName, + OrchestrationInput = config?.OrchestrationInput, + OrchestrationInstanceId = config?.OrchestrationInstanceId, + StartAt = config?.StartAt, + EndAt = config?.EndAt, + Interval = config?.Interval, + StartImmediatelyIfLate = config?.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + }; } - - ScheduleState state = metadata.State; - - ScheduleConfiguration? config = state.ScheduleConfiguration; - - return new ScheduleDescription + catch (OperationCanceledException ex) { - ScheduleId = this.ScheduleId, - OrchestrationName = config?.OrchestrationName, - OrchestrationInput = config?.OrchestrationInput, - OrchestrationInstanceId = config?.OrchestrationInstanceId, - StartAt = config?.StartAt, - EndAt = config?.EndAt, - Interval = config?.Interval, - StartImmediatelyIfLate = config?.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }; + this.logger.ClientError( + nameof(this.DescribeAsync), + this.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.DescribeAsync)} operation was canceled.", + null, + cancellation); + } } /// public async Task PauseAsync(CancellationToken cancellation = default) { - this.logger.ClientPausingSchedule(this.ScheduleId); + try + { + this.logger.ClientPausingSchedule(this.ScheduleId); - ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.PauseSchedule)); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.PauseSchedule)); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to pause schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + } + catch (OperationCanceledException ex) { - throw new InvalidOperationException($"Failed to pause schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + this.logger.ClientError( + nameof(this.PauseAsync), + this.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.PauseAsync)} operation was canceled.", + null, + cancellation); } } /// public async Task ResumeAsync(CancellationToken cancellation = default) { - this.logger.ClientResumingSchedule(this.ScheduleId); + try + { + this.logger.ClientResumingSchedule(this.ScheduleId); - ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.ResumeSchedule)); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.ResumeSchedule)); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to resume schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + } + catch (OperationCanceledException ex) { - throw new InvalidOperationException($"Failed to resume schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + this.logger.ClientError( + nameof(this.ResumeAsync), + this.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.ResumeAsync)} operation was canceled.", + null, + cancellation); } } /// public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default) { - this.logger.ClientUpdatingSchedule(this.ScheduleId); - Check.NotNull(updateOptions, nameof(updateOptions)); - - ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); - - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + try { - throw new InvalidOperationException($"Failed to update schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + this.logger.ClientUpdatingSchedule(this.ScheduleId); + Check.NotNull(updateOptions, nameof(updateOptions)); + + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to update schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + } + catch (OperationCanceledException ex) + { + this.logger.ClientError( + nameof(this.UpdateAsync), + this.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.UpdateAsync)} operation was canceled.", + null, + cancellation); } } /// public async Task DeleteAsync(CancellationToken cancellation = default) { - this.logger.ClientDeletingSchedule(this.ScheduleId); + try + { + this.logger.ClientDeletingSchedule(this.ScheduleId); - ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, "delete"); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, "delete"); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to delete schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + } + catch (OperationCanceledException ex) { - throw new InvalidOperationException($"Failed to delete schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + this.logger.ClientError( + nameof(this.DeleteAsync), + this.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.DeleteAsync)} operation was canceled.", + null, + cancellation); } } } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index f1d35e2a..34470886 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -31,34 +31,50 @@ public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) { Check.NotNull(creationOptions, nameof(creationOptions)); + this.logger.ClientCreatingSchedule(creationOptions); - string scheduleId = creationOptions.ScheduleId; - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - - // Check if schedule already exists - bool scheduleExists = await this.CheckScheduleExists(scheduleId, cancellation); - if (scheduleExists) + try { - throw new ScheduleAlreadyExistException(scheduleId); - } + string scheduleId = creationOptions.ScheduleId; + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + + // Check if schedule already exists + bool scheduleExists = await this.CheckScheduleExists(scheduleId, cancellation); + if (scheduleExists) + { + throw new ScheduleAlreadyExistException(scheduleId); + } + + // Call the orchestrator to create the schedule + ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); - // Call the orchestrator to create the schedule - ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to create schedule '{scheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + // Return a handle to the schedule + return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + } + catch (OperationCanceledException ex) { - throw new ScheduleCreationException(scheduleId, state.FailureDetails?.ErrorMessage ?? string.Empty); + this.logger.ClientError( + nameof(this.CreateScheduleAsync), + creationOptions.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.CreateScheduleAsync)} operation was canceled.", + null, + cancellation); } - - // Return a handle to the schedule - return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); } /// @@ -66,36 +82,55 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); - - if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) + try { - return null; - } + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); - ScheduleState state = metadata.State; - ScheduleConfiguration? config = state.ScheduleConfiguration; + if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) + { + return null; + } - return new ScheduleDescription + ScheduleState state = metadata.State; + ScheduleConfiguration? config = state.ScheduleConfiguration; + + return new ScheduleDescription + { + ScheduleId = scheduleId, + OrchestrationName = config?.OrchestrationName, + OrchestrationInput = config?.OrchestrationInput, + OrchestrationInstanceId = config?.OrchestrationInstanceId, + StartAt = config?.StartAt, + EndAt = config?.EndAt, + Interval = config?.Interval, + StartImmediatelyIfLate = config?.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + }; + } + catch (OperationCanceledException ex) { - ScheduleId = scheduleId, - OrchestrationName = config?.OrchestrationName, - OrchestrationInput = config?.OrchestrationInput, - OrchestrationInstanceId = config?.OrchestrationInstanceId, - StartAt = config?.StartAt, - EndAt = config?.EndAt, - Interval = config?.Interval, - StartImmediatelyIfLate = config?.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }; + this.logger.ClientError( + nameof(this.GetScheduleAsync), + scheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.GetScheduleAsync)} operation was canceled.", + null, + cancellation); + } } /// - public IScheduleHandle GetScheduleHandle(string scheduleId) => new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + public IScheduleHandle GetScheduleHandle(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + } /// public Task> ListSchedulesAsync(ScheduleQuery? filter = null) @@ -210,9 +245,22 @@ async Task CheckScheduleExists(string scheduleId, CancellationToken cancel { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, false, cancellation); + try + { + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, false, cancellation); - return metadata != null; + return metadata != null; + } + catch (OperationCanceledException e) + { + this.logger.ClientError( + nameof(this.CheckScheduleExists), + scheduleId, + e); + + throw new OperationCanceledException( + $"The {nameof(this.CheckScheduleExists)} operation was canceled.", e, e.CancellationToken); + } } } diff --git a/src/ScheduledTasks/Exception/ScheduleCreationException.cs b/src/ScheduledTasks/Exception/ScheduleCreationException.cs deleted file mode 100644 index 8e814f23..00000000 --- a/src/ScheduledTasks/Exception/ScheduleCreationException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Exception thrown when schedule creation fails. -/// -public class ScheduleCreationException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that failed to create. - /// The error message. - /// The inner exception. - public ScheduleCreationException(string scheduleId, string message, Exception? innerException = null) - : base($"Failed to create schedule '{scheduleId}': {message}", innerException) - { - this.ScheduleId = scheduleId; - } - - /// - /// Gets the ID of the schedule that failed to create. - /// - public string ScheduleId { get; } -} \ No newline at end of file diff --git a/src/ScheduledTasks/Exception/ScheduleInternalException.cs b/src/ScheduledTasks/Exception/ScheduleInternalException.cs deleted file mode 100644 index a5d682c8..00000000 --- a/src/ScheduledTasks/Exception/ScheduleInternalException.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Exception thrown when an internal server error occurs while processing a schedule. -/// -public class ScheduleInternalException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that encountered the error. - /// The error message that explains the reason for the exception. - public ScheduleInternalException(string scheduleId, string message) - : base($"An internal error occurred while processing schedule '{scheduleId}': {message}") - { - this.ScheduleId = scheduleId; - } - - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that encountered the error. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - public ScheduleInternalException(string scheduleId, string message, Exception innerException) - : base($"An internal error occurred while processing schedule '{scheduleId}': {message}", innerException) - { - this.ScheduleId = scheduleId; - } - - /// - /// Gets the ID of the schedule that encountered the internal error. - /// - public string ScheduleId { get; } -} diff --git a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs b/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs deleted file mode 100644 index d3107d45..00000000 --- a/src/ScheduledTasks/Exception/ScheduleOperationFailedException.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Exception thrown when a schedule operation fails. -/// -public class ScheduleOperationFailedException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The schedule that failed. - public ScheduleOperationFailedException(ScheduleDescription schedule) - : base($"Operation failed for schedule '{schedule.ScheduleId}'. Refer to schedule details {schedule.ToJsonString()} for more information.") - { - this.Schedule = schedule; - } - - /// - /// Initializes a new instance of the class. - /// - /// The schedule that failed. - /// The exception that is the cause of the current exception. - public ScheduleOperationFailedException(ScheduleDescription schedule, Exception innerException) - : base($"Operation failed for schedule '{schedule.ScheduleId}'. Refer to schedule details {schedule.ToJsonString()} for more information.", innerException) - { - this.Schedule = schedule; - } - - /// - /// Gets the schedule that failed. - /// - public ScheduleDescription? Schedule { get; } - - /// - /// Gets the ID of the schedule that failed. - /// - public string ScheduleId => this.Schedule?.ScheduleId ?? this.ScheduleId; -} diff --git a/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs b/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs deleted file mode 100644 index 0247cfee..00000000 --- a/src/ScheduledTasks/Exception/ScheduleStillBeingProvisionedException.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Exception thrown when attempting to perform an operation on a schedule that is still being provisioned. -/// -public class ScheduleStillBeingProvisionedException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that is still being provisioned. - public ScheduleStillBeingProvisionedException(string scheduleId) - : base($"Schedule '{scheduleId}' is still being provisioned.") - { - this.ScheduleId = scheduleId; - } - - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that is still being provisioned. - /// The exception that is the cause of the current exception. - public ScheduleStillBeingProvisionedException(string scheduleId, Exception innerException) - : base($"Schedule '{scheduleId}' is still being provisioned.", innerException) - { - this.ScheduleId = scheduleId; - } - - /// - /// Gets the ID of the schedule that is still being provisioned. - /// - public string ScheduleId { get; } -} diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Client/Logs.cs index 73569537..36817e9f 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Client/Logs.cs @@ -35,5 +35,5 @@ static partial class Logs public static partial void ClientWarning(this ILogger logger, string message, string scheduleId); [LoggerMessage(EventId = 88, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] - public static partial void ClientError(this ILogger logger, string message, string scheduleId); + public static partial void ClientError(this ILogger logger, string message, string scheduleId, Exception? exception = null); } diff --git a/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs b/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs deleted file mode 100644 index 702600e9..00000000 --- a/src/ScheduledTasks/Models/ScheduleOperationFailureType.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Represents the type of failure that occurred during a schedule operation. -/// -public enum ScheduleOperationFailureType -{ - /// - /// The operation failed due to an invalid operation being attempted. - /// - InvalidOperation, - - /// - /// The operation failed due to an invalid state transition. - /// - InvalidStateTransition, - - /// - /// The operation failed due to validation errors. - /// - ValidationError, - - /// - /// The operation failed due to an internal server error. - /// - InternalError, -} diff --git a/src/ScheduledTasks/Models/ScheduleOperationStatus.cs b/src/ScheduledTasks/Models/ScheduleOperationStatus.cs deleted file mode 100644 index 8f91d98a..00000000 --- a/src/ScheduledTasks/Models/ScheduleOperationStatus.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Represents the current status of a schedule operation. -/// -public enum ScheduleOperationStatus -{ - /// - /// The operation completed successfully. - /// - Succeeded, - - /// - /// The operation failed. - /// - Failed, -} From c96fdc3a6db2210d397ba781eb089bf60579559c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 02:36:10 -0800 Subject: [PATCH 126/203] more cleanup --- src/ScheduledTasks/Client/ScheduleHandle.cs | 2 ++ src/ScheduledTasks/Entity/Schedule.cs | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 30b9c048..880bb138 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -196,6 +196,8 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationT } } + // TODO: verify deleting non existent wont throw exception + /// public async Task DeleteAsync(CancellationToken cancellation = default) { diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 82b10e1b..48f684c9 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -6,8 +6,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; -// TODO: Support other schedule option properties like cron expression, max occurrence, etc. - /// /// Entity that manages the state and execution of a scheduled task. /// @@ -53,7 +51,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } catch (Exception ex) { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); + this.logger.ScheduleOperationError(scheduleCreationOptions.ScheduleId, nameof(this.CreateSchedule), "Failed to create schedule", ex); throw; } } From 0d40ae0b3a40eff6656cc0aad636a628fd53a322 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 23 Feb 2025 02:44:53 -0800 Subject: [PATCH 127/203] clean --- samples/ScheduleDemo/ScheduleDemo.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleDemo/ScheduleDemo.csproj index 9d28d20b..09d6050b 100644 --- a/samples/ScheduleDemo/ScheduleDemo.csproj +++ b/samples/ScheduleDemo/ScheduleDemo.csproj @@ -10,9 +10,6 @@ - - From c99cf1bb84cda91c51df5285875884b99db7b480 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:15:36 -0800 Subject: [PATCH 128/203] rename scheduleconsoleapp --- Microsoft.DurableTask.sln | 13 +- .../Activities/GetStockPrice.cs | 2 +- .../Orchestrators/StockPriceOrchestrator.cs | 0 samples/ScheduleConsoleApp/Program.cs | 148 ++++++++++++++++++ .../ScheduleConsoleApp.csproj} | 0 .../appsettings.json | 2 +- samples/ScheduleDemo/Program.cs | 2 +- 7 files changed, 158 insertions(+), 9 deletions(-) rename samples/{ScheduleDemo => ScheduleConsoleApp}/Activities/GetStockPrice.cs (90%) rename samples/{ScheduleDemo => ScheduleConsoleApp}/Orchestrators/StockPriceOrchestrator.cs (100%) create mode 100644 samples/ScheduleConsoleApp/Program.cs rename samples/{ScheduleDemo/ScheduleDemo.csproj => ScheduleConsoleApp/ScheduleConsoleApp.csproj} (100%) rename samples/{ScheduleDemo => ScheduleConsoleApp}/appsettings.json (78%) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 58dca808..627046bb 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -83,12 +83,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CECADD EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{3272C041-F81D-4C85-A4FB-2A700B5A7A9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleDemo", "samples\ScheduleDemo\ScheduleDemo.csproj", "{FF37BC53-8EC1-4673-915B-E59B38E286DF}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppMinimal", "samples\ConsoleAppMinimal\ConsoleAppMinimal.csproj", "{B48FACA9-A328-452A-BFAE-C4F60F9C7024}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduledTasks", "src\ScheduledTasks\ScheduledTasks.csproj", "{69ED743C-D616-4530-87E2-391D249D7368}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleConsoleApp", "samples\ScheduleConsoleApp\ScheduleConsoleApp.csproj", "{A89B766C-987F-4C9F-8937-D0AB9FE640C8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -223,10 +223,6 @@ Global {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3272C041-F81D-4C85-A4FB-2A700B5A7A9D}.Release|Any CPU.Build.0 = Release|Any CPU - {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF37BC53-8EC1-4673-915B-E59B38E286DF}.Release|Any CPU.Build.0 = Release|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Debug|Any CPU.Build.0 = Debug|Any CPU {B48FACA9-A328-452A-BFAE-C4F60F9C7024}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -235,6 +231,10 @@ Global {69ED743C-D616-4530-87E2-391D249D7368}.Debug|Any CPU.Build.0 = Debug|Any CPU {69ED743C-D616-4530-87E2-391D249D7368}.Release|Any CPU.ActiveCfg = Release|Any CPU {69ED743C-D616-4530-87E2-391D249D7368}.Release|Any CPU.Build.0 = Release|Any CPU + {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -278,6 +278,7 @@ Global {3272C041-F81D-4C85-A4FB-2A700B5A7A9D} = {CECADDB5-E30A-4CE2-8604-9AC596D4A2DC} {B48FACA9-A328-452A-BFAE-C4F60F9C7024} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {69ED743C-D616-4530-87E2-391D249D7368} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/ScheduleDemo/Activities/GetStockPrice.cs b/samples/ScheduleConsoleApp/Activities/GetStockPrice.cs similarity index 90% rename from samples/ScheduleDemo/Activities/GetStockPrice.cs rename to samples/ScheduleConsoleApp/Activities/GetStockPrice.cs index b9e96b3a..63db17f7 100644 --- a/samples/ScheduleDemo/Activities/GetStockPrice.cs +++ b/samples/ScheduleConsoleApp/Activities/GetStockPrice.cs @@ -3,7 +3,7 @@ using Microsoft.DurableTask; -namespace ScheduleDemo.Activities; +namespace ScheduleConsoleApp.Activities; [DurableTask] public class GetStockPrice : TaskActivity diff --git a/samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs b/samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs similarity index 100% rename from samples/ScheduleDemo/Orchestrators/StockPriceOrchestrator.cs rename to samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs new file mode 100644 index 00000000..6df7d860 --- /dev/null +++ b/samples/ScheduleConsoleApp/Program.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.AzureManaged; +using Microsoft.DurableTask.ScheduledTasks; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ScheduleConsoleApp.Activities; + + +// Create the host builder +IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + + // Configure the worker + _ = services.AddDurableTaskWorker(builder => + { + // Add the Schedule entity and demo orchestration + builder.AddTasks(r => + { + // Add the orchestrator class + r.AddOrchestrator(); + + // Add required activities + r.AddActivity(); + }); + + // Enable scheduled tasks support + builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); + }); + + // Configure the client + services.AddDurableTaskClient(builder => + { + builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); + }); + + // Configure console logging + services.AddLogging(logging => + { + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); + }); + }) + .Build(); + +await host.StartAsync(); + +IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); + +try +{ + // list all schedules + // Define the initial query with the desired page size + ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; + + // Retrieve the pageable collection of schedule IDs + AsyncPageable schedules = await scheduledTaskClient.ListScheduleIdsAsync(query); + + // Initialize the continuation token + string? continuationToken = null; + await foreach (Page page in schedules.AsPages(continuationToken)) + { + foreach (string scheduleId in page.Values) + { + // Obtain the schedule handle for the current scheduleId + IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); + + // Delete the schedule + await handle.DeleteAsync(); + + Console.WriteLine($"Deleted schedule {scheduleId}"); + } + + // Update the continuation token for the next iteration + continuationToken = page.ContinuationToken; + + // If there's no continuation token, we've reached the end of the collection + if (continuationToken == null) + { + break; + } + } + + + // Create schedule options that runs every 4 seconds + ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) + { + StartAt = DateTimeOffset.UtcNow, + OrchestrationInput = "MSFT" + }; + + // Create the schedule and get a handle to it + ScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + + // Get the schedule description + ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); + + // print the schedule description + Console.WriteLine(scheduleDescription.ToJsonString(true)); + + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); + + // Pause the schedule + Console.WriteLine("\nPausing schedule..."); + await scheduleHandle.PauseAsync(); + scheduleDescription = await scheduleHandle.DescribeAsync(); + Console.WriteLine(scheduleDescription.ToJsonString(true)); + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); + + + // Resume the schedule + Console.WriteLine("\nResuming schedule..."); + await scheduleHandle.ResumeAsync(); + scheduleDescription = await scheduleHandle.DescribeAsync(); + Console.WriteLine(scheduleDescription.ToJsonString(true)); + + Console.WriteLine(""); + Console.WriteLine(""); + Console.WriteLine(""); + + await Task.Delay(TimeSpan.FromMinutes(30)); + //Console.WriteLine("\nPress any key to delete the schedule and exit..."); + //Console.ReadKey(); +} +catch (Exception ex) +{ + Console.WriteLine($"One of your schedule operations failed, please fix and rerun: {ex.Message}"); +} + +await host.StopAsync(); \ No newline at end of file diff --git a/samples/ScheduleDemo/ScheduleDemo.csproj b/samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj similarity index 100% rename from samples/ScheduleDemo/ScheduleDemo.csproj rename to samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj diff --git a/samples/ScheduleDemo/appsettings.json b/samples/ScheduleConsoleApp/appsettings.json similarity index 78% rename from samples/ScheduleDemo/appsettings.json rename to samples/ScheduleConsoleApp/appsettings.json index fa3d5ee7..034ac402 100644 --- a/samples/ScheduleDemo/appsettings.json +++ b/samples/ScheduleConsoleApp/appsettings.json @@ -3,7 +3,7 @@ "LogLevel": { "Default": "Debug", "Microsoft": "Warning", - "ScheduleDemo": "Debug", + "ScheduleConsoleApp": "Debug", "DemoOrchestration": "Debug" } } diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs index d93314c5..6df7d860 100644 --- a/samples/ScheduleDemo/Program.cs +++ b/samples/ScheduleDemo/Program.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using ScheduleDemo.Activities; +using ScheduleConsoleApp.Activities; // Create the host builder From b8be74122e8f3ed566731f6aa990e26174e8fe8e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:39:39 -0800 Subject: [PATCH 129/203] remove --- samples/ScheduleDemo/Program.cs | 148 -------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 samples/ScheduleDemo/Program.cs diff --git a/samples/ScheduleDemo/Program.cs b/samples/ScheduleDemo/Program.cs deleted file mode 100644 index 6df7d860..00000000 --- a/samples/ScheduleDemo/Program.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.AzureManaged; -using Microsoft.DurableTask.ScheduledTasks; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Worker.AzureManaged; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using ScheduleConsoleApp.Activities; - - -// Create the host builder -IHost host = Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") - ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); - - // Configure the worker - _ = services.AddDurableTaskWorker(builder => - { - // Add the Schedule entity and demo orchestration - builder.AddTasks(r => - { - // Add the orchestrator class - r.AddOrchestrator(); - - // Add required activities - r.AddActivity(); - }); - - // Enable scheduled tasks support - builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); - }); - - // Configure the client - services.AddDurableTaskClient(builder => - { - builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); - }); - - // Configure console logging - services.AddLogging(logging => - { - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); - }); - }) - .Build(); - -await host.StartAsync(); - -IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); - -try -{ - // list all schedules - // Define the initial query with the desired page size - ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; - - // Retrieve the pageable collection of schedule IDs - AsyncPageable schedules = await scheduledTaskClient.ListScheduleIdsAsync(query); - - // Initialize the continuation token - string? continuationToken = null; - await foreach (Page page in schedules.AsPages(continuationToken)) - { - foreach (string scheduleId in page.Values) - { - // Obtain the schedule handle for the current scheduleId - IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); - - // Delete the schedule - await handle.DeleteAsync(); - - Console.WriteLine($"Deleted schedule {scheduleId}"); - } - - // Update the continuation token for the next iteration - continuationToken = page.ContinuationToken; - - // If there's no continuation token, we've reached the end of the collection - if (continuationToken == null) - { - break; - } - } - - - // Create schedule options that runs every 4 seconds - ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) - { - StartAt = DateTimeOffset.UtcNow, - OrchestrationInput = "MSFT" - }; - - // Create the schedule and get a handle to it - ScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); - - // Get the schedule description - ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); - - // print the schedule description - Console.WriteLine(scheduleDescription.ToJsonString(true)); - - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - // Pause the schedule - Console.WriteLine("\nPausing schedule..."); - await scheduleHandle.PauseAsync(); - scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription.ToJsonString(true)); - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - - // Resume the schedule - Console.WriteLine("\nResuming schedule..."); - await scheduleHandle.ResumeAsync(); - scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription.ToJsonString(true)); - - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - await Task.Delay(TimeSpan.FromMinutes(30)); - //Console.WriteLine("\nPress any key to delete the schedule and exit..."); - //Console.ReadKey(); -} -catch (Exception ex) -{ - Console.WriteLine($"One of your schedule operations failed, please fix and rerun: {ex.Message}"); -} - -await host.StopAsync(); \ No newline at end of file From 47858996ab4f289c449fbf33bddbadbc15307243 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:50:01 -0800 Subject: [PATCH 130/203] save --- samples/ScheduleConsoleApp/Program.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index 6df7d860..4178cad6 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -23,14 +23,7 @@ _ = services.AddDurableTaskWorker(builder => { // Add the Schedule entity and demo orchestration - builder.AddTasks(r => - { - // Add the orchestrator class - r.AddOrchestrator(); - - // Add required activities - r.AddActivity(); - }); + builder.AddTasks(r => r.AddAllGeneratedTasks()); // Enable scheduled tasks support builder.UseDurableTaskScheduler(connectionString); From 9ed36bdf54846de7ee51e7cc547f2b34f768d2ca Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:44:19 -0800 Subject: [PATCH 131/203] schedule webapp sample --- Microsoft.DurableTask.sln | 7 + samples/ScheduleConsoleApp/Program.cs | 2 - .../Models/CreateScheduleRequest.cs | 40 +++ .../Models/UpdateScheduleRequest.cs | 35 +++ .../CacheClearingOrchestrator.cs | 30 ++ samples/ScheduleWebApp/Program.cs | 57 ++++ .../Properties/launchSettings.json | 21 ++ samples/ScheduleWebApp/ScheduleController.cs | 261 ++++++++++++++++++ samples/ScheduleWebApp/ScheduleWebApp.csproj | 22 ++ samples/ScheduleWebApp/ScheduleWebApp.http | 0 .../appsettings.Development.json | 8 + samples/ScheduleWebApp/appsettings.json | 9 + 12 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 samples/ScheduleWebApp/Models/CreateScheduleRequest.cs create mode 100644 samples/ScheduleWebApp/Models/UpdateScheduleRequest.cs create mode 100644 samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs create mode 100644 samples/ScheduleWebApp/Program.cs create mode 100644 samples/ScheduleWebApp/Properties/launchSettings.json create mode 100644 samples/ScheduleWebApp/ScheduleController.cs create mode 100644 samples/ScheduleWebApp/ScheduleWebApp.csproj create mode 100644 samples/ScheduleWebApp/ScheduleWebApp.http create mode 100644 samples/ScheduleWebApp/appsettings.Development.json create mode 100644 samples/ScheduleWebApp/appsettings.json diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 627046bb..2cc79905 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduledTasks", "src\Sched EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleConsoleApp", "samples\ScheduleConsoleApp\ScheduleConsoleApp.csproj", "{A89B766C-987F-4C9F-8937-D0AB9FE640C8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleWebApp", "samples\ScheduleWebApp\ScheduleWebApp.csproj", "{100348B5-4D97-4A3F-B777-AB14F276F8FE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -235,6 +237,10 @@ Global {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {A89B766C-987F-4C9F-8937-D0AB9FE640C8}.Release|Any CPU.Build.0 = Release|Any CPU + {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -279,6 +285,7 @@ Global {B48FACA9-A328-452A-BFAE-C4F60F9C7024} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {69ED743C-D616-4530-87E2-391D249D7368} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index 4178cad6..da51be6d 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -9,8 +9,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using ScheduleConsoleApp.Activities; - // Create the host builder IHost host = Host.CreateDefaultBuilder(args) diff --git a/samples/ScheduleWebApp/Models/CreateScheduleRequest.cs b/samples/ScheduleWebApp/Models/CreateScheduleRequest.cs new file mode 100644 index 00000000..08c1c0e0 --- /dev/null +++ b/samples/ScheduleWebApp/Models/CreateScheduleRequest.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace ScheduleWebApp.Models; + +/// +/// Represents a request to create a new schedule. +/// +public class CreateScheduleRequest +{ + /// + /// Gets or sets the unique identifier for the schedule. + /// + public string Id { get; set; } = default!; + + /// + /// Gets or sets the name of the orchestration to be scheduled. + /// + public string OrchestrationName { get; set; } = default!; + + /// + /// Gets or sets the input data for the orchestration. + /// + public string? Input { get; set; } + + /// + /// Gets or sets the time interval between schedule executions. + /// + public TimeSpan Interval { get; set; } + + /// + /// Gets or sets the time when the schedule should start. + /// + public DateTimeOffset? StartAt { get; set; } + + /// + /// Gets or sets the time when the schedule should end. + /// + public DateTimeOffset? EndAt { get; set; } +} diff --git a/samples/ScheduleWebApp/Models/UpdateScheduleRequest.cs b/samples/ScheduleWebApp/Models/UpdateScheduleRequest.cs new file mode 100644 index 00000000..371af1e3 --- /dev/null +++ b/samples/ScheduleWebApp/Models/UpdateScheduleRequest.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace ScheduleWebApp.Models; + +/// +/// Represents a request to update an existing schedule. +/// +public class UpdateScheduleRequest +{ + /// + /// Gets or sets the name of the orchestration to be scheduled. + /// + public string OrchestrationName { get; set; } = default!; + + /// + /// Gets or sets the input data for the orchestration. + /// + public string? Input { get; set; } + + /// + /// Gets or sets the time interval between schedule executions. + /// + public TimeSpan Interval { get; set; } + + /// + /// Gets or sets the time when the schedule should start. + /// + public DateTimeOffset? StartAt { get; set; } + + /// + /// Gets or sets the time when the schedule should end. + /// + public DateTimeOffset? EndAt { get; set; } +} diff --git a/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs new file mode 100644 index 00000000..4113f07a --- /dev/null +++ b/samples/ScheduleWebApp/Orchestrations/CacheClearingOrchestrator.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace ScheduleWebApp.Orchestrations; + +public class CacheClearingOrchestrator : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string scheduleId) + { + ILogger logger = context.CreateReplaySafeLogger(nameof(CacheClearingOrchestrator)); + try + { + logger.LogInformation("Starting CacheClearingOrchestration for schedule ID: {ScheduleId}", scheduleId); + + // Simulate cache clearing + await Task.Delay(TimeSpan.FromSeconds(5)); + + logger.LogInformation("CacheClearingOrchestration completed for schedule ID: {ScheduleId}", scheduleId); + + return "ok"; + } + catch (Exception ex) + { + logger.LogError(ex, "Error in CacheClearingOrchestration for schedule ID: {ScheduleId}", scheduleId); + throw; + } + } +} \ No newline at end of file diff --git a/samples/ScheduleWebApp/Program.cs b/samples/ScheduleWebApp/Program.cs new file mode 100644 index 00000000..630602f4 --- /dev/null +++ b/samples/ScheduleWebApp/Program.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.AzureManaged; +using Microsoft.DurableTask.ScheduledTasks; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using ScheduleWebApp.Orchestrations; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +string connectionString = builder.Configuration["DURABLE_TASK_SCHEDULER_CONNECTION_STRING"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + +// Add all the generated orchestrations and activities automatically +builder.Services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(r => + { + // Add your orchestrators and activities here + r.AddOrchestrator(); + }); + builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); +}); + +// Register the client, which can be used to start orchestrations +builder.Services.AddDurableTaskClient(builder => +{ + builder.UseDurableTaskScheduler(connectionString); + builder.EnableScheduledTasksSupport(); +}); + +// Configure console logging using the simpler, more compact format +builder.Services.AddLogging(logging => +{ + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); +}); + +// Configure the HTTP request pipeline +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); + +// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" +WebApplication app = builder.Build(); +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/samples/ScheduleWebApp/Properties/launchSettings.json b/samples/ScheduleWebApp/Properties/launchSettings.json new file mode 100644 index 00000000..410861e4 --- /dev/null +++ b/samples/ScheduleWebApp/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:47697", + "sslPort": 44371 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "applicationUrl": "http://localhost:5008", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs new file mode 100644 index 00000000..c56a2953 --- /dev/null +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Mvc; +using Microsoft.DurableTask; +using Microsoft.DurableTask.ScheduledTasks; +using ScheduleWebApp.Models; + +namespace ScheduleWebApp.Controllers; + +/// +/// Controller for managing scheduled tasks through a REST API. +/// Provides endpoints for creating, reading, updating, and deleting schedules, +/// as well as pausing and resuming them. +/// +[ApiController] +[Route("schedules")] +public class ScheduleController : ControllerBase +{ + readonly IScheduledTaskClient scheduledTaskClient; + readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// Client for managing scheduled tasks. + /// Logger for recording controller operations. + public ScheduleController( + IScheduledTaskClient scheduledTaskClient, + ILogger logger) + { + this.scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Creates a new schedule based on the provided configuration. + /// + /// The schedule configuration to create. + /// The created schedule description. + [HttpPost] + public async Task> CreateSchedule([FromBody] CreateScheduleRequest createScheduleRequest) + { + if (createScheduleRequest == null) + { + return this.BadRequest("createScheduleRequest cannot be null"); + } + + try + { + ScheduleCreationOptions creationOptions = new ScheduleCreationOptions(createScheduleRequest.Id.ToString(), createScheduleRequest.OrchestrationName, createScheduleRequest.Interval) + { + OrchestrationInput = createScheduleRequest.Input, + StartAt = createScheduleRequest.StartAt, + EndAt = createScheduleRequest.EndAt, + StartImmediatelyIfLate = true + }; + + IScheduleHandle handle = await this.scheduledTaskClient.CreateScheduleAsync(creationOptions); + ScheduleDescription description = await handle.DescribeAsync(); + + this.logger.LogInformation("Created new schedule with ID: {ScheduleId}", createScheduleRequest.Id); + + return this.CreatedAtAction(nameof(GetScheduleAsync), new { id = createScheduleRequest.Id }, description); + } + catch (ScheduleClientValidationException ex) + { + this.logger.LogError(ex, "Validation failed while creating schedule {ScheduleId}", createScheduleRequest.Id); + return this.BadRequest(ex.Message); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error creating schedule {ScheduleId}", createScheduleRequest.Id); + return this.StatusCode(500, "An error occurred while creating the schedule"); + } + } + + /// + /// Retrieves a specific schedule by its ID. + /// + /// The ID of the schedule to retrieve. + /// The schedule description if found. + [HttpGet("{id}")] + public async Task> GetScheduleAsync(string id) + { + try + { + ScheduleDescription? schedule = await this.scheduledTaskClient.GetScheduleAsync(id); + return this.Ok(schedule); + } + catch (ScheduleNotFoundException) + { + return this.NotFound(); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error retrieving schedule {ScheduleId}", id); + return this.StatusCode(500, "An error occurred while retrieving the schedule"); + } + } + + /// + /// Lists all schedules, optionally filtered by status. + /// + /// A collection of schedule descriptions. + [HttpGet("list")] + public async Task>> ListSchedules() + { + try + { + AsyncPageable schedules = await this.scheduledTaskClient.ListSchedulesAsync(); + + // add schedule result list + List scheduleList = new List(); + // Initialize the continuation token + string? continuationToken = null; + await foreach (Page page in schedules.AsPages(continuationToken)) + { + scheduleList.AddRange(page.Values.ToArray()); + + // Update the continuation token for the next iteration + continuationToken = page.ContinuationToken; + + // If there's no continuation token, we've reached the end of the collection + if (continuationToken == null) + { + break; + } + } + + + return this.Ok(scheduleList); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error retrieving schedules"); + return this.StatusCode(500, "An error occurred while retrieving schedules"); + } + } + + /// + /// Updates an existing schedule with new configuration. + /// + /// The ID of the schedule to update. + /// The new schedule configuration. + /// The updated schedule description. + [HttpPut("{id}")] + public async Task> UpdateSchedule(string id, [FromBody] UpdateScheduleRequest updateScheduleRequest) + { + if (updateScheduleRequest == null) + { + return this.BadRequest("Schedule cannot be null"); + } + + try + { + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + + ScheduleUpdateOptions updateOptions = new ScheduleUpdateOptions + { + OrchestrationName = updateScheduleRequest.OrchestrationName, + OrchestrationInput = updateScheduleRequest.Input, + StartAt = updateScheduleRequest.StartAt, + EndAt = updateScheduleRequest.EndAt, + Interval = updateScheduleRequest.Interval + }; + + await handle.UpdateAsync(updateOptions); + return this.Ok(await handle.DescribeAsync()); + } + catch (ScheduleNotFoundException) + { + return this.NotFound(); + } + catch (ScheduleClientValidationException ex) + { + this.logger.LogError(ex, "Validation failed while updating schedule {ScheduleId}", id); + return this.BadRequest(ex.Message); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error updating schedule {ScheduleId}", id); + return this.StatusCode(500, "An error occurred while updating the schedule"); + } + } + + /// + /// Deletes a schedule by its ID. + /// + /// The ID of the schedule to delete. + /// No content if successful. + [HttpDelete("{id}")] + public async Task DeleteSchedule(string id) + { + try + { + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + await handle.DeleteAsync(); + return this.NoContent(); + } + catch (ScheduleNotFoundException) + { + return this.NotFound(); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error deleting schedule {ScheduleId}", id); + return this.StatusCode(500, "An error occurred while deleting the schedule"); + } + } + + /// + /// Pauses a running schedule. + /// + /// The ID of the schedule to pause. + /// The updated schedule description. + [HttpPost("{id}/pause")] + public async Task> PauseSchedule(string id) + { + try + { + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + await handle.PauseAsync(); + return this.Ok(await handle.DescribeAsync()); + } + catch (ScheduleNotFoundException) + { + return this.NotFound(); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error pausing schedule {ScheduleId}", id); + return this.StatusCode(500, "An error occurred while pausing the schedule"); + } + } + + /// + /// Resumes a paused schedule. + /// + /// The ID of the schedule to resume. + /// The updated schedule description. + [HttpPost("{id}/resume")] + public async Task> ResumeSchedule(string id) + { + try + { + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + await handle.ResumeAsync(); + return this.Ok(await handle.DescribeAsync()); + } + catch (ScheduleNotFoundException) + { + return this.NotFound(); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error resuming schedule {ScheduleId}", id); + return this.StatusCode(500, "An error occurred while resuming the schedule"); + } + } +} \ No newline at end of file diff --git a/samples/ScheduleWebApp/ScheduleWebApp.csproj b/samples/ScheduleWebApp/ScheduleWebApp.csproj new file mode 100644 index 00000000..8206fa42 --- /dev/null +++ b/samples/ScheduleWebApp/ScheduleWebApp.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + true + $(BaseIntermediateOutputPath)Generated + + + + + + + + + + + + + + diff --git a/samples/ScheduleWebApp/ScheduleWebApp.http b/samples/ScheduleWebApp/ScheduleWebApp.http new file mode 100644 index 00000000..e69de29b diff --git a/samples/ScheduleWebApp/appsettings.Development.json b/samples/ScheduleWebApp/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/ScheduleWebApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ScheduleWebApp/appsettings.json b/samples/ScheduleWebApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/ScheduleWebApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 0c5429f714fa3047efb986decdf65c777ce1a0fa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:55:06 -0800 Subject: [PATCH 132/203] add http client file for sample --- samples/ScheduleWebApp/ScheduleWebApp.http | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/samples/ScheduleWebApp/ScheduleWebApp.http b/samples/ScheduleWebApp/ScheduleWebApp.http index e69de29b..8915bbb4 100644 --- a/samples/ScheduleWebApp/ScheduleWebApp.http +++ b/samples/ScheduleWebApp/ScheduleWebApp.http @@ -0,0 +1,57 @@ +### Variables +@baseUrl = http://localhost:5008 +@scheduleId = schedule-123 + +### Create a new schedule +# @name createSchedule +POST {{baseUrl}}/schedules +Content-Type: application/json + +{ + "id": "{{scheduleId}}", + "orchestrationName": "CacheClearingOrchestrator", + "input": "{{scheduleId}}", + "interval": "PT1H", + "startAt": "2024-01-01T00:00:00Z", + "endAt": "2024-12-31T23:59:59Z" +} + +### Get a specific schedule by ID +GET {{baseUrl}}/schedules/{{scheduleId}} + +### List all schedules +GET {{baseUrl}}/schedules/list + +### Update an existing schedule +PUT {{baseUrl}}/schedules/{{scheduleId}} +Content-Type: application/json + +{ + "orchestrationName": "UpdatedTaskName", + "interval": "PT30M", + "input": { + "taskName": "ProcessData", + "parameters": { + "batchSize": 200 + } + }, + "startAt": "2024-01-01T00:00:00Z", + "endAt": "2024-12-31T23:59:59Z" +} + +### Pause a schedule +POST {{baseUrl}}/schedules/{{scheduleId}}/pause + +### Resume a schedule +POST {{baseUrl}}/schedules/{{scheduleId}}/resume + +### Delete a schedule +DELETE {{baseUrl}}/schedules/{{scheduleId}} + +### Tips: +# - Replace the baseUrl variable if your application runs on a different port +# - The scheduleId variable can be changed to test different schedule instances +# - Time intervals use ISO 8601 format (PT1H = 1 hour, PT30M = 30 minutes) +# - Dates are in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ) +# - You can use the REST Client extension in VS Code to execute these requests +# - The @name directive allows referencing the response in subsequent requests From f4f0b95277b81ab9231a1cc8a5d8363959088908 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:58:13 -0800 Subject: [PATCH 133/203] update --- samples/ScheduleWebApp/ScheduleWebApp.http | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/samples/ScheduleWebApp/ScheduleWebApp.http b/samples/ScheduleWebApp/ScheduleWebApp.http index 8915bbb4..497cc088 100644 --- a/samples/ScheduleWebApp/ScheduleWebApp.http +++ b/samples/ScheduleWebApp/ScheduleWebApp.http @@ -9,11 +9,9 @@ Content-Type: application/json { "id": "{{scheduleId}}", - "orchestrationName": "CacheClearingOrchestrator", + "orchestrationName": "CacheClearingOrchestrator", "input": "{{scheduleId}}", - "interval": "PT1H", - "startAt": "2024-01-01T00:00:00Z", - "endAt": "2024-12-31T23:59:59Z" + "interval": "PT10S" } ### Get a specific schedule by ID @@ -27,16 +25,7 @@ PUT {{baseUrl}}/schedules/{{scheduleId}} Content-Type: application/json { - "orchestrationName": "UpdatedTaskName", - "interval": "PT30M", - "input": { - "taskName": "ProcessData", - "parameters": { - "batchSize": 200 - } - }, - "startAt": "2024-01-01T00:00:00Z", - "endAt": "2024-12-31T23:59:59Z" + "interval": "PT20S" } ### Pause a schedule From 14edf509e8581a1050f73225ba67611a4d5f0f5c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:03:37 -0800 Subject: [PATCH 134/203] save --- .../ScheduleWebApp/Properties/launchSettings.json | 3 ++- samples/ScheduleWebApp/ScheduleWebApp.http | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/samples/ScheduleWebApp/Properties/launchSettings.json b/samples/ScheduleWebApp/Properties/launchSettings.json index 410861e4..1e6c9bf4 100644 --- a/samples/ScheduleWebApp/Properties/launchSettings.json +++ b/samples/ScheduleWebApp/Properties/launchSettings.json @@ -14,7 +14,8 @@ "applicationUrl": "http://localhost:5008", "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=https://wbtestschedule01-g7fvgkfhe4ac.eastus2euap.durabletask.io;TaskHub=wbtestschedule01th01;Authentication=DefaultAzure" } } } diff --git a/samples/ScheduleWebApp/ScheduleWebApp.http b/samples/ScheduleWebApp/ScheduleWebApp.http index 497cc088..2b3ec439 100644 --- a/samples/ScheduleWebApp/ScheduleWebApp.http +++ b/samples/ScheduleWebApp/ScheduleWebApp.http @@ -9,9 +9,9 @@ Content-Type: application/json { "id": "{{scheduleId}}", - "orchestrationName": "CacheClearingOrchestrator", + "orchestrationName": "CacheClearingOrchestrator", "input": "{{scheduleId}}", - "interval": "PT10S" + "interval": "00:00:10" } ### Get a specific schedule by ID @@ -25,7 +25,7 @@ PUT {{baseUrl}}/schedules/{{scheduleId}} Content-Type: application/json { - "interval": "PT20S" + "interval": "00:00:20" } ### Pause a schedule @@ -40,7 +40,12 @@ DELETE {{baseUrl}}/schedules/{{scheduleId}} ### Tips: # - Replace the baseUrl variable if your application runs on a different port # - The scheduleId variable can be changed to test different schedule instances -# - Time intervals use ISO 8601 format (PT1H = 1 hour, PT30M = 30 minutes) +# - Time intervals use TimeSpan format (hh:mm:ss) +# Examples: +# - "00:00:10" = 10 seconds +# - "00:30:00" = 30 minutes +# - "01:00:00" = 1 hour +# - "24:00:00" = 1 day # - Dates are in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ) # - You can use the REST Client extension in VS Code to execute these requests # - The @name directive allows referencing the response in subsequent requests From ce7a4d61c231c205fec640da74c5a4c65db2efaf Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:12:39 -0800 Subject: [PATCH 135/203] finalize schedule webapi sample --- samples/ScheduleWebApp/ScheduleController.cs | 4 ++-- samples/ScheduleWebApp/ScheduleWebApp.http | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index c56a2953..d569c013 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -61,7 +61,7 @@ public async Task> CreateSchedule([FromBody] C this.logger.LogInformation("Created new schedule with ID: {ScheduleId}", createScheduleRequest.Id); - return this.CreatedAtAction(nameof(GetScheduleAsync), new { id = createScheduleRequest.Id }, description); + return this.CreatedAtAction(nameof(GetSchedule), new { id = createScheduleRequest.Id }, description); } catch (ScheduleClientValidationException ex) { @@ -81,7 +81,7 @@ public async Task> CreateSchedule([FromBody] C /// The ID of the schedule to retrieve. /// The schedule description if found. [HttpGet("{id}")] - public async Task> GetScheduleAsync(string id) + public async Task> GetSchedule(string id) { try { diff --git a/samples/ScheduleWebApp/ScheduleWebApp.http b/samples/ScheduleWebApp/ScheduleWebApp.http index 2b3ec439..f5672e1b 100644 --- a/samples/ScheduleWebApp/ScheduleWebApp.http +++ b/samples/ScheduleWebApp/ScheduleWebApp.http @@ -15,6 +15,8 @@ Content-Type: application/json } ### Get a specific schedule by ID +# Note: This endpoint can be used to verify the schedule was created +# The ID in the URL should match the id used in create request GET {{baseUrl}}/schedules/{{scheduleId}} ### List all schedules @@ -25,6 +27,7 @@ PUT {{baseUrl}}/schedules/{{scheduleId}} Content-Type: application/json { + "orchestrationName": "CacheClearingOrchestrator", "interval": "00:00:20" } From 291a6ca43b458b0184bab447115a2f00469221d9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:10:09 -0800 Subject: [PATCH 136/203] partial fb addressed --- .../Orchestrators/StockPriceOrchestrator.cs | 2 +- samples/ScheduleConsoleApp/Program.cs | 37 ++++++------------- samples/ScheduleWebApp/Program.cs | 4 +- .../Properties/launchSettings.json | 2 +- .../Client/ScheduledTaskClient.cs | 17 ++------- .../ScheduleAlreadyExistException.cs | 2 +- .../ScheduleClientValidationException.cs | 2 +- .../ScheduleInvalidTransitionException.cs | 2 +- .../Exception/ScheduleNotFoundException.cs | 2 +- .../DurableTaskClientBuilderExtensions.cs | 4 +- .../DurableTaskSchedulerWorkerExtensions.cs | 4 +- .../{Client/Logs.cs => Logs.Client.cs} | 2 +- .../{Entity/Logs.cs => Logs.Entity.cs} | 2 +- .../Models/ScheduleDescription.cs | 16 -------- src/ScheduledTasks/ScheduledTasks.csproj | 2 +- 15 files changed, 29 insertions(+), 71 deletions(-) rename src/ScheduledTasks/Logging/{Client/Logs.cs => Logs.Client.cs} (98%) rename src/ScheduledTasks/Logging/{Entity/Logs.cs => Logs.Entity.cs} (98%) diff --git a/samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs b/samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs index 27852813..3cc04a60 100644 --- a/samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs +++ b/samples/ScheduleConsoleApp/Orchestrators/StockPriceOrchestrator.cs @@ -14,7 +14,7 @@ public override async Task RunAsync(TaskOrchestrationContext context, st try { // Get current stock price - decimal currentPrice = await context.CallActivityAsync("GetStockPrice", symbol); + decimal currentPrice = await context.CallGetStockPriceAsync(symbol); logger.LogInformation("Current price for {symbol} is ${price:F2}", symbol, currentPrice); diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index da51be6d..6e4d7b23 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -25,14 +25,14 @@ // Enable scheduled tasks support builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); + builder.UseScheduledTasks(); }); // Configure the client services.AddDurableTaskClient(builder => { builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); + builder.UseScheduledTasks(); }); // Configure console logging @@ -62,28 +62,15 @@ AsyncPageable schedules = await scheduledTaskClient.ListScheduleIdsAsync(query); // Initialize the continuation token - string? continuationToken = null; - await foreach (Page page in schedules.AsPages(continuationToken)) + await foreach (string scheduleId in schedules) { - foreach (string scheduleId in page.Values) - { - // Obtain the schedule handle for the current scheduleId - IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); - - // Delete the schedule - await handle.DeleteAsync(); + // Obtain the schedule handle for the current scheduleId + IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); - Console.WriteLine($"Deleted schedule {scheduleId}"); - } + // Delete the schedule + await handle.DeleteAsync(); - // Update the continuation token for the next iteration - continuationToken = page.ContinuationToken; - - // If there's no continuation token, we've reached the end of the collection - if (continuationToken == null) - { - break; - } + Console.WriteLine($"Deleted schedule {scheduleId}"); } @@ -101,7 +88,7 @@ ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); // print the schedule description - Console.WriteLine(scheduleDescription.ToJsonString(true)); + Console.WriteLine(scheduleDescription); Console.WriteLine(""); Console.WriteLine(""); @@ -111,7 +98,7 @@ Console.WriteLine("\nPausing schedule..."); await scheduleHandle.PauseAsync(); scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription.ToJsonString(true)); + Console.WriteLine(scheduleDescription); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine(""); @@ -121,15 +108,13 @@ Console.WriteLine("\nResuming schedule..."); await scheduleHandle.ResumeAsync(); scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription.ToJsonString(true)); + Console.WriteLine(scheduleDescription); Console.WriteLine(""); Console.WriteLine(""); Console.WriteLine(""); await Task.Delay(TimeSpan.FromMinutes(30)); - //Console.WriteLine("\nPress any key to delete the schedule and exit..."); - //Console.ReadKey(); } catch (Exception ex) { diff --git a/samples/ScheduleWebApp/Program.cs b/samples/ScheduleWebApp/Program.cs index 630602f4..f6837ec2 100644 --- a/samples/ScheduleWebApp/Program.cs +++ b/samples/ScheduleWebApp/Program.cs @@ -23,14 +23,14 @@ r.AddOrchestrator(); }); builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); + builder.UseScheduledTasks(); }); // Register the client, which can be used to start orchestrations builder.Services.AddDurableTaskClient(builder => { builder.UseDurableTaskScheduler(connectionString); - builder.EnableScheduledTasksSupport(); + builder.UseScheduledTasks(); }); // Configure console logging using the simpler, more compact format diff --git a/samples/ScheduleWebApp/Properties/launchSettings.json b/samples/ScheduleWebApp/Properties/launchSettings.json index 1e6c9bf4..f2acfbff 100644 --- a/samples/ScheduleWebApp/Properties/launchSettings.json +++ b/samples/ScheduleWebApp/Properties/launchSettings.json @@ -15,7 +15,7 @@ "dotnetRunMessages": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=https://wbtestschedule01-g7fvgkfhe4ac.eastus2euap.durabletask.io;TaskHub=wbtestschedule01th01;Authentication=DefaultAzure" + "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "" } } } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 34470886..6add39b4 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -11,21 +11,10 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Client for managing scheduled tasks in a Durable Task application. /// -public class ScheduledTaskClient : IScheduledTaskClient +public class ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) : IScheduledTaskClient { - readonly DurableTaskClient durableTaskClient; - readonly ILogger logger; - - /// - /// Initializes a new instance of the class. - /// - /// The Durable Task client to use for orchestration operations. - /// logger. - public ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) - { - this.durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); - this.logger = Check.NotNull(logger, nameof(logger)); - } + readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); + readonly ILogger logger = Check.NotNull(logger, nameof(logger)); /// public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) diff --git a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs index e05955ca..a4cc33cc 100644 --- a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs +++ b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Exception thrown when attempting to create a schedule with an ID that already exists. /// -public class ScheduleAlreadyExistException : Exception +public class ScheduleAlreadyExistException : InvalidOperationException { /// /// Initializes a new instance of the class. diff --git a/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs index 263e7902..d38e6a3c 100644 --- a/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs +++ b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Exception thrown when client-side validation fails for schedule operations. /// -public class ScheduleClientValidationException : Exception +public class ScheduleClientValidationException : InvalidOperationException { /// /// Initializes a new instance of the class. diff --git a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs index 2ff1224b..7d769eee 100644 --- a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs +++ b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Exception thrown when an invalid state transition is attempted on a schedule. /// -public class ScheduleInvalidTransitionException : Exception +public class ScheduleInvalidTransitionException : InvalidOperationException { /// /// Initializes a new instance of the class. diff --git a/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs index 24334008..e884b6b8 100644 --- a/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs +++ b/src/ScheduledTasks/Exception/ScheduleNotFoundException.cs @@ -6,7 +6,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Exception thrown when attempting to access a schedule that does not exist. /// -public class ScheduleNotFoundException : Exception +public class ScheduleNotFoundException : InvalidOperationException { /// /// Initializes a new instance of the class. diff --git a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs index 22881441..4f8c9b67 100644 --- a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs @@ -13,11 +13,11 @@ namespace Microsoft.DurableTask.ScheduledTasks; public static class DurableTaskClientBuilderExtensions { /// - /// Enables scheduled task support for the client builder. + /// Enables scheduled tasks support for the client builder. /// /// The client builder to add scheduled task support to. /// The original builder, for call chaining. - public static IDurableTaskClientBuilder EnableScheduledTasksSupport(this IDurableTaskClientBuilder builder) + public static IDurableTaskClientBuilder UseScheduledTasks(this IDurableTaskClientBuilder builder) { builder.Services.AddTransient(sp => { diff --git a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs index 8cb803c9..aa75fa6b 100644 --- a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs @@ -11,10 +11,10 @@ namespace Microsoft.DurableTask.ScheduledTasks; public static class DurableTaskSchedulerWorkerExtensions { /// - /// Adds scheduled task support to the worker builder. + /// Adds scheduled tasks support to the worker builder. /// /// The worker builder to add scheduled task support to. - public static void EnableScheduledTasksSupport(this IDurableTaskWorkerBuilder builder) + public static void UseScheduledTasks(this IDurableTaskWorkerBuilder builder) { builder.AddTasks(r => { diff --git a/src/ScheduledTasks/Logging/Client/Logs.cs b/src/ScheduledTasks/Logging/Logs.Client.cs similarity index 98% rename from src/ScheduledTasks/Logging/Client/Logs.cs rename to src/ScheduledTasks/Logging/Logs.Client.cs index 36817e9f..6a289959 100644 --- a/src/ScheduledTasks/Logging/Client/Logs.cs +++ b/src/ScheduledTasks/Logging/Logs.Client.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Extensions.Logging; diff --git a/src/ScheduledTasks/Logging/Entity/Logs.cs b/src/ScheduledTasks/Logging/Logs.Entity.cs similarity index 98% rename from src/ScheduledTasks/Logging/Entity/Logs.cs rename to src/ScheduledTasks/Logging/Logs.Entity.cs index cad2269c..4278cc03 100644 --- a/src/ScheduledTasks/Logging/Entity/Logs.cs +++ b/src/ScheduledTasks/Logging/Logs.Entity.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.Extensions.Logging; diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 1712500c..9975c376 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -69,20 +69,4 @@ public record ScheduleDescription /// Gets the next scheduled run time. /// public DateTimeOffset? NextRunAt { get; init; } - - /// - /// Returns a JSON string representation of the schedule description. - /// - /// If true, formats the JSON with indentation for readability. - /// A JSON string containing the schedule details. - public string ToJsonString(bool pretty = false) - { -#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances - JsonSerializerOptions options = new JsonSerializerOptions - { - WriteIndented = pretty, - }; -#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances - return System.Text.Json.JsonSerializer.Serialize(this, options); - } } diff --git a/src/ScheduledTasks/ScheduledTasks.csproj b/src/ScheduledTasks/ScheduledTasks.csproj index a6f8f871..a9951609 100644 --- a/src/ScheduledTasks/ScheduledTasks.csproj +++ b/src/ScheduledTasks/ScheduledTasks.csproj @@ -10,7 +10,7 @@ - + From d6b5e2aa90613bc2cb847d953fa1810d493fcc14 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:25:11 -0800 Subject: [PATCH 137/203] more fb address --- samples/ScheduleConsoleApp/Program.cs | 2 +- src/ScheduledTasks/Client/IScheduledTaskClient.cs | 2 +- src/ScheduledTasks/Client/ScheduleHandle.cs | 6 +++--- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index 6e4d7b23..e248411a 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -82,7 +82,7 @@ }; // Create the schedule and get a handle to it - ScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); // Get the schedule description ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 761e5745..4cc994fd 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -44,5 +44,5 @@ public interface IScheduledTaskClient /// The options for creating the schedule. /// Optional cancellation token. /// A handle to the created schedule. - Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); + Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index 880bb138..e45aa654 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -11,7 +11,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// -public class ScheduleHandle : IScheduleHandle +class ScheduleHandle : IScheduleHandle { readonly DurableTaskClient durableTaskClient; readonly ILogger logger; @@ -39,7 +39,7 @@ public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logge /// /// Gets the entity ID of the schedule. /// - public EntityInstanceId EntityId { get; } + EntityInstanceId EntityId { get; } /// public async Task DescribeAsync(CancellationToken cancellation = default) @@ -165,8 +165,8 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationT { try { - this.logger.ClientUpdatingSchedule(this.ScheduleId); Check.NotNull(updateOptions, nameof(updateOptions)); + this.logger.ClientUpdatingSchedule(this.ScheduleId); ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.UpdateSchedule), updateOptions); string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 6add39b4..c1e3710c 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -17,7 +17,7 @@ public class ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger lo readonly ILogger logger = Check.NotNull(logger, nameof(logger)); /// - public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) { Check.NotNull(creationOptions, nameof(creationOptions)); this.logger.ClientCreatingSchedule(creationOptions); From 3267641e90b83042199358af71964bab0e4dfce5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:41:01 -0800 Subject: [PATCH 138/203] minimal --- samples/ScheduleConsoleApp/Program.cs | 71 +++++++++---------- .../ScheduleConsoleApp.csproj | 2 +- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index e248411a..8a9bf405 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -10,44 +10,41 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -// Create the host builder -IHost host = Host.CreateDefaultBuilder(args) - .ConfigureServices(services => +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); + +// Configure the worker +builder.Services.AddDurableTaskWorker(builder => +{ + // Add the Schedule entity and demo orchestration + builder.AddTasks(r => r.AddAllGeneratedTasks()); + + // Enable scheduled tasks support + builder.UseDurableTaskScheduler(connectionString); + builder.UseScheduledTasks(); +}); + +// Configure the client +builder.Services.AddDurableTaskClient(builder => +{ + builder.UseDurableTaskScheduler(connectionString); + builder.UseScheduledTasks(); +}); + +// Configure console logging +builder.Services.AddLogging(logging => +{ + logging.AddSimpleConsole(options => { - string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") - ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); - - // Configure the worker - _ = services.AddDurableTaskWorker(builder => - { - // Add the Schedule entity and demo orchestration - builder.AddTasks(r => r.AddAllGeneratedTasks()); - - // Enable scheduled tasks support - builder.UseDurableTaskScheduler(connectionString); - builder.UseScheduledTasks(); - }); - - // Configure the client - services.AddDurableTaskClient(builder => - { - builder.UseDurableTaskScheduler(connectionString); - builder.UseScheduledTasks(); - }); - - // Configure console logging - services.AddLogging(logging => - { - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); - }); - }) - .Build(); + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); +}); +IHost host = builder.Build(); await host.StartAsync(); IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); @@ -73,7 +70,6 @@ Console.WriteLine($"Deleted schedule {scheduleId}"); } - // Create schedule options that runs every 4 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) { @@ -103,7 +99,6 @@ Console.WriteLine(""); Console.WriteLine(""); - // Resume the schedule Console.WriteLine("\nResuming schedule..."); await scheduleHandle.ResumeAsync(); diff --git a/samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj b/samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj index 09d6050b..7efdf0fc 100644 --- a/samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj +++ b/samples/ScheduleConsoleApp/ScheduleConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable From aed2a48c0e1a82622e4220140429709f7013e7c9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:52:19 -0800 Subject: [PATCH 139/203] config --- samples/ScheduleConsoleApp/Program.cs | 6 ++++-- samples/ScheduleWebApp/Program.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index 8a9bf405..af4a3200 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -6,14 +6,16 @@ using Microsoft.DurableTask.ScheduledTasks; using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -string connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") - ?? throw new InvalidOperationException("Missing required environment variable 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); +// Get configuration +string connectionString = builder.Configuration.GetValue("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); // Configure the worker builder.Services.AddDurableTaskWorker(builder => diff --git a/samples/ScheduleWebApp/Program.cs b/samples/ScheduleWebApp/Program.cs index f6837ec2..3495af0b 100644 --- a/samples/ScheduleWebApp/Program.cs +++ b/samples/ScheduleWebApp/Program.cs @@ -11,7 +11,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -string connectionString = builder.Configuration["DURABLE_TASK_SCHEDULER_CONNECTION_STRING"] +string connectionString = builder.Configuration.GetValue("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_CONNECTION_STRING'"); // Add all the generated orchestrations and activities automatically From 8853e14eafd2448be1921f244cadff6ab728d64c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:02:26 -0800 Subject: [PATCH 140/203] fb --- samples/ScheduleConsoleApp/Program.cs | 71 +--------------- .../ScheduleConsoleApp/ScheduleOperations.cs | 81 +++++++++++++++++++ 2 files changed, 85 insertions(+), 67 deletions(-) create mode 100644 samples/ScheduleConsoleApp/ScheduleOperations.cs diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index af4a3200..01469967 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using ScheduleConsoleApp; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); @@ -49,73 +50,9 @@ IHost host = builder.Build(); await host.StartAsync(); +// Run the schedule operations IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); - -try -{ - // list all schedules - // Define the initial query with the desired page size - ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; - - // Retrieve the pageable collection of schedule IDs - AsyncPageable schedules = await scheduledTaskClient.ListScheduleIdsAsync(query); - - // Initialize the continuation token - await foreach (string scheduleId in schedules) - { - // Obtain the schedule handle for the current scheduleId - IScheduleHandle handle = scheduledTaskClient.GetScheduleHandle(scheduleId); - - // Delete the schedule - await handle.DeleteAsync(); - - Console.WriteLine($"Deleted schedule {scheduleId}"); - } - - // Create schedule options that runs every 4 seconds - ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions("demo-schedule101", nameof(StockPriceOrchestrator), TimeSpan.FromSeconds(4)) - { - StartAt = DateTimeOffset.UtcNow, - OrchestrationInput = "MSFT" - }; - - // Create the schedule and get a handle to it - IScheduleHandle scheduleHandle = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); - - // Get the schedule description - ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); - - // print the schedule description - Console.WriteLine(scheduleDescription); - - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - // Pause the schedule - Console.WriteLine("\nPausing schedule..."); - await scheduleHandle.PauseAsync(); - scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription); - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - // Resume the schedule - Console.WriteLine("\nResuming schedule..."); - await scheduleHandle.ResumeAsync(); - scheduleDescription = await scheduleHandle.DescribeAsync(); - Console.WriteLine(scheduleDescription); - - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine(""); - - await Task.Delay(TimeSpan.FromMinutes(30)); -} -catch (Exception ex) -{ - Console.WriteLine($"One of your schedule operations failed, please fix and rerun: {ex.Message}"); -} +ScheduleOperations scheduleOperations = new ScheduleOperations(scheduledTaskClient); +await scheduleOperations.RunAsync(); await host.StopAsync(); \ No newline at end of file diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs new file mode 100644 index 00000000..c917330e --- /dev/null +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; +using Microsoft.DurableTask.ScheduledTasks; + +namespace ScheduleConsoleApp; + +class ScheduleOperations(IScheduledTaskClient scheduledTaskClient) +{ + readonly IScheduledTaskClient scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); + + public async Task RunAsync() + { + try + { + await this.DeleteExistingSchedulesAsync(); + await this.CreateAndManageScheduleAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"One of your schedule operations failed, please fix and rerun: {ex.Message}"); + } + } + + async Task DeleteExistingSchedulesAsync() + { + // Define the initial query with the desired page size + ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; + + // Retrieve the pageable collection of schedule IDs + AsyncPageable schedules = await this.scheduledTaskClient.ListScheduleIdsAsync(query); + + // Delete each existing schedule + await foreach (string scheduleId in schedules) + { + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(scheduleId); + await handle.DeleteAsync(); + Console.WriteLine($"Deleted schedule {scheduleId}"); + } + } + + async Task CreateAndManageScheduleAsync() + { + // Create schedule options that runs every 4 seconds + ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions( + "demo-schedule101", + nameof(StockPriceOrchestrator), + TimeSpan.FromSeconds(4)) + { + StartAt = DateTimeOffset.UtcNow, + OrchestrationInput = "MSFT" + }; + + // Create the schedule and get a handle to it + IScheduleHandle scheduleHandle = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + + // Get and print the initial schedule description + await PrintScheduleDescriptionAsync(scheduleHandle); + + // Pause the schedule + Console.WriteLine("\nPausing schedule..."); + await scheduleHandle.PauseAsync(); + await PrintScheduleDescriptionAsync(scheduleHandle); + + // Resume the schedule + Console.WriteLine("\nResuming schedule..."); + await scheduleHandle.ResumeAsync(); + await PrintScheduleDescriptionAsync(scheduleHandle); + + // Wait for a while to let the schedule run + await Task.Delay(TimeSpan.FromMinutes(30)); + } + + static async Task PrintScheduleDescriptionAsync(IScheduleHandle scheduleHandle) + { + ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); + Console.WriteLine(scheduleDescription); + Console.WriteLine("\n\n"); + } +} \ No newline at end of file From aeb7c6e7ba8d01a7ba813ade0e3890eb40f38eea Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:10:44 -0800 Subject: [PATCH 141/203] cleanup --- .../ScheduleConfigurationCreateOptions.cs | 90 ------------------- .../ScheduleConfigurationUpdateOptions.cs | 77 ---------------- 2 files changed, 167 deletions(-) delete mode 100644 src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs delete mode 100644 src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs deleted file mode 100644 index 38becc40..00000000 --- a/src/ScheduledTasks/Models/ScheduleConfigurationCreateOptions.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Configuration options for creating a scheduled task. -/// -public class ScheduleConfigurationCreateOptions -{ - string orchestrationName; - TimeSpan? interval; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the orchestration to schedule. - /// Optional ID for the schedule. If not provided, a new GUID will be generated. - public ScheduleConfigurationCreateOptions(string orchestrationName, string scheduleId) - { - this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); - this.ScheduleId = scheduleId ?? Guid.NewGuid().ToString("N"); - } - - /// - /// Gets or sets the name of the orchestration to schedule. - /// - public string OrchestrationName - { - get => this.orchestrationName; - set => this.orchestrationName = Check.NotNullOrEmpty(value, nameof(value)); - } - - /// - /// Gets the unique identifier for this schedule. - /// - public string ScheduleId { get; init; } - - /// - /// Gets or sets the input data to pass to the orchestration. - /// - public string? OrchestrationInput { get; set; } - - /// - /// Gets or sets the instance ID for the orchestration. Defaults to a new GUID. - /// - public string? OrchestrationInstanceId { get; set; } = Guid.NewGuid().ToString("N"); - - /// - /// Gets or sets when the schedule should start. - /// - public DateTimeOffset? StartAt { get; set; } - - /// - /// Gets or sets when the schedule should end. - /// - public DateTimeOffset? EndAt { get; set; } - - /// - /// Gets or sets the time interval between schedule executions. Must be at least 1 second. - /// - public TimeSpan? Interval - { - get => this.interval; - set - { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } - - this.interval = value; - } - } - - /// - /// Gets or sets whether to start immediately if the schedule is already late. - /// - public bool? StartImmediatelyIfLate { get; set; } -} diff --git a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs deleted file mode 100644 index fca78a21..00000000 --- a/src/ScheduledTasks/Models/ScheduleConfigurationUpdateOptions.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Configuration options for updating a scheduled task. -/// -public class ScheduleConfigurationUpdateOptions -{ - string? orchestrationName; - TimeSpan? interval; - - /// - /// Gets or sets the name of the orchestration to schedule. - /// - public string? OrchestrationName - { - get => this.orchestrationName; - set - { - this.orchestrationName = value; - } - } - - /// - /// Gets or sets the input data to pass to the orchestration. - /// - public string? OrchestrationInput { get; set; } - - /// - /// Gets or sets the instance ID for the orchestration. - /// - public string? OrchestrationInstanceId { get; set; } - - /// - /// Gets or sets when the schedule should start. - /// - public DateTimeOffset? StartAt { get; set; } - - /// - /// Gets or sets when the schedule should end. - /// - public DateTimeOffset? EndAt { get; set; } - - /// - /// Gets or sets the time interval between schedule executions. Must be at least 1 second. - /// - public TimeSpan? Interval - { - get => this.interval; - set - { - if (!value.HasValue) - { - return; - } - - if (value.Value <= TimeSpan.Zero) - { - throw new ArgumentException("Interval must be positive", nameof(value)); - } - - if (value.Value.TotalSeconds < 1) - { - throw new ArgumentException("Interval must be at least 1 second", nameof(value)); - } - - this.interval = value; - } - } - - /// - /// Gets or sets whether to start immediately if the schedule is already late. - /// - public bool? StartImmediatelyIfLate { get; set; } -} From 072092267b5a1f4d800173e236ef5ee8e52d4f20 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:35:43 -0800 Subject: [PATCH 142/203] save --- .../ScheduleClientValidationException.cs | 12 ++++++++++++ .../ScheduleInvalidTransitionException.cs | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs index d38e6a3c..98a1a663 100644 --- a/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs +++ b/src/ScheduledTasks/Exception/ScheduleClientValidationException.cs @@ -19,6 +19,18 @@ public ScheduleClientValidationException(string scheduleId, string message) this.ScheduleId = scheduleId; } + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule that failed validation. + /// The validation error message. + /// The exception that is the cause of the current exception. + public ScheduleClientValidationException(string scheduleId, string message, Exception innerException) + : base($"Validation failed for schedule '{scheduleId}': {message}", innerException) + { + this.ScheduleId = scheduleId; + } + /// /// Gets the ID of the schedule that failed validation. /// diff --git a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs index 7d769eee..b95e3824 100644 --- a/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs +++ b/src/ScheduledTasks/Exception/ScheduleInvalidTransitionException.cs @@ -24,6 +24,23 @@ public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus from this.OperationName = operationName; } + /// + /// Initializes a new instance of the class. + /// + /// The ID of the schedule on which the invalid transition was attempted. + /// The current status of the schedule. + /// The target status that was invalid. + /// The name of the operation that was attempted. + /// The exception that is the cause of the current exception. + public ScheduleInvalidTransitionException(string scheduleId, ScheduleStatus fromStatus, ScheduleStatus toStatus, string operationName, Exception innerException) + : base($"Invalid state transition attempted for schedule '{scheduleId}': Cannot transition from {fromStatus} to {toStatus} during {operationName} operation.", innerException) + { + this.ScheduleId = scheduleId; + this.FromStatus = fromStatus; + this.ToStatus = toStatus; + this.OperationName = operationName; + } + /// /// Gets the ID of the schedule that encountered the invalid transition. /// From 280cb51864950e478b44cbc4398dc3ad333081ab Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:41:02 -0800 Subject: [PATCH 143/203] log --- src/ScheduledTasks/Entity/Schedule.cs | 2 +- src/ScheduledTasks/Logging/Logs.Entity.cs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 48f684c9..69e5ed72 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -83,7 +83,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche if (updatedScheduleConfigFields.Count == 0) { // no need to interrupt and update current schedule run as there is no change in the schedule config - this.logger.ScheduleOperationWarning(this.State.ScheduleConfiguration.ScheduleId, nameof(this.UpdateSchedule), "Schedule configuration is up to date."); + this.logger.ScheduleOperationDebug(this.State.ScheduleConfiguration.ScheduleId, nameof(this.UpdateSchedule), "Schedule configuration is up to date."); return; } diff --git a/src/ScheduledTasks/Logging/Logs.Entity.cs b/src/ScheduledTasks/Logging/Logs.Entity.cs index 4278cc03..0fb81c6e 100644 --- a/src/ScheduledTasks/Logging/Logs.Entity.cs +++ b/src/ScheduledTasks/Logging/Logs.Entity.cs @@ -49,15 +49,18 @@ static partial class Logs [LoggerMessage(EventId = 111, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is deleted")] public static partial void DeletedSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 112, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] + [LoggerMessage(EventId = 112, Level = LogLevel.Debug, Message = "Schedule '{scheduleId}' operation '{operationName}' debug: {debugMessage}")] + public static partial void ScheduleOperationDebug(this ILogger logger, string scheduleId, string operationName, string debugMessage); + + [LoggerMessage(EventId = 113, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' operation '{operationName}' info: {infoMessage}")] public static partial void ScheduleOperationInfo(this ILogger logger, string scheduleId, string operationName, string infoMessage); - [LoggerMessage(EventId = 113, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] + [LoggerMessage(EventId = 114, Level = LogLevel.Warning, Message = "Schedule '{scheduleId}' operation '{operationName}' warning: {warningMessage}")] public static partial void ScheduleOperationWarning(this ILogger logger, string scheduleId, string operationName, string warningMessage); - [LoggerMessage(EventId = 114, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] + [LoggerMessage(EventId = 115, Level = LogLevel.Error, Message = "Operation '{operationName}' failed for schedule '{scheduleId}': {errorMessage}")] public static partial void ScheduleOperationError(this ILogger logger, string scheduleId, string operationName, string errorMessage, Exception? exception = null); - [LoggerMessage(EventId = 115, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] + [LoggerMessage(EventId = 116, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' run cancelled with execution token '{executionToken}'")] public static partial void ScheduleRunCancelled(this ILogger logger, string scheduleId, string executionToken); } From c1f21d53c8e8f3d501924bb0430af03716a08120 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:45:46 -0800 Subject: [PATCH 144/203] throw --- src/ScheduledTasks/Entity/Schedule.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 69e5ed72..5871399c 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -114,6 +114,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.UpdateSchedule), "Failed to update schedule", ex); + throw; } } @@ -142,6 +143,7 @@ public void PauseSchedule(TaskEntityContext context) catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.PauseSchedule), "Failed to pause schedule", ex); + throw; } } @@ -171,6 +173,7 @@ public void ResumeSchedule(TaskEntityContext context) catch (Exception ex) { this.logger.ScheduleOperationError(this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.ResumeSchedule), "Failed to resume schedule", ex); + throw; } } From 9eaf9b61d2366d495bb707a228310277d8acf9ee Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:56:58 -0800 Subject: [PATCH 145/203] fb --- src/ScheduledTasks/Entity/Schedule.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 5871399c..49e0e624 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -185,7 +185,9 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { - ScheduleConfiguration scheduleConfig = Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); + ScheduleConfiguration scheduleConfig = + this.State.ScheduleConfiguration ?? + throw new InvalidOperationException("Schedule configuration is missing."); TimeSpan interval = scheduleConfig.Interval; if (executionToken != this.State.ExecutionToken) @@ -198,7 +200,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { string errorMessage = "Schedule must be in Active status to run."; Exception exception = new InvalidOperationException(errorMessage); - this.logger.ScheduleOperationError(scheduleConfig.ScheduleId, nameof(this.RunSchedule), errorMessage, exception); + this.logger.ScheduleOperationError(scheduleConfig.ScheduleId, nameof(this.RunSchedule), errorMessage); throw exception; } From 7a74e203e77e73c840b0cfb16bba8b0d6299e8bb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:10:15 -0800 Subject: [PATCH 146/203] fb --- src/ScheduledTasks/Client/ScheduleHandle.cs | 2 +- src/ScheduledTasks/Entity/Schedule.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/ScheduleHandle.cs index e45aa654..ed65a83a 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/ScheduleHandle.cs @@ -85,7 +85,7 @@ public async Task DescribeAsync(CancellationToken cancellat throw new OperationCanceledException( $"The {nameof(this.DescribeAsync)} operation was canceled.", - null, + ex, cancellation); } } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 49e0e624..dcb325d7 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -279,10 +279,9 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) bool CanTransitionTo(string operationName, ScheduleStatus targetStatus) { - HashSet validTargetStates; ScheduleStatus currentStatus = this.State.Status; - return ScheduleTransitions.TryGetValidTransitions(operationName, currentStatus, out validTargetStates) && + return ScheduleTransitions.TryGetValidTransitions(operationName, currentStatus, out HashSet validTargetStates) && validTargetStates.Contains(targetStatus); } From ea6befe9a7e2afaca99a0098092ad8e15675aab8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:26:03 -0800 Subject: [PATCH 147/203] save --- .../ScheduleConsoleApp/ScheduleOperations.cs | 8 ++--- .../Client/IScheduledTaskClient.cs | 8 ----- .../Client/ScheduledTaskClient.cs | 34 ------------------- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index c917330e..85109c86 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -29,14 +29,14 @@ async Task DeleteExistingSchedulesAsync() ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; // Retrieve the pageable collection of schedule IDs - AsyncPageable schedules = await this.scheduledTaskClient.ListScheduleIdsAsync(query); + AsyncPageable schedules = await this.scheduledTaskClient.ListSchedulesAsync(query); // Delete each existing schedule - await foreach (string scheduleId in schedules) + await foreach (ScheduleDescription schedule in schedules) { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(scheduleId); + IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(schedule.ScheduleId); await handle.DeleteAsync(); - Console.WriteLine($"Deleted schedule {scheduleId}"); + Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } } diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 4cc994fd..4a04126d 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -30,14 +30,6 @@ public interface IScheduledTaskClient /// A pageable list of schedule descriptions. Task> ListSchedulesAsync(ScheduleQuery? filter = null); - /// - /// Gets a pageable list of schedule IDs matching the specified filter criteria. - /// This is a more efficient version of ListSchedulesAsync when only the IDs are needed. - /// - /// Optional filter criteria for the schedules. If null, returns all schedule IDs. - /// A pageable list of schedule IDs. - Task> ListScheduleIdsAsync(ScheduleQuery? filter = null); - /// /// Creates a new schedule with the specified configuration. /// diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index c1e3710c..6b6fa1b5 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -190,40 +190,6 @@ public Task> ListSchedulesAsync(ScheduleQuery })); } - /// - public Task> ListScheduleIdsAsync(ScheduleQuery? filter = null) - { - EntityQuery query = new EntityQuery - { - InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), - IncludeState = false, // We don't need the state since we only want IDs - PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, - ContinuationToken = filter?.ContinuationToken, - }; - - // Create an async pageable using the Pageable.Create helper - return Task.FromResult(Pageable.Create(async (continuationToken, pageSize, cancellation) => - { - try - { - List scheduleIds = new List(); - - await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) - { - // Extract just the schedule ID from the entity ID - scheduleIds.Add(metadata.Id.Key); - } - - return new Page(scheduleIds, continuationToken); - } - catch (OperationCanceledException e) - { - throw new OperationCanceledException( - $"The {nameof(this.ListScheduleIdsAsync)} operation was canceled.", e, e.CancellationToken); - } - })); - } - /// /// Checks if a schedule with the specified ID exists. /// From de8163d17b4f931ab5f2a1b62b2671bc5d3b4679 Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 24 Feb 2025 15:55:01 -0800 Subject: [PATCH 148/203] Update src/ScheduledTasks/Client/ScheduledTaskClient.cs Co-authored-by: Jacob Viau --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 6b6fa1b5..4bca288c 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -122,7 +122,7 @@ public IScheduleHandle GetScheduleHandle(string scheduleId) } /// - public Task> ListSchedulesAsync(ScheduleQuery? filter = null) + public AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) { // TODO: map to entity query last modified from/to filters EntityQuery query = new EntityQuery From de71f2542a492a18565c0a1c426fec7806a4e282 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:55:31 -0800 Subject: [PATCH 149/203] pagefix --- .../Client/IScheduledTaskClient.cs | 2 +- .../Client/ScheduledTaskClient.cs | 95 +++++++++---------- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index 4a04126d..ebc9339a 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -28,7 +28,7 @@ public interface IScheduledTaskClient /// /// Optional filter criteria for the schedules. If null, returns all schedules. /// A pageable list of schedule descriptions. - Task> ListSchedulesAsync(ScheduleQuery? filter = null); + AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null); /// /// Creates a new schedule with the specified configuration. diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 4bca288c..11aefa39 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -124,70 +124,65 @@ public IScheduleHandle GetScheduleHandle(string scheduleId) /// public AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) { - // TODO: map to entity query last modified from/to filters - EntityQuery query = new EntityQuery - { - InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), - IncludeState = true, - PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, - ContinuationToken = filter?.ContinuationToken, - }; - // Create an async pageable using the Pageable.Create helper - return Task.FromResult(Pageable.Create(async (continuationToken, pageSize, cancellation) => + return Pageable.Create(async (continuationToken, pageSize, cancellation) => { try { - List schedules = new List(); + // TODO: map to entity query last modified from/to filters + EntityQuery query = new EntityQuery + { + InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), + IncludeState = true, + PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, + ContinuationToken = continuationToken, + }; + + // Get one page of entities + IAsyncEnumerable>> entityPages = + this.durableTaskClient.Entities.GetAllEntitiesAsync(query).AsPages(); - await foreach (EntityMetadata metadata in this.durableTaskClient.Entities.GetAllEntitiesAsync(query)) + await foreach (Page> entityPage in entityPages) { - ScheduleState state = metadata.State; - - // Skip if status filter is specified and doesn't match - if (filter?.Status.HasValue == true && state.Status != filter.Status.Value) - { - continue; - } - - // Skip if created time filter is specified and doesn't match - if (filter?.CreatedFrom.HasValue == true && state.ScheduleCreatedAt <= filter.CreatedFrom) - { - continue; - } - - if (filter?.CreatedTo.HasValue == true && state.ScheduleCreatedAt >= filter.CreatedTo) - { - continue; - } - - ScheduleConfiguration config = state.ScheduleConfiguration!; - - schedules.Add(new ScheduleDescription - { - ScheduleId = metadata.Id.Key, - OrchestrationName = config.OrchestrationName, - OrchestrationInput = config.OrchestrationInput, - OrchestrationInstanceId = config.OrchestrationInstanceId, - StartAt = config.StartAt, - EndAt = config.EndAt, - Interval = config.Interval, - StartImmediatelyIfLate = config.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }); + List schedules = entityPage.Values + .Where(metadata => + (!filter?.Status.HasValue ?? true || metadata.State.Status == filter.Status.Value) && + (filter?.CreatedFrom.HasValue != true || metadata.State.ScheduleCreatedAt > filter.CreatedFrom) && + (filter?.CreatedTo.HasValue != true || metadata.State.ScheduleCreatedAt < filter.CreatedTo)) + .Select(metadata => + { + ScheduleState state = metadata.State; + ScheduleConfiguration config = state.ScheduleConfiguration!; + return new ScheduleDescription + { + ScheduleId = metadata.Id.Key, + OrchestrationName = config.OrchestrationName, + OrchestrationInput = config.OrchestrationInput, + OrchestrationInstanceId = config.OrchestrationInstanceId, + StartAt = config.StartAt, + EndAt = config.EndAt, + Interval = config.Interval, + StartImmediatelyIfLate = config.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + }; + }) + .ToList(); + + return new Page(schedules, entityPage.ContinuationToken); } - return new Page(schedules, continuationToken); + // Return empty page if no results + return new Page(new List(), null); } catch (OperationCanceledException e) { throw new OperationCanceledException( $"The {nameof(this.ListSchedulesAsync)} operation was canceled.", e, e.CancellationToken); } - })); + }); } /// From bca682f5972d88da8bd3cd8637b9285a03641212 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:57:55 -0800 Subject: [PATCH 150/203] fix --- samples/ScheduleConsoleApp/ScheduleOperations.cs | 2 +- samples/ScheduleWebApp/ScheduleController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index 85109c86..dfdd077a 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -29,7 +29,7 @@ async Task DeleteExistingSchedulesAsync() ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; // Retrieve the pageable collection of schedule IDs - AsyncPageable schedules = await this.scheduledTaskClient.ListSchedulesAsync(query); + AsyncPageable schedules = this.scheduledTaskClient.ListSchedulesAsync(query); // Delete each existing schedule await foreach (ScheduleDescription schedule in schedules) diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index d569c013..ee4d1f6b 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -108,7 +108,7 @@ public async Task>> ListSchedules( { try { - AsyncPageable schedules = await this.scheduledTaskClient.ListSchedulesAsync(); + AsyncPageable schedules = this.scheduledTaskClient.ListSchedulesAsync(); // add schedule result list List scheduleList = new List(); From cd0f8a0b0908bdb1eda81be8976391df44e6c246 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:56:55 -0800 Subject: [PATCH 151/203] fix --- .../Client/ScheduledTaskClient.cs | 36 ------------------- .../Models/ScheduleCreationOptions.cs | 5 ++- 2 files changed, 4 insertions(+), 37 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 11aefa39..114fa2d7 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -27,13 +27,6 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions c string scheduleId = creationOptions.ScheduleId; EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - // Check if schedule already exists - bool scheduleExists = await this.CheckScheduleExists(scheduleId, cancellation); - if (scheduleExists) - { - throw new ScheduleAlreadyExistException(scheduleId); - } - // Call the orchestrator to create the schedule ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( @@ -184,33 +177,4 @@ public AsyncPageable ListSchedulesAsync(ScheduleQuery? filt } }); } - - /// - /// Checks if a schedule with the specified ID exists. - /// - /// The ID of the schedule to check. - /// Optional cancellation token. - /// True if the schedule exists, false otherwise. - async Task CheckScheduleExists(string scheduleId, CancellationToken cancellation = default) - { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - - try - { - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, false, cancellation); - - return metadata != null; - } - catch (OperationCanceledException e) - { - this.logger.ClientError( - nameof(this.CheckScheduleExists), - scheduleId, - e); - - throw new OperationCanceledException( - $"The {nameof(this.CheckScheduleExists)} operation was canceled.", e, e.CancellationToken); - } - } } diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 86b8f319..9a937504 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -67,7 +67,10 @@ public ScheduleCreationOptions(string scheduleId, string orchestrationName, Time public TimeSpan Interval { get; } /// - /// Gets a value indicating whether to start the schedule immediately if it is late. Default is false. + /// Gets a value indicating whether to start the orchestration immediately when the current time is past the StartAt time. + /// By default it is false. + /// If false, the first run will be scheduled at the next interval based on the original start time. + /// If true, the first run will start immediately and subsequent runs will follow the regular interval. /// public bool StartImmediatelyIfLate { get; init; } } From ae649c98cfaa90001734bb6c7b322f46bec29b66 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:14:10 -0800 Subject: [PATCH 152/203] createorupdate --- src/ScheduledTasks/Entity/Schedule.cs | 3 ++- src/ScheduledTasks/Models/ScheduleTransitions.cs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index dcb325d7..ad89b51c 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -20,7 +20,7 @@ class Schedule(ILogger logger) : TaskEntity readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// - /// Creates a new schedule with the specified configuration. + /// Creates a new schedule with the specified configuration. If already exists, update it in place. /// /// The task entity context. /// The configuration options for creating the schedule. @@ -43,6 +43,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); + this.State.RefreshScheduleRunExecutionToken(); this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately diff --git a/src/ScheduledTasks/Models/ScheduleTransitions.cs b/src/ScheduledTasks/Models/ScheduleTransitions.cs index f74c409a..4b6bb423 100644 --- a/src/ScheduledTasks/Models/ScheduleTransitions.cs +++ b/src/ScheduledTasks/Models/ScheduleTransitions.cs @@ -13,6 +13,8 @@ static class ScheduleTransitions new Dictionary> { { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, + { ScheduleStatus.Active, new HashSet { ScheduleStatus.Active } }, + { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, }; // define valid transitions for update schedule From 76f56b382fb81fdc1b0b3e8db5a5b9615a60a6c7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:36:04 -0800 Subject: [PATCH 153/203] fb --- src/ScheduledTasks/Models/ScheduleConfiguration.cs | 5 ++++- src/ScheduledTasks/Models/ScheduleCreationOptions.cs | 2 +- src/ScheduledTasks/Models/ScheduleDescription.cs | 5 ++++- src/ScheduledTasks/Models/ScheduleUpdateOptions.cs | 5 ++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 82c4a8f1..d32739c9 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -93,7 +93,10 @@ public TimeSpan Interval } /// - /// Gets or sets a value indicating whether gets or sets whether the schedule should start immediately if it's late. + /// Gets or sets a value indicating whether to start the orchestration immediately when the current time is past the StartAt time. + /// By default it is false. + /// If false, the first run will be scheduled at the next interval based on the original start time. + /// If true, the first run will start immediately and subsequent runs will follow the regular interval. /// public bool StartImmediatelyIfLate { get; set; } diff --git a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs index 9a937504..fda1fa1a 100644 --- a/src/ScheduledTasks/Models/ScheduleCreationOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleCreationOptions.cs @@ -11,7 +11,7 @@ public record ScheduleCreationOptions /// /// Initializes a new instance of the class. /// - /// The ID of the schedule, or null to generate one. + /// The ID of the schedule. /// The name of the orchestration to schedule. /// The time interval between schedule executions. Must be at least 1 second and cannot be negative. public ScheduleCreationOptions(string scheduleId, string orchestrationName, TimeSpan interval) diff --git a/src/ScheduledTasks/Models/ScheduleDescription.cs b/src/ScheduledTasks/Models/ScheduleDescription.cs index 9975c376..d5e6691d 100644 --- a/src/ScheduledTasks/Models/ScheduleDescription.cs +++ b/src/ScheduledTasks/Models/ScheduleDescription.cs @@ -46,7 +46,10 @@ public record ScheduleDescription public TimeSpan? Interval { get; init; } /// - /// Gets whether the schedule should run immediately if started late. + /// Gets a value indicating whether to start the orchestration immediately when the current time is past the StartAt time. + /// By default it is false. + /// If false, the first run will be scheduled at the next interval based on the original start time. + /// If true, the first run will start immediately and subsequent runs will follow the regular interval. /// public bool? StartImmediatelyIfLate { get; init; } diff --git a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs index 5e9fcc25..e9185c61 100644 --- a/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs +++ b/src/ScheduledTasks/Models/ScheduleUpdateOptions.cs @@ -61,7 +61,10 @@ public TimeSpan? Interval } /// - /// Gets or initializes whether the schedule should start immediately if it's late. + /// Gets a value indicating whether to start the orchestration immediately when the current time is past the StartAt time. + /// By default it is false. + /// If false, the first run will be scheduled at the next interval based on the original start time. + /// If true, the first run will start immediately and subsequent runs will follow the regular interval. /// public bool? StartImmediatelyIfLate { get; init; } } From b764a953c2c6f069728ae4daca396405e049b027 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:59:43 -0800 Subject: [PATCH 154/203] REFACTOR --- src/ScheduledTasks/Entity/Schedule.cs | 5 +- .../Models/ScheduleTransitions.cs | 74 ++++++++----------- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index ad89b51c..e9b0c164 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -280,10 +280,7 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) bool CanTransitionTo(string operationName, ScheduleStatus targetStatus) { - ScheduleStatus currentStatus = this.State.Status; - - return ScheduleTransitions.TryGetValidTransitions(operationName, currentStatus, out HashSet validTargetStates) && - validTargetStates.Contains(targetStatus); + return ScheduleTransitions.IsValidTransition(operationName, this.State.Status, targetStatus); } void TryStatusTransition(string operationName, ScheduleStatus to) diff --git a/src/ScheduledTasks/Models/ScheduleTransitions.cs b/src/ScheduledTasks/Models/ScheduleTransitions.cs index 4b6bb423..d7cf7939 100644 --- a/src/ScheduledTasks/Models/ScheduleTransitions.cs +++ b/src/ScheduledTasks/Models/ScheduleTransitions.cs @@ -8,57 +8,41 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// static class ScheduleTransitions { - // define valid transitions for create schedule - static readonly Dictionary> CreateScheduleStatusTransitions = - new Dictionary> - { - { ScheduleStatus.Uninitialized, new HashSet { ScheduleStatus.Active } }, - { ScheduleStatus.Active, new HashSet { ScheduleStatus.Active } }, - { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, - }; - - // define valid transitions for update schedule - static readonly Dictionary> UpdateScheduleStatusTransitions = - new Dictionary> - { - { ScheduleStatus.Active, new HashSet { ScheduleStatus.Active } }, - { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Paused } }, - }; - - // define valid transitions for pause schedule - static readonly Dictionary> PauseScheduleStatusTransitions = - new Dictionary> - { - { ScheduleStatus.Active, new HashSet { ScheduleStatus.Paused } }, - }; - - // define valid transitions for resume schedule - static readonly Dictionary> ResumeScheduleStatusTransitions = - new Dictionary> - { - { ScheduleStatus.Paused, new HashSet { ScheduleStatus.Active } }, - }; - /// - /// Attempts to get the valid target states for a given schedule state and operation. + /// Checks if a transition to the target state is valid for a given schedule state and operation. /// /// The name of the operation being performed. /// The current schedule state. - /// When this method returns, contains the valid target states if found; otherwise, an empty set. - /// True if valid transitions exist for the given state and operation; otherwise, false. - public static bool TryGetValidTransitions(string operationName, ScheduleStatus from, out HashSet validTargetStates) + /// The target state to transition to. + /// True if the transition is valid; otherwise, false. + public static bool IsValidTransition(string operationName, ScheduleStatus from, ScheduleStatus targetState) { - Dictionary> transitionMap = operationName switch + return operationName switch { - nameof(Schedule.CreateSchedule) => CreateScheduleStatusTransitions, - nameof(Schedule.UpdateSchedule) => UpdateScheduleStatusTransitions, - nameof(Schedule.PauseSchedule) => PauseScheduleStatusTransitions, - nameof(Schedule.ResumeSchedule) => ResumeScheduleStatusTransitions, - _ => new Dictionary>(), + nameof(Schedule.CreateSchedule) => from switch + { + ScheduleStatus.Uninitialized when targetState == ScheduleStatus.Active => true, + ScheduleStatus.Active when targetState == ScheduleStatus.Active => true, + ScheduleStatus.Paused when targetState == ScheduleStatus.Active => true, + _ => false, + }, + nameof(Schedule.UpdateSchedule) => from switch + { + ScheduleStatus.Active when targetState == ScheduleStatus.Active => true, + ScheduleStatus.Paused when targetState == ScheduleStatus.Paused => true, + _ => false, + }, + nameof(Schedule.PauseSchedule) => from switch + { + ScheduleStatus.Active when targetState == ScheduleStatus.Paused => true, + _ => false, + }, + nameof(Schedule.ResumeSchedule) => from switch + { + ScheduleStatus.Paused when targetState == ScheduleStatus.Active => true, + _ => false, + }, + _ => false, }; - - bool exists = transitionMap.TryGetValue(from, out HashSet? states); - validTargetStates = states ?? new HashSet(); - return exists; } } From 93620031fee5858926831ef51b31334bd09ed90a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:05:06 -0800 Subject: [PATCH 155/203] clean --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3933bd67..a1e78379 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + From ceaf8e48b8e3d594d38d34f09c636a84aa8494dd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:00:23 -0800 Subject: [PATCH 156/203] updatre nextrunat computation --- src/ScheduledTasks/Entity/Schedule.cs | 52 +++++++++++---------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index e9b0c164..cc3a678e 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -213,16 +213,16 @@ public void RunSchedule(TaskEntityContext context, string executionToken) return; } - this.DetermineNextRunTime(scheduleConfig); + this.State.NextRunAt = this.DetermineNextRunTime(scheduleConfig); DateTimeOffset currentTime = DateTimeOffset.UtcNow; if (this.State.NextRunAt!.Value <= currentTime) { - this.State.NextRunAt = currentTime; this.StartOrchestrationIfNotRunning(context); - this.State.LastRunAt = this.State.NextRunAt; - this.State.NextRunAt = this.State.LastRunAt.Value + interval; + this.State.LastRunAt = currentTime; + this.State.NextRunAt = null; + this.State.NextRunAt = this.DetermineNextRunTime(scheduleConfig); } context.SignalEntity( @@ -234,28 +234,6 @@ public void RunSchedule(TaskEntityContext context, string executionToken) new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); } - static DateTimeOffset? ComputeInitialRunTime(ScheduleConfiguration scheduleConfig) - { - if (scheduleConfig.StartImmediatelyIfLate && - scheduleConfig.StartAt.HasValue && - DateTimeOffset.UtcNow > scheduleConfig.StartAt.Value) - { - return DateTimeOffset.UtcNow; - } - - return scheduleConfig.StartAt ?? DateTimeOffset.UtcNow; // Default to now if StartAt not defined - } - - static DateTimeOffset ComputeNextRunTime(ScheduleConfiguration scheduleConfig, DateTimeOffset lastRunAt) - { - // Calculate number of intervals between last run and now - TimeSpan timeSinceLastRun = DateTimeOffset.UtcNow - lastRunAt; - int intervalsElapsed = (int)(timeSinceLastRun.Ticks / scheduleConfig.Interval.Ticks); - - // Compute and return the next run time - return lastRunAt + TimeSpan.FromTicks(scheduleConfig.Interval.Ticks * (intervalsElapsed + 1)); - } - void StartOrchestrationIfNotRunning(TaskEntityContext context) { try @@ -294,21 +272,31 @@ void TryStatusTransition(string operationName, ScheduleStatus to) this.State.Status = to; } - void DetermineNextRunTime(ScheduleConfiguration scheduleConfig) + DateTimeOffset DetermineNextRunTime(ScheduleConfiguration scheduleConfig) { if (this.State.NextRunAt.HasValue) { - return; // NextRunAt already set, no need to compute + return this.State.NextRunAt.Value; // NextRunAt already set, no need to compute } - // If LastRunAt is not set, determine if we should start immediately or at StartAt - if (!this.State.LastRunAt.HasValue) + // timenow + DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset startTime = scheduleConfig.StartAt ?? this.State.ScheduleCreatedAt ?? now; + + // compute time gap between now and startat if set else with ScheduleCreatedAt + TimeSpan timeSinceStart = now - startTime; + + if (timeSinceStart <= TimeSpan.Zero && scheduleConfig.StartImmediatelyIfLate) { - this.State.NextRunAt = ComputeInitialRunTime(scheduleConfig); + return now; } else { - this.State.NextRunAt = ComputeNextRunTime(scheduleConfig, this.State.LastRunAt.Value); + // Calculate number of intervals between start time and now + int intervalsElapsed = (int)(timeSinceStart.Ticks / scheduleConfig.Interval.Ticks); + + // Compute next run time based on intervals elapsed since start + return startTime + TimeSpan.FromTicks(scheduleConfig.Interval.Ticks * (intervalsElapsed + 1)); } } } From 448eb3010a93f67c558e8b92e32bc587abf702fc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:08:48 -0800 Subject: [PATCH 157/203] save --- src/ScheduledTasks/Entity/Schedule.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index cc3a678e..c39be07a 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -44,6 +44,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); this.State.RefreshScheduleRunExecutionToken(); + this.State.ScheduleCreatedAt = this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately @@ -88,6 +89,8 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche return; } + this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; + // after schedule config is updated, perform post-config-update logic separately foreach (string updatedScheduleConfigField in updatedScheduleConfigFields) { From 067bd3a97189a940bac92e95c7511534c962eafb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:41:22 -0800 Subject: [PATCH 158/203] fix --- src/ScheduledTasks/Entity/Schedule.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index c39be07a..1744abc4 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -110,10 +110,6 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche this.State.RefreshScheduleRunExecutionToken(); this.logger.UpdatedSchedule(this.State.ScheduleConfiguration.ScheduleId); - - // Signal to run schedule immediately after update and let runSchedule determine if it should run immediately - // or later to separate response from schedule creation and schedule responsibilities - context.SignalEntity(new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), nameof(this.RunSchedule), this.State.ExecutionToken); } catch (Exception ex) { From 7b413757399207d4a6b880c088b463cbf3ada30c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:43:19 -0800 Subject: [PATCH 159/203] excep --- .../Exception/ScheduleAlreadyExistException.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs index a4cc33cc..b43b2873 100644 --- a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs +++ b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs @@ -6,24 +6,24 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Exception thrown when attempting to create a schedule with an ID that already exists. /// -public class ScheduleAlreadyExistException : InvalidOperationException +public class ScheduleAlreadyExistsException : InvalidOperationException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ID of the schedule that already exists. - public ScheduleAlreadyExistException(string scheduleId) + public ScheduleAlreadyExistsException(string scheduleId) : base($"A schedule with ID '{scheduleId}' already exists.") { this.ScheduleId = scheduleId; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ID of the schedule that already exists. /// The exception that is the cause of the current exception. - public ScheduleAlreadyExistException(string scheduleId, Exception innerException) + public ScheduleAlreadyExistsException(string scheduleId, Exception innerException) : base($"A schedule with ID '{scheduleId}' already exists.", innerException) { this.ScheduleId = scheduleId; From 7d76f8c46e0d7b2e2f7a2f6ca2aface241fcd58d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:08:22 -0800 Subject: [PATCH 160/203] rename schedulehandle and ch to abstract --- .../ScheduleConsoleApp/ScheduleOperations.cs | 18 ++--- samples/ScheduleWebApp/ScheduleController.cs | 10 +-- ...duleHandle.cs => DefaultScheduleClient.cs} | 23 +++--- src/ScheduledTasks/Client/IScheduleHandle.cs | 51 ------------- .../Client/IScheduledTaskClient.cs | 4 +- src/ScheduledTasks/Client/ScheduleClient.cs | 72 +++++++++++++++++++ .../Client/ScheduledTaskClient.cs | 8 +-- 7 files changed, 101 insertions(+), 85 deletions(-) rename src/ScheduledTasks/Client/{ScheduleHandle.cs => DefaultScheduleClient.cs} (90%) delete mode 100644 src/ScheduledTasks/Client/IScheduleHandle.cs create mode 100644 src/ScheduledTasks/Client/ScheduleClient.cs diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index dfdd077a..7626523e 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -34,7 +34,7 @@ async Task DeleteExistingSchedulesAsync() // Delete each existing schedule await foreach (ScheduleDescription schedule in schedules) { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(schedule.ScheduleId); + ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(schedule.ScheduleId); await handle.DeleteAsync(); Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } @@ -53,28 +53,28 @@ async Task CreateAndManageScheduleAsync() }; // Create the schedule and get a handle to it - IScheduleHandle scheduleHandle = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + ScheduleClient DefaultScheduleClient = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); // Get and print the initial schedule description - await PrintScheduleDescriptionAsync(scheduleHandle); + await PrintScheduleDescriptionAsync(DefaultScheduleClient); // Pause the schedule Console.WriteLine("\nPausing schedule..."); - await scheduleHandle.PauseAsync(); - await PrintScheduleDescriptionAsync(scheduleHandle); + await DefaultScheduleClient.PauseAsync(); + await PrintScheduleDescriptionAsync(DefaultScheduleClient); // Resume the schedule Console.WriteLine("\nResuming schedule..."); - await scheduleHandle.ResumeAsync(); - await PrintScheduleDescriptionAsync(scheduleHandle); + await DefaultScheduleClient.ResumeAsync(); + await PrintScheduleDescriptionAsync(DefaultScheduleClient); // Wait for a while to let the schedule run await Task.Delay(TimeSpan.FromMinutes(30)); } - static async Task PrintScheduleDescriptionAsync(IScheduleHandle scheduleHandle) + static async Task PrintScheduleDescriptionAsync(ScheduleClient DefaultScheduleClient) { - ScheduleDescription scheduleDescription = await scheduleHandle.DescribeAsync(); + ScheduleDescription scheduleDescription = await DefaultScheduleClient.DescribeAsync(); Console.WriteLine(scheduleDescription); Console.WriteLine("\n\n"); } diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index ee4d1f6b..4f7ef411 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -56,7 +56,7 @@ public async Task> CreateSchedule([FromBody] C StartImmediatelyIfLate = true }; - IScheduleHandle handle = await this.scheduledTaskClient.CreateScheduleAsync(creationOptions); + ScheduleClient handle = await this.scheduledTaskClient.CreateScheduleAsync(creationOptions); ScheduleDescription description = await handle.DescribeAsync(); this.logger.LogInformation("Created new schedule with ID: {ScheduleId}", createScheduleRequest.Id); @@ -154,7 +154,7 @@ public async Task> UpdateSchedule(string id, [ try { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); ScheduleUpdateOptions updateOptions = new ScheduleUpdateOptions { @@ -194,7 +194,7 @@ public async Task DeleteSchedule(string id) { try { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); await handle.DeleteAsync(); return this.NoContent(); } @@ -219,7 +219,7 @@ public async Task> PauseSchedule(string id) { try { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); await handle.PauseAsync(); return this.Ok(await handle.DescribeAsync()); } @@ -244,7 +244,7 @@ public async Task> ResumeSchedule(string id) { try { - IScheduleHandle handle = this.scheduledTaskClient.GetScheduleHandle(id); + ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); await handle.ResumeAsync(); return this.Ok(await handle.DescribeAsync()); } diff --git a/src/ScheduledTasks/Client/ScheduleHandle.cs b/src/ScheduledTasks/Client/DefaultScheduleClient.cs similarity index 90% rename from src/ScheduledTasks/Client/ScheduleHandle.cs rename to src/ScheduledTasks/Client/DefaultScheduleClient.cs index ed65a83a..ba66a157 100644 --- a/src/ScheduledTasks/Client/ScheduleHandle.cs +++ b/src/ScheduledTasks/Client/DefaultScheduleClient.cs @@ -11,38 +11,33 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// -class ScheduleHandle : IScheduleHandle +class DefaultScheduleClient : ScheduleClient { readonly DurableTaskClient durableTaskClient; readonly ILogger logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The durable task client. /// The ID of the schedule. /// The logger. /// Thrown if or is null. - public ScheduleHandle(DurableTaskClient client, string scheduleId, ILogger logger) + public DefaultScheduleClient(DurableTaskClient client, string scheduleId, ILogger logger) + : base(scheduleId) { this.durableTaskClient = client ?? throw new ArgumentNullException(nameof(client)); - this.ScheduleId = scheduleId ?? throw new ArgumentNullException(nameof(scheduleId)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.EntityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); } - /// - /// Gets the ID of the schedule. - /// - public string ScheduleId { get; } - /// /// Gets the entity ID of the schedule. /// EntityInstanceId EntityId { get; } /// - public async Task DescribeAsync(CancellationToken cancellation = default) + public override async Task DescribeAsync(CancellationToken cancellation = default) { try { @@ -91,7 +86,7 @@ public async Task DescribeAsync(CancellationToken cancellat } /// - public async Task PauseAsync(CancellationToken cancellation = default) + public override async Task PauseAsync(CancellationToken cancellation = default) { try { @@ -126,7 +121,7 @@ public async Task PauseAsync(CancellationToken cancellation = default) } /// - public async Task ResumeAsync(CancellationToken cancellation = default) + public override async Task ResumeAsync(CancellationToken cancellation = default) { try { @@ -161,7 +156,7 @@ public async Task ResumeAsync(CancellationToken cancellation = default) } /// - public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default) + public override async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default) { try { @@ -199,7 +194,7 @@ public async Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationT // TODO: verify deleting non existent wont throw exception /// - public async Task DeleteAsync(CancellationToken cancellation = default) + public override async Task DeleteAsync(CancellationToken cancellation = default) { try { diff --git a/src/ScheduledTasks/Client/IScheduleHandle.cs b/src/ScheduledTasks/Client/IScheduleHandle.cs deleted file mode 100644 index 9bfc4092..00000000 --- a/src/ScheduledTasks/Client/IScheduleHandle.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Represents a handle to a schedule, allowing operations on a specific schedule instance. -/// -public interface IScheduleHandle -{ - /// - /// Gets the ID of this schedule. - /// - string ScheduleId { get; } - - /// - /// Retrieves the current details of this schedule. - /// - /// A cancellation token that can be used to cancel the operation. - /// The schedule details. - Task DescribeAsync(CancellationToken cancellation = default); - - /// - /// Deletes this schedule. The schedule will stop executing and be removed from the system. - /// - /// A cancellation token that can be used to cancel the operation. - /// A task that completes when the schedule has been deleted. - Task DeleteAsync(CancellationToken cancellation = default); - - /// - /// Pauses this schedule. The schedule will stop executing but remain in the system. - /// - /// A cancellation token that can be used to cancel the operation. - /// A task that completes when the schedule has been paused. - Task PauseAsync(CancellationToken cancellation = default); - - /// - /// Resumes this schedule. The schedule will continue executing from where it was paused. - /// - /// A cancellation token that can be used to cancel the operation. - /// A task that completes when the schedule has been resumed. - Task ResumeAsync(CancellationToken cancellation = default); - - /// - /// Updates this schedule with new configuration. The schedule will continue executing with the new configuration. - /// - /// The options for updating the schedule configuration. - /// A cancellation token that can be used to cancel the operation. - /// A task that completes when the schedule has been updated. - Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default); -} diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index ebc9339a..ee86877b 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -13,7 +13,7 @@ public interface IScheduledTaskClient /// /// The ID of the schedule. /// A handle to manage the schedule. - IScheduleHandle GetScheduleHandle(string scheduleId); + ScheduleClient GetDefaultScheduleClient(string scheduleId); /// /// Gets a schedule description by its ID. @@ -36,5 +36,5 @@ public interface IScheduledTaskClient /// The options for creating the schedule. /// Optional cancellation token. /// A handle to the created schedule. - Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); + Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClient.cs new file mode 100644 index 00000000..e098eee7 --- /dev/null +++ b/src/ScheduledTasks/Client/ScheduleClient.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Represents a handle to a schedule, allowing operations on a specific schedule instance. +/// +public abstract class ScheduleClient +{ + /// + /// Initializes a new instance of the class. + /// + /// Id of schedule. + protected ScheduleClient(string scheduleId) + { + this.ScheduleId = Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + } + + /// + /// Gets the ID of this schedule. + /// + public string ScheduleId { get; } + + /// + /// Retrieves the current details of this schedule. + /// + /// The cancellation token that can be used to cancel the operation. + /// A task that returns the schedule details when complete. + public abstract Task DescribeAsync(CancellationToken cancellation = default); + + /// + /// Deletes this schedule. + /// + /// + /// The schedule will stop executing and be removed from the system. + /// + /// The cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been deleted. + public abstract Task DeleteAsync(CancellationToken cancellation = default); + + /// + /// Pauses this schedule. + /// + /// + /// The schedule will stop executing but remain in the system. + /// + /// The cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been paused. + public abstract Task PauseAsync(CancellationToken cancellation = default); + + /// + /// Resumes this schedule. + /// + /// + /// The schedule will continue executing from where it was paused. + /// + /// The cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been resumed. + public abstract Task ResumeAsync(CancellationToken cancellation = default); + + /// + /// Updates this schedule with new configuration. + /// + /// + /// The schedule will continue executing with the new configuration. + /// + /// The options for updating the schedule configuration. + /// The cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been updated. + public abstract Task UpdateAsync(ScheduleUpdateOptions updateOptions, CancellationToken cancellation = default); +} diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 114fa2d7..698ea3c7 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -17,7 +17,7 @@ public class ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger lo readonly ILogger logger = Check.NotNull(logger, nameof(logger)); /// - public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) { Check.NotNull(creationOptions, nameof(creationOptions)); this.logger.ClientCreatingSchedule(creationOptions); @@ -43,7 +43,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions c } // Return a handle to the schedule - return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + return new DefaultScheduleClient(this.durableTaskClient, scheduleId, this.logger); } catch (OperationCanceledException ex) { @@ -108,10 +108,10 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions c } /// - public IScheduleHandle GetScheduleHandle(string scheduleId) + public ScheduleClient GetDefaultScheduleClient(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - return new ScheduleHandle(this.durableTaskClient, scheduleId, this.logger); + return new DefaultScheduleClient(this.durableTaskClient, scheduleId, this.logger); } /// From c88c98da286ac7ee8a78507d37eea784f137fc51 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:39:35 -0800 Subject: [PATCH 161/203] renmae to scheduleclientimpl --- .../ScheduleConsoleApp/ScheduleOperations.cs | 18 ++++++------- samples/ScheduleWebApp/ScheduleController.cs | 26 +++++++++---------- .../Client/IScheduledTaskClient.cs | 2 +- ...cheduleClient.cs => ScheduleClientImpl.cs} | 8 +++--- .../Client/ScheduledTaskClient.cs | 6 ++--- 5 files changed, 29 insertions(+), 31 deletions(-) rename src/ScheduledTasks/Client/{DefaultScheduleClient.cs => ScheduleClientImpl.cs} (96%) diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index 7626523e..0ad33e31 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -34,7 +34,7 @@ async Task DeleteExistingSchedulesAsync() // Delete each existing schedule await foreach (ScheduleDescription schedule in schedules) { - ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(schedule.ScheduleId); + ScheduleClient handle = this.scheduledTaskClient.GetScheduleClient(schedule.ScheduleId); await handle.DeleteAsync(); Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } @@ -53,28 +53,28 @@ async Task CreateAndManageScheduleAsync() }; // Create the schedule and get a handle to it - ScheduleClient DefaultScheduleClient = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + ScheduleClient scheduleClient = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); // Get and print the initial schedule description - await PrintScheduleDescriptionAsync(DefaultScheduleClient); + await PrintScheduleDescriptionAsync(scheduleClient); // Pause the schedule Console.WriteLine("\nPausing schedule..."); - await DefaultScheduleClient.PauseAsync(); - await PrintScheduleDescriptionAsync(DefaultScheduleClient); + await scheduleClient.PauseAsync(); + await PrintScheduleDescriptionAsync(scheduleClient); // Resume the schedule Console.WriteLine("\nResuming schedule..."); - await DefaultScheduleClient.ResumeAsync(); - await PrintScheduleDescriptionAsync(DefaultScheduleClient); + await scheduleClient.ResumeAsync(); + await PrintScheduleDescriptionAsync(scheduleClient); // Wait for a while to let the schedule run await Task.Delay(TimeSpan.FromMinutes(30)); } - static async Task PrintScheduleDescriptionAsync(ScheduleClient DefaultScheduleClient) + static async Task PrintScheduleDescriptionAsync(ScheduleClient scheduleClient) { - ScheduleDescription scheduleDescription = await DefaultScheduleClient.DescribeAsync(); + ScheduleDescription scheduleDescription = await scheduleClient.DescribeAsync(); Console.WriteLine(scheduleDescription); Console.WriteLine("\n\n"); } diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index 4f7ef411..069d1506 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -56,8 +56,8 @@ public async Task> CreateSchedule([FromBody] C StartImmediatelyIfLate = true }; - ScheduleClient handle = await this.scheduledTaskClient.CreateScheduleAsync(creationOptions); - ScheduleDescription description = await handle.DescribeAsync(); + ScheduleClient scheduleClient = await this.scheduledTaskClient.CreateScheduleAsync(creationOptions); + ScheduleDescription description = await scheduleClient.DescribeAsync(); this.logger.LogInformation("Created new schedule with ID: {ScheduleId}", createScheduleRequest.Id); @@ -154,7 +154,7 @@ public async Task> UpdateSchedule(string id, [ try { - ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); + ScheduleClient scheduleClient = this.scheduledTaskClient.GetScheduleClient(id); ScheduleUpdateOptions updateOptions = new ScheduleUpdateOptions { @@ -165,8 +165,8 @@ public async Task> UpdateSchedule(string id, [ Interval = updateScheduleRequest.Interval }; - await handle.UpdateAsync(updateOptions); - return this.Ok(await handle.DescribeAsync()); + await scheduleClient.UpdateAsync(updateOptions); + return this.Ok(await scheduleClient.DescribeAsync()); } catch (ScheduleNotFoundException) { @@ -194,8 +194,8 @@ public async Task DeleteSchedule(string id) { try { - ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); - await handle.DeleteAsync(); + ScheduleClient scheduleClient = this.scheduledTaskClient.GetScheduleClient(id); + await scheduleClient.DeleteAsync(); return this.NoContent(); } catch (ScheduleNotFoundException) @@ -219,9 +219,9 @@ public async Task> PauseSchedule(string id) { try { - ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); - await handle.PauseAsync(); - return this.Ok(await handle.DescribeAsync()); + ScheduleClient scheduleClient = this.scheduledTaskClient.GetScheduleClient(id); + await scheduleClient.PauseAsync(); + return this.Ok(await scheduleClient.DescribeAsync()); } catch (ScheduleNotFoundException) { @@ -244,9 +244,9 @@ public async Task> ResumeSchedule(string id) { try { - ScheduleClient handle = this.scheduledTaskClient.GetDefaultScheduleClient(id); - await handle.ResumeAsync(); - return this.Ok(await handle.DescribeAsync()); + ScheduleClient scheduleClient = this.scheduledTaskClient.GetScheduleClient(id); + await scheduleClient.ResumeAsync(); + return this.Ok(await scheduleClient.DescribeAsync()); } catch (ScheduleNotFoundException) { diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs index ee86877b..51e6fdfb 100644 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/IScheduledTaskClient.cs @@ -13,7 +13,7 @@ public interface IScheduledTaskClient /// /// The ID of the schedule. /// A handle to manage the schedule. - ScheduleClient GetDefaultScheduleClient(string scheduleId); + ScheduleClient GetScheduleClient(string scheduleId); /// /// Gets a schedule description by its ID. diff --git a/src/ScheduledTasks/Client/DefaultScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs similarity index 96% rename from src/ScheduledTasks/Client/DefaultScheduleClient.cs rename to src/ScheduledTasks/Client/ScheduleClientImpl.cs index ba66a157..7da891c4 100644 --- a/src/ScheduledTasks/Client/DefaultScheduleClient.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -11,19 +11,19 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// -class DefaultScheduleClient : ScheduleClient +class ScheduleClientImpl : ScheduleClient { readonly DurableTaskClient durableTaskClient; readonly ILogger logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The durable task client. /// The ID of the schedule. /// The logger. /// Thrown if or is null. - public DefaultScheduleClient(DurableTaskClient client, string scheduleId, ILogger logger) + public ScheduleClientImpl(DurableTaskClient client, string scheduleId, ILogger logger) : base(scheduleId) { this.durableTaskClient = client ?? throw new ArgumentNullException(nameof(client)); @@ -191,8 +191,6 @@ public override async Task UpdateAsync(ScheduleUpdateOptions updateOptions, Canc } } - // TODO: verify deleting non existent wont throw exception - /// public override async Task DeleteAsync(CancellationToken cancellation = default) { diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 698ea3c7..19be26d9 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -43,7 +43,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr } // Return a handle to the schedule - return new DefaultScheduleClient(this.durableTaskClient, scheduleId, this.logger); + return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); } catch (OperationCanceledException ex) { @@ -108,10 +108,10 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr } /// - public ScheduleClient GetDefaultScheduleClient(string scheduleId) + public ScheduleClient GetScheduleClient(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - return new DefaultScheduleClient(this.durableTaskClient, scheduleId, this.logger); + return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); } /// From 19144a9427d8c98e1cf92a80589e18c31ace3ad5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:45:05 -0800 Subject: [PATCH 162/203] SAVE --- samples/ScheduleConsoleApp/Program.cs | 2 +- .../ScheduleConsoleApp/ScheduleOperations.cs | 4 +- samples/ScheduleWebApp/ScheduleController.cs | 4 +- .../Client/IScheduledTaskClient.cs | 40 ---- .../Client/ScheduledTaskClient.cs | 202 +++--------------- .../Client/ScheduledTaskClientImpl.cs | 180 ++++++++++++++++ .../DurableTaskClientBuilderExtensions.cs | 2 +- 7 files changed, 217 insertions(+), 217 deletions(-) delete mode 100644 src/ScheduledTasks/Client/IScheduledTaskClient.cs create mode 100644 src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index 01469967..a91440a2 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -51,7 +51,7 @@ await host.StartAsync(); // Run the schedule operations -IScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); +ScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); ScheduleOperations scheduleOperations = new ScheduleOperations(scheduledTaskClient); await scheduleOperations.RunAsync(); diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index 0ad33e31..38546bf7 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -6,9 +6,9 @@ namespace ScheduleConsoleApp; -class ScheduleOperations(IScheduledTaskClient scheduledTaskClient) +class ScheduleOperations(ScheduledTaskClient scheduledTaskClient) { - readonly IScheduledTaskClient scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); + readonly ScheduledTaskClient scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); public async Task RunAsync() { diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index 069d1506..87854f53 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -17,7 +17,7 @@ namespace ScheduleWebApp.Controllers; [Route("schedules")] public class ScheduleController : ControllerBase { - readonly IScheduledTaskClient scheduledTaskClient; + readonly ScheduledTaskClient scheduledTaskClient; readonly ILogger logger; /// @@ -26,7 +26,7 @@ public class ScheduleController : ControllerBase /// Client for managing scheduled tasks. /// Logger for recording controller operations. public ScheduleController( - IScheduledTaskClient scheduledTaskClient, + ScheduledTaskClient scheduledTaskClient, ILogger logger) { this.scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); diff --git a/src/ScheduledTasks/Client/IScheduledTaskClient.cs b/src/ScheduledTasks/Client/IScheduledTaskClient.cs deleted file mode 100644 index 51e6fdfb..00000000 --- a/src/ScheduledTasks/Client/IScheduledTaskClient.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Interface for managing scheduled tasks in a Durable Task application. -/// -public interface IScheduledTaskClient -{ - /// - /// Gets a handle to a schedule, allowing operations on it. - /// - /// The ID of the schedule. - /// A handle to manage the schedule. - ScheduleClient GetScheduleClient(string scheduleId); - - /// - /// Gets a schedule description by its ID. - /// - /// The ID of the schedule to retrieve. - /// Optional cancellation token. - /// The schedule description if found, null otherwise. - Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default); - - /// - /// Gets a pageable list of schedules matching the specified filter criteria. - /// - /// Optional filter criteria for the schedules. If null, returns all schedules. - /// A pageable list of schedule descriptions. - AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null); - - /// - /// Creates a new schedule with the specified configuration. - /// - /// The options for creating the schedule. - /// Optional cancellation token. - /// A handle to the created schedule. - Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); -} diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 19be26d9..537aac77 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -1,180 +1,40 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.Entities; -using Microsoft.DurableTask.Entities; -using Microsoft.Extensions.Logging; - namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Client for managing scheduled tasks in a Durable Task application. +/// Base class for managing scheduled tasks in a Durable Task application. /// -public class ScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) : IScheduledTaskClient +public abstract class ScheduledTaskClient { - readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); - readonly ILogger logger = Check.NotNull(logger, nameof(logger)); - - /// - public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) - { - Check.NotNull(creationOptions, nameof(creationOptions)); - this.logger.ClientCreatingSchedule(creationOptions); - - try - { - string scheduleId = creationOptions.ScheduleId; - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - - // Call the orchestrator to create the schedule - ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); - - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); - - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) - { - throw new InvalidOperationException($"Failed to create schedule '{scheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); - } - - // Return a handle to the schedule - return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); - } - catch (OperationCanceledException ex) - { - this.logger.ClientError( - nameof(this.CreateScheduleAsync), - creationOptions.ScheduleId, - ex); - - throw new OperationCanceledException( - $"The {nameof(this.CreateScheduleAsync)} operation was canceled.", - null, - cancellation); - } - } - - /// - public async Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default) - { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - - try - { - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); - - if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) - { - return null; - } - - ScheduleState state = metadata.State; - ScheduleConfiguration? config = state.ScheduleConfiguration; - - return new ScheduleDescription - { - ScheduleId = scheduleId, - OrchestrationName = config?.OrchestrationName, - OrchestrationInput = config?.OrchestrationInput, - OrchestrationInstanceId = config?.OrchestrationInstanceId, - StartAt = config?.StartAt, - EndAt = config?.EndAt, - Interval = config?.Interval, - StartImmediatelyIfLate = config?.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }; - } - catch (OperationCanceledException ex) - { - this.logger.ClientError( - nameof(this.GetScheduleAsync), - scheduleId, - ex); - - throw new OperationCanceledException( - $"The {nameof(this.GetScheduleAsync)} operation was canceled.", - null, - cancellation); - } - } - - /// - public ScheduleClient GetScheduleClient(string scheduleId) - { - Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); - } - - /// - public AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) - { - // Create an async pageable using the Pageable.Create helper - return Pageable.Create(async (continuationToken, pageSize, cancellation) => - { - try - { - // TODO: map to entity query last modified from/to filters - EntityQuery query = new EntityQuery - { - InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), - IncludeState = true, - PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, - ContinuationToken = continuationToken, - }; - - // Get one page of entities - IAsyncEnumerable>> entityPages = - this.durableTaskClient.Entities.GetAllEntitiesAsync(query).AsPages(); - - await foreach (Page> entityPage in entityPages) - { - List schedules = entityPage.Values - .Where(metadata => - (!filter?.Status.HasValue ?? true || metadata.State.Status == filter.Status.Value) && - (filter?.CreatedFrom.HasValue != true || metadata.State.ScheduleCreatedAt > filter.CreatedFrom) && - (filter?.CreatedTo.HasValue != true || metadata.State.ScheduleCreatedAt < filter.CreatedTo)) - .Select(metadata => - { - ScheduleState state = metadata.State; - ScheduleConfiguration config = state.ScheduleConfiguration!; - return new ScheduleDescription - { - ScheduleId = metadata.Id.Key, - OrchestrationName = config.OrchestrationName, - OrchestrationInput = config.OrchestrationInput, - OrchestrationInstanceId = config.OrchestrationInstanceId, - StartAt = config.StartAt, - EndAt = config.EndAt, - Interval = config.Interval, - StartImmediatelyIfLate = config.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }; - }) - .ToList(); - - return new Page(schedules, entityPage.ContinuationToken); - } - - // Return empty page if no results - return new Page(new List(), null); - } - catch (OperationCanceledException e) - { - throw new OperationCanceledException( - $"The {nameof(this.ListSchedulesAsync)} operation was canceled.", e, e.CancellationToken); - } - }); - } + /// + /// Gets a handle to a schedule, allowing operations on it. + /// + /// The ID of the schedule. + /// A handle to manage the schedule. + public abstract ScheduleClient GetScheduleClient(string scheduleId); + + /// + /// Gets a schedule description by its ID. + /// + /// The ID of the schedule to retrieve. + /// Optional cancellation token. + /// The schedule description if found, null otherwise. + public abstract Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default); + + /// + /// Gets a pageable list of schedules matching the specified filter criteria. + /// + /// Optional filter criteria for the schedules. If null, returns all schedules. + /// A pageable list of schedule descriptions. + public abstract AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null); + + /// + /// Creates a new schedule with the specified configuration. + /// + /// The options for creating the schedule. + /// Optional cancellation token. + /// A handle to the created schedule. + public abstract Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs new file mode 100644 index 00000000..a8f6c04e --- /dev/null +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.ScheduledTasks; + +/// +/// Client for managing scheduled tasks in a Durable Task application. +/// +public class ScheduledTaskClientImpl(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient +{ + readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); + readonly ILogger logger = Check.NotNull(logger, nameof(logger)); + + /// + public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + { + Check.NotNull(creationOptions, nameof(creationOptions)); + this.logger.ClientCreatingSchedule(creationOptions); + + try + { + string scheduleId = creationOptions.ScheduleId; + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + + // Call the orchestrator to create the schedule + ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to create schedule '{scheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + + // Return a handle to the schedule + return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); + } + catch (OperationCanceledException ex) + { + this.logger.ClientError( + nameof(this.CreateScheduleAsync), + creationOptions.ScheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.CreateScheduleAsync)} operation was canceled.", + null, + cancellation); + } + } + + /// + public async Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + + try + { + EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); + EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); + + if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) + { + return null; + } + + ScheduleState state = metadata.State; + ScheduleConfiguration? config = state.ScheduleConfiguration; + + return new ScheduleDescription + { + ScheduleId = scheduleId, + OrchestrationName = config?.OrchestrationName, + OrchestrationInput = config?.OrchestrationInput, + OrchestrationInstanceId = config?.OrchestrationInstanceId, + StartAt = config?.StartAt, + EndAt = config?.EndAt, + Interval = config?.Interval, + StartImmediatelyIfLate = config?.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + }; + } + catch (OperationCanceledException ex) + { + this.logger.ClientError( + nameof(this.GetScheduleAsync), + scheduleId, + ex); + + throw new OperationCanceledException( + $"The {nameof(this.GetScheduleAsync)} operation was canceled.", + null, + cancellation); + } + } + + /// + public ScheduleClient GetScheduleClient(string scheduleId) + { + Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); + return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); + } + + /// + public AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) + { + // Create an async pageable using the Pageable.Create helper + return Pageable.Create(async (continuationToken, pageSize, cancellation) => + { + try + { + // TODO: map to entity query last modified from/to filters + EntityQuery query = new EntityQuery + { + InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), + IncludeState = true, + PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, + ContinuationToken = continuationToken, + }; + + // Get one page of entities + IAsyncEnumerable>> entityPages = + this.durableTaskClient.Entities.GetAllEntitiesAsync(query).AsPages(); + + await foreach (Page> entityPage in entityPages) + { + List schedules = entityPage.Values + .Where(metadata => + (!filter?.Status.HasValue ?? true || metadata.State.Status == filter.Status.Value) && + (filter?.CreatedFrom.HasValue != true || metadata.State.ScheduleCreatedAt > filter.CreatedFrom) && + (filter?.CreatedTo.HasValue != true || metadata.State.ScheduleCreatedAt < filter.CreatedTo)) + .Select(metadata => + { + ScheduleState state = metadata.State; + ScheduleConfiguration config = state.ScheduleConfiguration!; + return new ScheduleDescription + { + ScheduleId = metadata.Id.Key, + OrchestrationName = config.OrchestrationName, + OrchestrationInput = config.OrchestrationInput, + OrchestrationInstanceId = config.OrchestrationInstanceId, + StartAt = config.StartAt, + EndAt = config.EndAt, + Interval = config.Interval, + StartImmediatelyIfLate = config.StartImmediatelyIfLate, + Status = state.Status, + ExecutionToken = state.ExecutionToken, + LastRunAt = state.LastRunAt, + NextRunAt = state.NextRunAt, + }; + }) + .ToList(); + + return new Page(schedules, entityPage.ContinuationToken); + } + + // Return empty page if no results + return new Page(new List(), null); + } + catch (OperationCanceledException e) + { + throw new OperationCanceledException( + $"The {nameof(this.ListSchedulesAsync)} operation was canceled.", e, e.CancellationToken); + } + }); + } +} diff --git a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs index 4f8c9b67..df64b826 100644 --- a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs @@ -19,7 +19,7 @@ public static class DurableTaskClientBuilderExtensions /// The original builder, for call chaining. public static IDurableTaskClientBuilder UseScheduledTasks(this IDurableTaskClientBuilder builder) { - builder.Services.AddTransient(sp => + builder.Services.AddTransient(sp => { DurableTaskClient client = sp.GetRequiredService(); ILogger logger = sp.GetRequiredService>(); From 4f5c71b2f52456aeadf7e45012d659087be93ba7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:50:48 -0800 Subject: [PATCH 163/203] rename scheduledtaskclient and abstract --- src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs | 10 ++++++---- .../Extension/DurableTaskClientBuilderExtensions.cs | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index a8f6c04e..219439ef 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -11,13 +11,15 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Client for managing scheduled tasks in a Durable Task application. /// +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix public class ScheduledTaskClientImpl(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix { readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); readonly ILogger logger = Check.NotNull(logger, nameof(logger)); /// - public async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + public override async Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) { Check.NotNull(creationOptions, nameof(creationOptions)); this.logger.ClientCreatingSchedule(creationOptions); @@ -60,7 +62,7 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr } /// - public async Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default) + public override async Task GetScheduleAsync(string scheduleId, CancellationToken cancellation = default) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); @@ -108,14 +110,14 @@ public async Task CreateScheduleAsync(ScheduleCreationOptions cr } /// - public ScheduleClient GetScheduleClient(string scheduleId) + public override ScheduleClient GetScheduleClient(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); } /// - public AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) + public override AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) { // Create an async pageable using the Pageable.Create helper return Pageable.Create(async (continuationToken, pageSize, cancellation) => diff --git a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs index df64b826..33a35aca 100644 --- a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs @@ -22,8 +22,8 @@ public static IDurableTaskClientBuilder UseScheduledTasks(this IDurableTaskClien builder.Services.AddTransient(sp => { DurableTaskClient client = sp.GetRequiredService(); - ILogger logger = sp.GetRequiredService>(); - return new ScheduledTaskClient(client, logger); + ILogger logger = sp.GetRequiredService>(); + return new ScheduledTaskClientImpl(client, logger); }); return builder; From c64de7c71ea8bb1e1547308525d685fd60bbcd88 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:54:16 -0800 Subject: [PATCH 164/203] fix --- src/ScheduledTasks/Client/ScheduleClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ScheduledTasks/Client/ScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClient.cs index e098eee7..0b701a36 100644 --- a/src/ScheduledTasks/Client/ScheduleClient.cs +++ b/src/ScheduledTasks/Client/ScheduleClient.cs @@ -34,6 +34,7 @@ protected ScheduleClient(string scheduleId) /// /// /// The schedule will stop executing and be removed from the system. + /// Does not affect orchestrations that have already been started. /// /// The cancellation token that can be used to cancel the operation. /// A task that completes when the schedule has been deleted. From cc00dd804cca9e4dadf5334bc5f3567dd0d9e9a0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:55:03 -0800 Subject: [PATCH 165/203] fix --- src/ScheduledTasks/Client/ScheduleClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ScheduledTasks/Client/ScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClient.cs index 0b701a36..68cb1f3b 100644 --- a/src/ScheduledTasks/Client/ScheduleClient.cs +++ b/src/ScheduledTasks/Client/ScheduleClient.cs @@ -45,6 +45,7 @@ protected ScheduleClient(string scheduleId) /// /// /// The schedule will stop executing but remain in the system. + /// Does not affect orchestrations that have already been started. /// /// The cancellation token that can be used to cancel the operation. /// A task that completes when the schedule has been paused. From 63678609f061b07ff67d78a1b1603cd28c18c1d4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:19:10 -0800 Subject: [PATCH 166/203] fix --- src/ScheduledTasks/Models/ScheduleConfiguration.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index d32739c9..7b83eb3d 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -17,12 +17,10 @@ class ScheduleConfiguration /// The ID of the schedule. /// The name of the orchestration to schedule. /// The interval between schedule executions. Must be positive and at least 1 second. -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public ScheduleConfiguration(string scheduleId, string orchestrationName, TimeSpan interval) -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. { this.ScheduleId = Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - this.OrchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); + this.orchestrationName = Check.NotNullOrEmpty(orchestrationName, nameof(orchestrationName)); if (interval <= TimeSpan.Zero) { throw new ArgumentException("Interval must be positive", nameof(interval)); From 202be35e91f05c3edf9b6a13dd216fe77114a666 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:31:39 -0800 Subject: [PATCH 167/203] fix --- .../Client/ScheduleClientImpl.cs | 60 +++++++++++-------- .../Client/ScheduledTaskClientImpl.cs | 24 ++++---- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index 7da891c4..69ffb298 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -71,17 +71,19 @@ public override async Task DescribeAsync(CancellationToken NextRunAt = state.NextRunAt, }; } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.DescribeAsync), this.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.DescribeAsync)} operation was canceled.", - ex, - cancellation); + throw; } } @@ -106,17 +108,19 @@ public override async Task PauseAsync(CancellationToken cancellation = default) throw new InvalidOperationException($"Failed to pause schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.PauseAsync), this.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.PauseAsync)} operation was canceled.", - null, - cancellation); + throw; } } @@ -141,17 +145,19 @@ public override async Task ResumeAsync(CancellationToken cancellation = default) throw new InvalidOperationException($"Failed to resume schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.ResumeAsync), this.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.ResumeAsync)} operation was canceled.", - null, - cancellation); + throw; } } @@ -177,17 +183,19 @@ public override async Task UpdateAsync(ScheduleUpdateOptions updateOptions, Canc throw new InvalidOperationException($"Failed to update schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.UpdateAsync), this.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.UpdateAsync)} operation was canceled.", - null, - cancellation); + throw; } } @@ -212,17 +220,19 @@ public override async Task DeleteAsync(CancellationToken cancellation = default) throw new InvalidOperationException($"Failed to delete schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); } } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.DeleteAsync), this.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.DeleteAsync)} operation was canceled.", - null, - cancellation); + throw; } } } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index 219439ef..a453d59c 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -47,17 +47,19 @@ public override async Task CreateScheduleAsync(ScheduleCreationO // Return a handle to the schedule return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.CreateScheduleAsync), creationOptions.ScheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.CreateScheduleAsync)} operation was canceled.", - null, - cancellation); + throw; } } @@ -95,17 +97,19 @@ public override async Task CreateScheduleAsync(ScheduleCreationO NextRunAt = state.NextRunAt, }; } - catch (OperationCanceledException ex) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) { this.logger.ClientError( nameof(this.GetScheduleAsync), scheduleId, ex); - throw new OperationCanceledException( - $"The {nameof(this.GetScheduleAsync)} operation was canceled.", - null, - cancellation); + throw; } } From 34c5ee22e5e95629fc6db42234cf996e0b82bdfc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:25:17 -0800 Subject: [PATCH 168/203] test p1 --- Microsoft.DurableTask.sln | 7 + .../Client/ScheduleClientImplTests.cs | 242 ++++++++++++++++++ .../Client/ScheduledTaskClientImplTests.cs | 205 +++++++++++++++ .../Entity/ScheduleTests.cs | 206 +++++++++++++++ .../Models/ScheduleConfigurationTests.cs | 177 +++++++++++++ .../Models/ScheduleCreationOptionsTests.cs | 122 +++++++++ .../Models/ScheduleStateTests.cs | 84 ++++++ ...ecuteScheduleOperationOrchestratorTests.cs | 90 +++++++ .../ScheduledTasks.Tests.csproj | 30 +++ 9 files changed, 1163 insertions(+) create mode 100644 test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs create mode 100644 test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs create mode 100644 test/ScheduledTasks.Tests/Entity/ScheduleTests.cs create mode 100644 test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs create mode 100644 test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs create mode 100644 test/ScheduledTasks.Tests/Models/ScheduleStateTests.cs create mode 100644 test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs create mode 100644 test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 2cc79905..26c2e80d 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -91,6 +91,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleConsoleApp", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduleWebApp", "samples\ScheduleWebApp\ScheduleWebApp.csproj", "{100348B5-4D97-4A3F-B777-AB14F276F8FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScheduledTasks.Tests", "test\ScheduledTasks.Tests\ScheduledTasks.Tests.csproj", "{D2779F32-A548-44F8-B60A-6AC018966C79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -241,6 +243,10 @@ Global {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {100348B5-4D97-4A3F-B777-AB14F276F8FE}.Release|Any CPU.Build.0 = Release|Any CPU + {D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -286,6 +292,7 @@ Global {69ED743C-D616-4530-87E2-391D249D7368} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {D2779F32-A548-44F8-B60A-6AC018966C79} = {E5637F81-2FB9-4CD7-900D-455363B142A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs new file mode 100644 index 00000000..ffa120a0 --- /dev/null +++ b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; + +public class ScheduleClientImplTests +{ + private readonly Mock durableTaskClientMock; + private readonly Mock loggerMock; + private readonly Mock entityClientMock; + private readonly ScheduleClientImpl client; + private readonly string scheduleId = "test-schedule"; + + public ScheduleClientImplTests() + { + this.durableTaskClientMock = new Mock(); + this.loggerMock = new Mock(); + this.entityClientMock = new Mock(); + this.durableTaskClientMock.Setup(c => c.Entities).Returns(this.entityClientMock.Object); + this.client = new ScheduleClientImpl(this.durableTaskClientMock.Object, this.scheduleId, this.loggerMock.Object); + } + + [Fact] + public void Constructor_WithNullClient_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => + new ScheduleClientImpl(null!, this.scheduleId, this.loggerMock.Object)); + Assert.Equal("client", ex.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string invalidScheduleId) + { + // Act & Assert + var ex = Assert.Throws(() => + new ScheduleClientImpl(this.durableTaskClientMock.Object, invalidScheduleId, this.loggerMock.Object)); + Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Constructor_WithNullLogger_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => + new ScheduleClientImpl(this.durableTaskClientMock.Object, this.scheduleId, null!)); + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public async Task DescribeAsync_WhenExists_ReturnsDescription() + { + // Arrange + var state = new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration(this.scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) + }; + + this.entityClientMock + .Setup(c => c.GetEntityAsync( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + It.IsAny())) + .ReturnsAsync(new EntityMetadata(new EntityInstanceId(nameof(Schedule), this.scheduleId), state)); + + // Act + var description = await this.client.DescribeAsync(); + + // Assert + Assert.NotNull(description); + Assert.Equal(this.scheduleId, description.ScheduleId); + Assert.Equal(state.Status, description.Status); + Assert.Equal(state.ScheduleConfiguration.OrchestrationName, description.OrchestrationName); + } + + [Fact] + public async Task DescribeAsync_WhenNotExists_ThrowsScheduleNotFoundException() + { + // Arrange + this.entityClientMock + .Setup(c => c.GetEntityAsync( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + It.IsAny())) + .ReturnsAsync((EntityMetadata)null!); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => this.client.DescribeAsync()); + Assert.Equal(this.scheduleId, ex.ScheduleId); + } + + [Fact] + public async Task DeleteAsync_ExecutesDeleteOperation() + { + // Arrange + string instanceId = "test-instance"; + + this.durableTaskClientMock + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClientMock + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata()); + + // Act + await this.client.DeleteAsync(); + + // Assert + this.durableTaskClientMock.Verify( + c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == this.scheduleId && + r.OperationName == nameof(Schedule.DeleteSchedule)), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task PauseAsync_ExecutesPauseOperation() + { + // Arrange + string instanceId = "test-instance"; + + this.durableTaskClientMock + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClientMock + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata()); + + // Act + await this.client.PauseAsync(); + + // Assert + this.durableTaskClientMock.Verify( + c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == this.scheduleId && + r.OperationName == nameof(Schedule.PauseSchedule)), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ResumeAsync_ExecutesResumeOperation() + { + // Arrange + string instanceId = "test-instance"; + + this.durableTaskClientMock + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClientMock + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata()); + + // Act + await this.client.ResumeAsync(); + + // Assert + this.durableTaskClientMock.Verify( + c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == this.scheduleId && + r.OperationName == nameof(Schedule.ResumeSchedule)), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task UpdateAsync_ExecutesUpdateOperation() + { + // Arrange + string instanceId = "test-instance"; + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationName = "new-orchestration", + Interval = TimeSpan.FromMinutes(10) + }; + + this.durableTaskClientMock + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClientMock + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata()); + + // Act + await this.client.UpdateAsync(updateOptions); + + // Assert + this.durableTaskClientMock.Verify( + c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == this.scheduleId && + r.OperationName == nameof(Schedule.UpdateSchedule) && + r.OperationInput == updateOptions), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task UpdateAsync_WithNullOptions_ThrowsArgumentNullException() + { + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => this.client.UpdateAsync(null!)); + Assert.Equal("updateOptions", ex.ParamName); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs new file mode 100644 index 00000000..bb252f40 --- /dev/null +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; + +public class ScheduledTaskClientImplTests +{ + private readonly Mock durableTaskClientMock; + private readonly Mock loggerMock; + private readonly Mock entityClientMock; + private readonly ScheduledTaskClientImpl client; + + public ScheduledTaskClientImplTests() + { + this.durableTaskClientMock = new Mock(); + this.loggerMock = new Mock(); + this.entityClientMock = new Mock(); + this.durableTaskClientMock.Setup(c => c.Entities).Returns(this.entityClientMock.Object); + this.client = new ScheduledTaskClientImpl(this.durableTaskClientMock.Object, this.loggerMock.Object); + } + + [Fact] + public void Constructor_WithNullClient_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.loggerMock.Object)); + Assert.Equal("durableTaskClient", ex.ParamName); + } + + [Fact] + public void Constructor_WithNullLogger_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => new ScheduledTaskClientImpl(this.durableTaskClientMock.Object, null!)); + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public void GetScheduleClient_ReturnsValidClient() + { + // Arrange + string scheduleId = "test-schedule"; + + // Act + var scheduleClient = this.client.GetScheduleClient(scheduleId); + + // Assert + Assert.NotNull(scheduleClient); + Assert.Equal(scheduleId, scheduleClient.ScheduleId); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetScheduleClient_WithInvalidId_ThrowsArgumentException(string scheduleId) + { + // Act & Assert + var ex = Assert.Throws(() => this.client.GetScheduleClient(scheduleId)); + Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + string instanceId = "test-instance"; + + this.durableTaskClientMock + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClientMock + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata()); + + // Act + var scheduleClient = await this.client.CreateScheduleAsync(options); + + // Assert + Assert.NotNull(scheduleClient); + Assert.Equal(options.ScheduleId, scheduleClient.ScheduleId); + + this.durableTaskClientMock.Verify( + c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == options.ScheduleId && + r.OperationName == nameof(Schedule.CreateSchedule)), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task CreateScheduleAsync_WithNullOptions_ThrowsArgumentNullException() + { + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => this.client.CreateScheduleAsync(null!)); + Assert.Equal("creationOptions", ex.ParamName); + } + + [Fact] + public async Task GetScheduleAsync_WhenExists_ReturnsDescription() + { + // Arrange + string scheduleId = "test-schedule"; + var state = new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration(scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) + }; + + this.entityClientMock + .Setup(c => c.GetEntityAsync( + It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), + It.IsAny())) + .ReturnsAsync(new EntityMetadata(new EntityInstanceId(nameof(Schedule), scheduleId), state)); + + // Act + var description = await this.client.GetScheduleAsync(scheduleId); + + // Assert + Assert.NotNull(description); + Assert.Equal(scheduleId, description.ScheduleId); + Assert.Equal(state.Status, description.Status); + Assert.Equal(state.ScheduleConfiguration.OrchestrationName, description.OrchestrationName); + } + + [Fact] + public async Task GetScheduleAsync_WhenNotExists_ReturnsNull() + { + // Arrange + string scheduleId = "test-schedule"; + + this.entityClientMock + .Setup(c => c.GetEntityAsync( + It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), + It.IsAny())) + .ReturnsAsync((EntityMetadata)null!); + + // Act + var description = await this.client.GetScheduleAsync(scheduleId); + + // Assert + Assert.Null(description); + } + + [Fact] + public async Task ListSchedulesAsync_ReturnsSchedules() + { + // Arrange + var query = new ScheduleQuery + { + ScheduleIdPrefix = "test", + Status = ScheduleStatus.Active, + PageSize = 10 + }; + + var states = new[] + { + new EntityMetadata( + new EntityInstanceId(nameof(Schedule), "test-1"), + new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration("test-1", "test-orchestration", TimeSpan.FromMinutes(5)) + }), + new EntityMetadata( + new EntityInstanceId(nameof(Schedule), "test-2"), + new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration("test-2", "test-orchestration", TimeSpan.FromMinutes(5)) + }) + }; + + this.entityClientMock + .Setup(c => c.GetAllEntitiesAsync(It.IsAny())) + .Returns(AsyncEnumerable.FromPages(new[] { new Page>(states.ToList(), null) })); + + // Act + var schedules = new List(); + await foreach (var schedule in this.client.ListSchedulesAsync(query)) + { + schedules.Add(schedule); + } + + // Assert + Assert.Equal(2, schedules.Count); + Assert.All(schedules, s => Assert.StartsWith("test-", s.ScheduleId)); + Assert.All(schedules, s => Assert.Equal(ScheduleStatus.Active, s.Status)); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs new file mode 100644 index 00000000..2bc91c40 --- /dev/null +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Entity; + +public class ScheduleTests +{ + private readonly Mock> mockLogger; + private readonly Mock mockContext; + private readonly Schedule schedule; + private readonly string scheduleId = "test-schedule"; + + public ScheduleTests() + { + this.mockLogger = new Mock>(); + this.mockContext = new Mock(); + this.schedule = new Schedule(this.mockLogger.Object); + } + + [Fact] + public void CreateSchedule_WithValidOptions_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + // Act + this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Assert + this.mockContext.Verify(c => c.SignalEntity( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + nameof(Schedule.RunSchedule), + It.IsAny(), + null), Times.Once); + } + + [Fact] + public void CreateSchedule_WithNullOptions_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => + this.schedule.CreateSchedule(this.mockContext.Object, null!)); + Assert.Contains("Schedule creation options cannot be null", ex.Message); + } + + [Fact] + public void PauseSchedule_WhenActive_PausesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Act + this.schedule.PauseSchedule(this.mockContext.Object); + + // Assert + this.mockContext.Verify(c => c.SignalEntity( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + nameof(Schedule.RunSchedule), + It.IsAny(), + null), Times.Once); + } + + [Fact] + public void PauseSchedule_WhenAlreadyPaused_ThrowsInvalidTransitionException() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + this.schedule.PauseSchedule(this.mockContext.Object); + + // Act & Assert + var ex = Assert.Throws(() => + this.schedule.PauseSchedule(this.mockContext.Object)); + } + + [Fact] + public void ResumeSchedule_WhenPaused_ResumesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + this.schedule.PauseSchedule(this.mockContext.Object); + + // Act + this.schedule.ResumeSchedule(this.mockContext.Object); + + // Assert + this.mockContext.Verify(c => c.SignalEntity( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + nameof(Schedule.RunSchedule), + It.IsAny(), + null), Times.Exactly(2)); + } + + [Fact] + public void ResumeSchedule_WhenActive_ThrowsInvalidTransitionException() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Act & Assert + var ex = Assert.Throws(() => + this.schedule.ResumeSchedule(this.mockContext.Object)); + } + + [Fact] + public void UpdateSchedule_WithValidOptions_UpdatesSchedule() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, createOptions); + + var updateOptions = new ScheduleUpdateOptions + { + Interval = TimeSpan.FromMinutes(10) + }; + + // Act + this.schedule.UpdateSchedule(this.mockContext.Object, updateOptions); + + // Assert + this.mockContext.Verify(c => c.SignalEntity( + It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + nameof(Schedule.RunSchedule), + It.IsAny(), + null), Times.Once); + } + + [Fact] + public void UpdateSchedule_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, createOptions); + + // Act & Assert + var ex = Assert.Throws(() => + this.schedule.UpdateSchedule(this.mockContext.Object, null!)); + Assert.Contains("Schedule update options cannot be null", ex.Message); + } + + [Fact] + public void RunSchedule_WhenNotActive_ThrowsInvalidOperationException() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + this.schedule.PauseSchedule(this.mockContext.Object); + + // Act & Assert + var ex = Assert.Throws(() => + this.schedule.RunSchedule(this.mockContext.Object, "token")); + Assert.Contains("Schedule must be in Active status to run", ex.Message); + } + + [Fact] + public void RunSchedule_WithInvalidToken_DoesNotRun() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Act + this.schedule.RunSchedule(this.mockContext.Object, "invalid-token"); + + // Assert + this.mockContext.Verify(c => c.ScheduleNewOrchestration( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs new file mode 100644 index 00000000..d1602c0a --- /dev/null +++ b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Models; + +public class ScheduleConfigurationTests +{ + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromMinutes(5); + + // Act + var config = new ScheduleConfiguration(scheduleId, orchestrationName, interval); + + // Assert + Assert.Equal(scheduleId, config.ScheduleId); + Assert.Equal(orchestrationName, config.OrchestrationName); + Assert.Equal(interval, config.Interval); + } + + [Theory] + [InlineData(null, "orchestration", "scheduleId cannot be null or empty")] + [InlineData("", "orchestration", "scheduleId cannot be null or empty")] + [InlineData("schedule", null, "orchestrationName cannot be null or empty")] + [InlineData("schedule", "", "orchestrationName cannot be null or empty")] + public void Constructor_WithInvalidParameters_ThrowsArgumentException(string scheduleId, string orchestrationName, string expectedMessage) + { + // Arrange + TimeSpan interval = TimeSpan.FromMinutes(5); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); + Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void Constructor_WithNonPositiveInterval_ThrowsArgumentException(int seconds) + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromSeconds(seconds); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); + Assert.Contains("Interval must be positive", ex.Message); + } + + [Fact] + public void Constructor_WithIntervalLessThanOneSecond_ThrowsArgumentException() + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromMilliseconds(500); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); + Assert.Contains("Interval must be at least 1 second", ex.Message); + } + + [Fact] + public void OrchestrationName_SetToNull_ThrowsArgumentException() + { + // Arrange + var config = new ScheduleConfiguration("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + + // Act & Assert + var ex = Assert.Throws(() => config.OrchestrationName = null!); + Assert.Contains("OrchestrationName cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Interval_SetToInvalidValue_ThrowsArgumentException() + { + // Arrange + var config = new ScheduleConfiguration("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + + // Act & Assert + var ex = Assert.Throws(() => config.Interval = TimeSpan.Zero); + Assert.Contains("Interval must be positive", ex.Message); + + ex = Assert.Throws(() => config.Interval = TimeSpan.FromMilliseconds(500)); + Assert.Contains("Interval must be at least 1 second", ex.Message); + } + + [Fact] + public void FromCreateOptions_WithValidOptions_CreatesConfiguration() + { + // Arrange + var options = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)) + { + OrchestrationInput = "test-input", + OrchestrationInstanceId = "test-instance", + StartAt = DateTimeOffset.UtcNow, + EndAt = DateTimeOffset.UtcNow.AddDays(1), + StartImmediatelyIfLate = true + }; + + // Act + var config = ScheduleConfiguration.FromCreateOptions(options); + + // Assert + Assert.Equal(options.ScheduleId, config.ScheduleId); + Assert.Equal(options.OrchestrationName, config.OrchestrationName); + Assert.Equal(options.Interval, config.Interval); + Assert.Equal(options.OrchestrationInput, config.OrchestrationInput); + Assert.Equal(options.OrchestrationInstanceId, config.OrchestrationInstanceId); + Assert.Equal(options.StartAt, config.StartAt); + Assert.Equal(options.EndAt, config.EndAt); + Assert.Equal(options.StartImmediatelyIfLate, config.StartImmediatelyIfLate); + } + + [Fact] + public void FromCreateOptions_WithNullOptions_ThrowsArgumentNullException() + { + // Act & Assert + var ex = Assert.Throws(() => ScheduleConfiguration.FromCreateOptions(null!)); + Assert.Equal("createOptions", ex.ParamName); + } + + [Fact] + public void Update_WithValidOptions_UpdatesConfiguration() + { + // Arrange + var config = new ScheduleConfiguration("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationName = "new-orchestration", + OrchestrationInput = "new-input", + OrchestrationInstanceId = "new-instance", + StartAt = DateTimeOffset.UtcNow, + EndAt = DateTimeOffset.UtcNow.AddDays(1), + Interval = TimeSpan.FromMinutes(10), + StartImmediatelyIfLate = true + }; + + // Act + var updatedFields = config.Update(updateOptions); + + // Assert + Assert.Equal(updateOptions.OrchestrationName, config.OrchestrationName); + Assert.Equal(updateOptions.OrchestrationInput, config.OrchestrationInput); + Assert.Equal(updateOptions.OrchestrationInstanceId, config.OrchestrationInstanceId); + Assert.Equal(updateOptions.StartAt, config.StartAt); + Assert.Equal(updateOptions.EndAt, config.EndAt); + Assert.Equal(updateOptions.Interval, config.Interval); + Assert.Equal(updateOptions.StartImmediatelyIfLate, config.StartImmediatelyIfLate); + + Assert.Contains(nameof(ScheduleConfiguration.OrchestrationName), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.OrchestrationInput), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.OrchestrationInstanceId), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.StartAt), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.EndAt), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.Interval), updatedFields); + Assert.Contains(nameof(ScheduleConfiguration.StartImmediatelyIfLate), updatedFields); + } + + [Fact] + public void Update_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var config = new ScheduleConfiguration("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + + // Act & Assert + var ex = Assert.Throws(() => config.Update(null!)); + Assert.Equal("updateOptions", ex.ParamName); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs new file mode 100644 index 00000000..8750bfdf --- /dev/null +++ b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Models; + +public class ScheduleCreationOptionsTests +{ + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromMinutes(5); + + // Act + var options = new ScheduleCreationOptions(scheduleId, orchestrationName, interval); + + // Assert + Assert.Equal(scheduleId, options.ScheduleId); + Assert.Equal(orchestrationName, options.OrchestrationName); + Assert.Equal(interval, options.Interval); + Assert.Null(options.OrchestrationInput); + Assert.Null(options.OrchestrationInstanceId); + Assert.Null(options.StartAt); + Assert.Null(options.EndAt); + Assert.False(options.StartImmediatelyIfLate); + } + + [Theory] + [InlineData(null, "orchestration", "scheduleId cannot be null or empty")] + [InlineData("", "orchestration", "scheduleId cannot be null or empty")] + [InlineData("schedule", null, "orchestrationName cannot be null or empty")] + [InlineData("schedule", "", "orchestrationName cannot be null or empty")] + public void Constructor_WithInvalidParameters_ThrowsArgumentException(string scheduleId, string orchestrationName, string expectedMessage) + { + // Arrange + TimeSpan interval = TimeSpan.FromMinutes(5); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); + Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void Constructor_WithNonPositiveInterval_ThrowsArgumentException(int seconds) + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromSeconds(seconds); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); + Assert.Contains("Interval must be positive", ex.Message); + } + + [Fact] + public void Constructor_WithIntervalLessThanOneSecond_ThrowsArgumentException() + { + // Arrange + string scheduleId = "test-schedule"; + string orchestrationName = "test-orchestration"; + TimeSpan interval = TimeSpan.FromMilliseconds(500); + + // Act & Assert + var ex = Assert.Throws(() => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); + Assert.Contains("Interval must be at least 1 second", ex.Message); + } + + [Fact] + public void Properties_SetAndGetCorrectly() + { + // Arrange + var options = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + var now = DateTimeOffset.UtcNow; + + // Act + options = options with + { + OrchestrationInput = "test-input", + OrchestrationInstanceId = "test-instance", + StartAt = now, + EndAt = now.AddDays(1), + StartImmediatelyIfLate = true + }; + + // Assert + Assert.Equal("test-input", options.OrchestrationInput); + Assert.Equal("test-instance", options.OrchestrationInstanceId); + Assert.Equal(now, options.StartAt); + Assert.Equal(now.AddDays(1), options.EndAt); + Assert.True(options.StartImmediatelyIfLate); + } + + [Fact] + public void WithOperator_CreatesNewInstance() + { + // Arrange + var original = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); + var now = DateTimeOffset.UtcNow; + + // Act + var modified = original with + { + OrchestrationInput = "test-input", + StartAt = now + }; + + // Assert + Assert.Equal(original.ScheduleId, modified.ScheduleId); + Assert.Equal(original.OrchestrationName, modified.OrchestrationName); + Assert.Equal(original.Interval, modified.Interval); + Assert.Equal("test-input", modified.OrchestrationInput); + Assert.Equal(now, modified.StartAt); + Assert.NotSame(original, modified); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleStateTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleStateTests.cs new file mode 100644 index 00000000..ebc3fad7 --- /dev/null +++ b/test/ScheduledTasks.Tests/Models/ScheduleStateTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Models; + +public class ScheduleStateTests +{ + [Fact] + public void DefaultConstructor_InitializesCorrectly() + { + // Act + var state = new ScheduleState(); + + // Assert + Assert.Equal(ScheduleStatus.Uninitialized, state.Status); + Assert.NotNull(state.ExecutionToken); + Assert.NotEmpty(state.ExecutionToken); + Assert.Null(state.LastRunAt); + Assert.Null(state.NextRunAt); + Assert.Null(state.ScheduleCreatedAt); + Assert.Null(state.ScheduleLastModifiedAt); + Assert.Null(state.ScheduleConfiguration); + } + + [Fact] + public void RefreshScheduleRunExecutionToken_GeneratesNewToken() + { + // Arrange + var state = new ScheduleState(); + string originalToken = state.ExecutionToken; + + // Act + state.RefreshScheduleRunExecutionToken(); + + // Assert + Assert.NotEqual(originalToken, state.ExecutionToken); + Assert.NotEmpty(state.ExecutionToken); + } + + [Fact] + public void RefreshScheduleRunExecutionToken_GeneratesUniqueTokens() + { + // Arrange + var state = new ScheduleState(); + var tokens = new HashSet(); + + // Act + for (int i = 0; i < 100; i++) + { + state.RefreshScheduleRunExecutionToken(); + tokens.Add(state.ExecutionToken); + } + + // Assert + Assert.Equal(100, tokens.Count); // All tokens should be unique + } + + [Fact] + public void Properties_SetAndGetCorrectly() + { + // Arrange + var state = new ScheduleState(); + var now = DateTimeOffset.UtcNow; + var config = new ScheduleConfiguration("test-id", "test-orchestration", TimeSpan.FromMinutes(5)); + + // Act + state.Status = ScheduleStatus.Active; + state.LastRunAt = now; + state.NextRunAt = now.AddMinutes(5); + state.ScheduleCreatedAt = now; + state.ScheduleLastModifiedAt = now; + state.ScheduleConfiguration = config; + + // Assert + Assert.Equal(ScheduleStatus.Active, state.Status); + Assert.Equal(now, state.LastRunAt); + Assert.Equal(now.AddMinutes(5), state.NextRunAt); + Assert.Equal(now, state.ScheduleCreatedAt); + Assert.Equal(now, state.ScheduleLastModifiedAt); + Assert.Same(config, state.ScheduleConfiguration); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs b/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs new file mode 100644 index 00000000..21ee14eb --- /dev/null +++ b/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Entities; +using Moq; +using Xunit; + +namespace Microsoft.DurableTask.ScheduledTasks.Tests.Orchestrations; + +public class ExecuteScheduleOperationOrchestratorTests +{ + private readonly Mock mockContext; + private readonly Mock mockEntityClient; + private readonly ExecuteScheduleOperationOrchestrator orchestrator; + + public ExecuteScheduleOperationOrchestratorTests() + { + this.mockContext = new Mock(); + this.mockEntityClient = new Mock(); + this.mockContext.Setup(c => c.Entities).Returns(this.mockEntityClient.Object); + this.orchestrator = new ExecuteScheduleOperationOrchestrator(); + } + + [Fact] + public async Task RunAsync_ValidRequest_CallsEntityOperation() + { + // Arrange + var entityId = new EntityInstanceId(nameof(Schedule), "test-schedule"); + var operationName = "TestOperation"; + var input = new { TestData = "test" }; + var expectedResult = new { Result = "success" }; + var request = new ScheduleOperationRequest(entityId, operationName, input); + + this.mockEntityClient + .Setup(e => e.CallEntityAsync(entityId, operationName, input, default)) + .ReturnsAsync(expectedResult); + + // Act + var result = await this.orchestrator.RunAsync(this.mockContext.Object, request); + + // Assert + Assert.Equal(expectedResult, result); + this.mockEntityClient.Verify( + e => e.CallEntityAsync(entityId, operationName, input, default), + Times.Once); + } + + [Fact] + public async Task RunAsync_NullInput_CallsEntityOperation() + { + // Arrange + var entityId = new EntityInstanceId(nameof(Schedule), "test-schedule"); + var operationName = "TestOperation"; + var expectedResult = new { Result = "success" }; + var request = new ScheduleOperationRequest(entityId, operationName); + + this.mockEntityClient + .Setup(e => e.CallEntityAsync(entityId, operationName, null, default)) + .ReturnsAsync(expectedResult); + + // Act + var result = await this.orchestrator.RunAsync(this.mockContext.Object, request); + + // Assert + Assert.Equal(expectedResult, result); + this.mockEntityClient.Verify( + e => e.CallEntityAsync(entityId, operationName, null, default), + Times.Once); + } + + [Fact] + public async Task RunAsync_EntityOperationFails_PropagatesException() + { + // Arrange + var entityId = new EntityInstanceId(nameof(Schedule), "test-schedule"); + var operationName = "TestOperation"; + var request = new ScheduleOperationRequest(entityId, operationName); + var expectedException = new InvalidOperationException("Test exception"); + + this.mockEntityClient + .Setup(e => e.CallEntityAsync(entityId, operationName, null, default)) + .ThrowsAsync(expectedException); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => this.orchestrator.RunAsync(this.mockContext.Object, request)); + Assert.Equal(expectedException.Message, exception.Message); + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj new file mode 100644 index 00000000..1f57ddb1 --- /dev/null +++ b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj @@ -0,0 +1,30 @@ + + + + + net6.0 + enable + enable + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file From 1077a0c615cd00beac7f3a87f380e678e2a54c54 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:39:12 -0800 Subject: [PATCH 169/203] test p2 --- .../Client/ScheduleClientImplTests.cs | 31 +++++----- .../Client/ScheduledTaskClientImplTests.cs | 58 ++----------------- 2 files changed, 21 insertions(+), 68 deletions(-) diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs index ffa120a0..a0a8e31c 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs @@ -12,11 +12,11 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; public class ScheduleClientImplTests { - private readonly Mock durableTaskClientMock; - private readonly Mock loggerMock; - private readonly Mock entityClientMock; - private readonly ScheduleClientImpl client; - private readonly string scheduleId = "test-schedule"; + readonly Mock durableTaskClientMock; + readonly Mock loggerMock; + readonly Mock entityClientMock; + readonly ScheduleClientImpl client; + readonly string scheduleId = "test-schedule"; public ScheduleClientImplTests() { @@ -31,7 +31,7 @@ public ScheduleClientImplTests() public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => new ScheduleClientImpl(null!, this.scheduleId, this.loggerMock.Object)); Assert.Equal("client", ex.ParamName); } @@ -42,7 +42,7 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string invalidScheduleId) { // Act & Assert - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => new ScheduleClientImpl(this.durableTaskClientMock.Object, invalidScheduleId, this.loggerMock.Object)); Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -51,7 +51,7 @@ public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string inv public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => new ScheduleClientImpl(this.durableTaskClientMock.Object, this.scheduleId, null!)); Assert.Equal("logger", ex.ParamName); } @@ -112,7 +112,7 @@ public async Task DeleteAsync_ExecutesDeleteOperation() this.durableTaskClientMock .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata()); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act await this.client.DeleteAsync(); @@ -124,7 +124,7 @@ public async Task DeleteAsync_ExecutesDeleteOperation() It.Is(r => r.EntityId.Name == nameof(Schedule) && r.EntityId.Key == this.scheduleId && - r.OperationName == nameof(Schedule.DeleteSchedule)), + r.OperationName == "delete"), It.IsAny()), Times.Once); } @@ -144,7 +144,7 @@ public async Task PauseAsync_ExecutesPauseOperation() this.durableTaskClientMock .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata()); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act await this.client.PauseAsync(); @@ -176,7 +176,7 @@ public async Task ResumeAsync_ExecutesResumeOperation() this.durableTaskClientMock .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata()); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act await this.client.ResumeAsync(); @@ -213,7 +213,7 @@ public async Task UpdateAsync_ExecutesUpdateOperation() this.durableTaskClientMock .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata()); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act await this.client.UpdateAsync(updateOptions); @@ -225,8 +225,7 @@ public async Task UpdateAsync_ExecutesUpdateOperation() It.Is(r => r.EntityId.Name == nameof(Schedule) && r.EntityId.Key == this.scheduleId && - r.OperationName == nameof(Schedule.UpdateSchedule) && - r.OperationInput == updateOptions), + r.OperationName == nameof(Schedule.UpdateSchedule)), It.IsAny()), Times.Once); } @@ -239,4 +238,4 @@ public async Task UpdateAsync_WithNullOptions_ThrowsArgumentNullException() () => this.client.UpdateAsync(null!)); Assert.Equal("updateOptions", ex.ParamName); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs index bb252f40..db7909aa 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -12,10 +12,10 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; public class ScheduledTaskClientImplTests { - private readonly Mock durableTaskClientMock; - private readonly Mock loggerMock; - private readonly Mock entityClientMock; - private readonly ScheduledTaskClientImpl client; + readonly Mock durableTaskClientMock; + readonly Mock loggerMock; + readonly Mock entityClientMock; + readonly ScheduledTaskClientImpl client; public ScheduledTaskClientImplTests() { @@ -82,7 +82,7 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() this.durableTaskClientMock .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata()); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act var scheduleClient = await this.client.CreateScheduleAsync(options); @@ -156,50 +156,4 @@ public async Task GetScheduleAsync_WhenNotExists_ReturnsNull() // Assert Assert.Null(description); } - - [Fact] - public async Task ListSchedulesAsync_ReturnsSchedules() - { - // Arrange - var query = new ScheduleQuery - { - ScheduleIdPrefix = "test", - Status = ScheduleStatus.Active, - PageSize = 10 - }; - - var states = new[] - { - new EntityMetadata( - new EntityInstanceId(nameof(Schedule), "test-1"), - new ScheduleState - { - Status = ScheduleStatus.Active, - ScheduleConfiguration = new ScheduleConfiguration("test-1", "test-orchestration", TimeSpan.FromMinutes(5)) - }), - new EntityMetadata( - new EntityInstanceId(nameof(Schedule), "test-2"), - new ScheduleState - { - Status = ScheduleStatus.Active, - ScheduleConfiguration = new ScheduleConfiguration("test-2", "test-orchestration", TimeSpan.FromMinutes(5)) - }) - }; - - this.entityClientMock - .Setup(c => c.GetAllEntitiesAsync(It.IsAny())) - .Returns(AsyncEnumerable.FromPages(new[] { new Page>(states.ToList(), null) })); - - // Act - var schedules = new List(); - await foreach (var schedule in this.client.ListSchedulesAsync(query)) - { - schedules.Add(schedule); - } - - // Assert - Assert.Equal(2, schedules.Count); - Assert.All(schedules, s => Assert.StartsWith("test-", s.ScheduleId)); - Assert.All(schedules, s => Assert.Equal(ScheduleStatus.Active, s.Status)); - } -} \ No newline at end of file +} \ No newline at end of file From 2e0917c0f7116ddd29ac575238ce3da0b6ba1123 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:41:24 -0800 Subject: [PATCH 170/203] remove ScheduleAlreadyExistsException --- .../ScheduleAlreadyExistException.cs | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs diff --git a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs b/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs deleted file mode 100644 index b43b2873..00000000 --- a/src/ScheduledTasks/Exception/ScheduleAlreadyExistException.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.DurableTask.ScheduledTasks; - -/// -/// Exception thrown when attempting to create a schedule with an ID that already exists. -/// -public class ScheduleAlreadyExistsException : InvalidOperationException -{ - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that already exists. - public ScheduleAlreadyExistsException(string scheduleId) - : base($"A schedule with ID '{scheduleId}' already exists.") - { - this.ScheduleId = scheduleId; - } - - /// - /// Initializes a new instance of the class. - /// - /// The ID of the schedule that already exists. - /// The exception that is the cause of the current exception. - public ScheduleAlreadyExistsException(string scheduleId, Exception innerException) - : base($"A schedule with ID '{scheduleId}' already exists.", innerException) - { - this.ScheduleId = scheduleId; - } - - /// - /// Gets the ID of the schedule that already exists. - /// - public string ScheduleId { get; } -} From 69cb2f8f84a2104e4a84668a24baa7cd1c8bcf28 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:06:05 -0800 Subject: [PATCH 171/203] add createasync in scheduleclient --- src/ScheduledTasks/Client/ScheduleClient.cs | 8 ++++ .../Client/ScheduleClientImpl.cs | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/ScheduledTasks/Client/ScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClient.cs index 68cb1f3b..979c0765 100644 --- a/src/ScheduledTasks/Client/ScheduleClient.cs +++ b/src/ScheduledTasks/Client/ScheduleClient.cs @@ -22,6 +22,14 @@ protected ScheduleClient(string scheduleId) /// public string ScheduleId { get; } + /// + /// Creates or update a schedule with the specified configuration. + /// + /// The options for creating the schedule. + /// The cancellation token that can be used to cancel the operation. + /// A task that completes when the schedule has been created. + public abstract Task CreateAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); + /// /// Retrieves the current details of this schedule. /// diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index 69ffb298..10d03465 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -36,6 +36,43 @@ public ScheduleClientImpl(DurableTaskClient client, string scheduleId, ILogger l /// EntityInstanceId EntityId { get; } + /// + public override async Task CreateAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default) + { + try + { + Check.NotNull(creationOptions, nameof(creationOptions)); + + ScheduleOperationRequest request = new ScheduleOperationRequest(this.EntityId, nameof(Schedule.CreateSchedule), creationOptions); + string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( + new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), + request, + cancellation); + + // Wait for the orchestration to complete + OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + + if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) + { + throw new InvalidOperationException($"Failed to create schedule '{this.ScheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); + } + } + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) + { + this.logger.ClientError( + nameof(this.CreateAsync), + this.ScheduleId, + ex); + + throw; + } + } + /// public override async Task DescribeAsync(CancellationToken cancellation = default) { From ca31f1f3da10a56760516bff4223659f3b151bc0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:24:57 -0800 Subject: [PATCH 172/203] fix unit tests --- .../Client/ScheduleClientImplTests.cs | 50 ++++---- .../Client/ScheduledTaskClientImplTests.cs | 111 +++++++++++++----- .../Entity/ScheduleTests.cs | 14 +-- .../Models/ScheduleConfigurationTests.cs | 20 ++-- .../Models/ScheduleCreationOptionsTests.cs | 20 ++-- ...ecuteScheduleOperationOrchestratorTests.cs | 5 +- 6 files changed, 141 insertions(+), 79 deletions(-) diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs index a0a8e31c..5125a28b 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs @@ -12,19 +12,19 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; public class ScheduleClientImplTests { - readonly Mock durableTaskClientMock; - readonly Mock loggerMock; - readonly Mock entityClientMock; + readonly Mock durableTaskClient; + readonly Mock entityClient; + readonly Mock logger; readonly ScheduleClientImpl client; readonly string scheduleId = "test-schedule"; public ScheduleClientImplTests() { - this.durableTaskClientMock = new Mock(); - this.loggerMock = new Mock(); - this.entityClientMock = new Mock(); - this.durableTaskClientMock.Setup(c => c.Entities).Returns(this.entityClientMock.Object); - this.client = new ScheduleClientImpl(this.durableTaskClientMock.Object, this.scheduleId, this.loggerMock.Object); + this.durableTaskClient = new Mock("test", MockBehavior.Strict); + this.entityClient = new Mock("test", MockBehavior.Strict); + this.logger = new Mock(MockBehavior.Loose); + this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); + this.client = new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, this.logger.Object); } [Fact] @@ -32,7 +32,7 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert var ex = Assert.Throws(() => - new ScheduleClientImpl(null!, this.scheduleId, this.loggerMock.Object)); + new ScheduleClientImpl(null!, this.scheduleId, this.logger.Object)); Assert.Equal("client", ex.ParamName); } @@ -43,7 +43,7 @@ public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string inv { // Act & Assert var ex = Assert.Throws(() => - new ScheduleClientImpl(this.durableTaskClientMock.Object, invalidScheduleId, this.loggerMock.Object)); + new ScheduleClientImpl(this.durableTaskClient.Object, invalidScheduleId, this.logger.Object)); Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -52,7 +52,7 @@ public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert var ex = Assert.Throws(() => - new ScheduleClientImpl(this.durableTaskClientMock.Object, this.scheduleId, null!)); + new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, null!)); Assert.Equal("logger", ex.ParamName); } @@ -66,7 +66,7 @@ public async Task DescribeAsync_WhenExists_ReturnsDescription() ScheduleConfiguration = new ScheduleConfiguration(this.scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) }; - this.entityClientMock + this.entityClient .Setup(c => c.GetEntityAsync( It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), It.IsAny())) @@ -86,7 +86,7 @@ public async Task DescribeAsync_WhenExists_ReturnsDescription() public async Task DescribeAsync_WhenNotExists_ThrowsScheduleNotFoundException() { // Arrange - this.entityClientMock + this.entityClient .Setup(c => c.GetEntityAsync( It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), It.IsAny())) @@ -103,14 +103,14 @@ public async Task DeleteAsync_ExecutesDeleteOperation() // Arrange string instanceId = "test-instance"; - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.IsAny(), It.IsAny())) .ReturnsAsync(instanceId); - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); @@ -118,7 +118,7 @@ public async Task DeleteAsync_ExecutesDeleteOperation() await this.client.DeleteAsync(); // Assert - this.durableTaskClientMock.Verify( + this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => @@ -135,14 +135,14 @@ public async Task PauseAsync_ExecutesPauseOperation() // Arrange string instanceId = "test-instance"; - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.IsAny(), It.IsAny())) .ReturnsAsync(instanceId); - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); @@ -150,7 +150,7 @@ public async Task PauseAsync_ExecutesPauseOperation() await this.client.PauseAsync(); // Assert - this.durableTaskClientMock.Verify( + this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => @@ -167,14 +167,14 @@ public async Task ResumeAsync_ExecutesResumeOperation() // Arrange string instanceId = "test-instance"; - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.IsAny(), It.IsAny())) .ReturnsAsync(instanceId); - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); @@ -182,7 +182,7 @@ public async Task ResumeAsync_ExecutesResumeOperation() await this.client.ResumeAsync(); // Assert - this.durableTaskClientMock.Verify( + this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => @@ -204,14 +204,14 @@ public async Task UpdateAsync_ExecutesUpdateOperation() Interval = TimeSpan.FromMinutes(10) }; - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.IsAny(), It.IsAny())) .ReturnsAsync(instanceId); - this.durableTaskClientMock + this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); @@ -219,7 +219,7 @@ public async Task UpdateAsync_ExecutesUpdateOperation() await this.client.UpdateAsync(updateOptions); // Assert - this.durableTaskClientMock.Verify( + this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs index db7909aa..1de5fc82 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; @@ -12,25 +13,25 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; public class ScheduledTaskClientImplTests { - readonly Mock durableTaskClientMock; - readonly Mock loggerMock; - readonly Mock entityClientMock; + readonly Mock durableTaskClient; + readonly Mock entityClient; + readonly Mock> logger; readonly ScheduledTaskClientImpl client; public ScheduledTaskClientImplTests() { - this.durableTaskClientMock = new Mock(); - this.loggerMock = new Mock(); - this.entityClientMock = new Mock(); - this.durableTaskClientMock.Setup(c => c.Entities).Returns(this.entityClientMock.Object); - this.client = new ScheduledTaskClientImpl(this.durableTaskClientMock.Object, this.loggerMock.Object); + this.durableTaskClient = new Mock("test", MockBehavior.Strict); + this.entityClient = new Mock("test", MockBehavior.Strict); + this.logger = new Mock>(MockBehavior.Loose); + this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); + this.client = new ScheduledTaskClientImpl(this.durableTaskClient.Object, this.logger.Object); } [Fact] public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.loggerMock.Object)); + var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.logger.Object)); Assert.Equal("durableTaskClient", ex.ParamName); } @@ -38,7 +39,7 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => new ScheduledTaskClientImpl(this.durableTaskClientMock.Object, null!)); + var ex = Assert.Throws(() => new ScheduledTaskClientImpl(this.durableTaskClient.Object, null!)); Assert.Equal("logger", ex.ParamName); } @@ -73,15 +74,20 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() var options = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); string instanceId = "test-instance"; - this.durableTaskClientMock - .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + this.durableTaskClient + .Setup(x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), - It.IsAny(), - It.IsAny())) + It.Is(r => + r.EntityId.Name == nameof(Schedule) && + r.EntityId.Key == options.ScheduleId && + r.OperationName == nameof(Schedule.CreateSchedule) && + r.Input == options), + null, + default)) .ReturnsAsync(instanceId); - this.durableTaskClientMock - .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + this.durableTaskClient + .Setup(x => x.WaitForInstanceCompletionAsync(instanceId, true, default)) .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); // Act @@ -91,14 +97,16 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() Assert.NotNull(scheduleClient); Assert.Equal(options.ScheduleId, scheduleClient.ScheduleId); - this.durableTaskClientMock.Verify( - c => c.ScheduleNewOrchestrationInstanceAsync( + this.durableTaskClient.Verify( + x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => r.EntityId.Name == nameof(Schedule) && r.EntityId.Key == options.ScheduleId && - r.OperationName == nameof(Schedule.CreateSchedule)), - It.IsAny()), + r.OperationName == nameof(Schedule.CreateSchedule) && + r.Input == options), + null, + default), Times.Once); } @@ -122,10 +130,11 @@ public async Task GetScheduleAsync_WhenExists_ReturnsDescription() ScheduleConfiguration = new ScheduleConfiguration(scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) }; - this.entityClientMock - .Setup(c => c.GetEntityAsync( + this.entityClient + .Setup(x => x.GetEntityAsync( It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), - It.IsAny())) + true, + default)) .ReturnsAsync(new EntityMetadata(new EntityInstanceId(nameof(Schedule), scheduleId), state)); // Act @@ -144,11 +153,12 @@ public async Task GetScheduleAsync_WhenNotExists_ReturnsNull() // Arrange string scheduleId = "test-schedule"; - this.entityClientMock - .Setup(c => c.GetEntityAsync( + this.entityClient + .Setup(x => x.GetEntityAsync( It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), - It.IsAny())) - .ReturnsAsync((EntityMetadata)null!); + true, + default)) + .ReturnsAsync((EntityMetadata)null); // Act var description = await this.client.GetScheduleAsync(scheduleId); @@ -156,4 +166,51 @@ public async Task GetScheduleAsync_WhenNotExists_ReturnsNull() // Assert Assert.Null(description); } + + [Fact] + public async Task ListSchedulesAsync_ReturnsSchedules() + { + // Arrange + var query = new ScheduleQuery + { + ScheduleIdPrefix = "test", + Status = ScheduleStatus.Active, + PageSize = 10 + }; + + var states = new[] + { + new EntityMetadata( + new EntityInstanceId(nameof(Schedule), "test-1"), + new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration("test-1", "test-orchestration", TimeSpan.FromMinutes(5)) + }), + new EntityMetadata( + new EntityInstanceId(nameof(Schedule), "test-2"), + new ScheduleState + { + Status = ScheduleStatus.Active, + ScheduleConfiguration = new ScheduleConfiguration("test-2", "test-orchestration", TimeSpan.FromMinutes(5)) + }) + }; + + this.entityClient + .Setup(x => x.GetAllEntitiesAsync(It.IsAny())) + .Returns(Pageable.Create>((continuation, pageSize, cancellation) => + Task.FromResult(new Page>(states.ToList(), null)))); + + // Act + var schedules = new List(); + await foreach (var schedule in this.client.ListSchedulesAsync(query)) + { + schedules.Add(schedule); + } + + // Assert + Assert.Equal(2, schedules.Count); + Assert.All(schedules, s => Assert.StartsWith("test-", s.ScheduleId)); + Assert.All(schedules, s => Assert.Equal(ScheduleStatus.Active, s.Status)); + } } \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 2bc91c40..78a704d4 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -10,15 +10,15 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Entity; public class ScheduleTests { - private readonly Mock> mockLogger; - private readonly Mock mockContext; - private readonly Schedule schedule; - private readonly string scheduleId = "test-schedule"; + readonly Mock> mockLogger; + readonly Mock mockContext; + readonly Schedule schedule; + readonly string scheduleId = "test-schedule"; public ScheduleTests() { - this.mockLogger = new Mock>(); - this.mockContext = new Mock(); + this.mockLogger = new Mock>(MockBehavior.Loose); + this.mockContext = new Mock(MockBehavior.Strict); this.schedule = new Schedule(this.mockLogger.Object); } @@ -203,4 +203,4 @@ public void RunSchedule_WithInvalidToken_DoesNotRun() It.IsAny(), It.IsAny()), Times.Never); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs index d1602c0a..b5bf2881 100644 --- a/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs +++ b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs @@ -25,18 +25,18 @@ public void Constructor_WithValidParameters_CreatesInstance() } [Theory] - [InlineData(null, "orchestration", "scheduleId cannot be null or empty")] - [InlineData("", "orchestration", "scheduleId cannot be null or empty")] - [InlineData("schedule", null, "orchestrationName cannot be null or empty")] - [InlineData("schedule", "", "orchestrationName cannot be null or empty")] - public void Constructor_WithInvalidParameters_ThrowsArgumentException(string scheduleId, string orchestrationName, string expectedMessage) + [InlineData(null, "orchestration", typeof(ArgumentNullException), "Value cannot be null")] + [InlineData("", "orchestration", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] + [InlineData("schedule", null, typeof(ArgumentNullException), "Value cannot be null")] + [InlineData("schedule", "", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] + public void Constructor_WithInvalidParameters_ThrowsException(string scheduleId, string orchestrationName, Type expectedExceptionType, string expectedMessage) { // Arrange TimeSpan interval = TimeSpan.FromMinutes(5); // Act & Assert - var ex = Assert.Throws(() => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); - Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); + var ex = Assert.Throws(expectedExceptionType, () => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); + Assert.Contains(expectedMessage, ex.Message); } [Theory] @@ -74,8 +74,8 @@ public void OrchestrationName_SetToNull_ThrowsArgumentException() var config = new ScheduleConfiguration("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); // Act & Assert - var ex = Assert.Throws(() => config.OrchestrationName = null!); - Assert.Contains("OrchestrationName cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + var ex = Assert.Throws(() => config.OrchestrationName = null!); + Assert.Contains("Value cannot be null.", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -174,4 +174,4 @@ public void Update_WithNullOptions_ThrowsArgumentNullException() var ex = Assert.Throws(() => config.Update(null!)); Assert.Equal("updateOptions", ex.ParamName); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs index 8750bfdf..10d21a6b 100644 --- a/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs +++ b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs @@ -30,18 +30,22 @@ public void Constructor_WithValidParameters_CreatesInstance() } [Theory] - [InlineData(null, "orchestration", "scheduleId cannot be null or empty")] - [InlineData("", "orchestration", "scheduleId cannot be null or empty")] - [InlineData("schedule", null, "orchestrationName cannot be null or empty")] - [InlineData("schedule", "", "orchestrationName cannot be null or empty")] - public void Constructor_WithInvalidParameters_ThrowsArgumentException(string scheduleId, string orchestrationName, string expectedMessage) + [InlineData(null, "orchestration", typeof(ArgumentNullException), "Value cannot be null")] + [InlineData("", "orchestration", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] + [InlineData("schedule", null, typeof(ArgumentNullException), "Value cannot be null.")] + [InlineData("schedule", "", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] + public void Constructor_WithInvalidParameters_ThrowsArgumentException( + string scheduleId, + string orchestrationName, + Type expectedExceptionType, + string expectedMessage) { // Arrange TimeSpan interval = TimeSpan.FromMinutes(5); // Act & Assert - var ex = Assert.Throws(() => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); - Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); + var exception = Assert.Throws(expectedExceptionType, () => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); + Assert.Contains(expectedMessage, exception.Message); } [Theory] @@ -119,4 +123,4 @@ public void WithOperator_CreatesNewInstance() Assert.Equal(now, modified.StartAt); Assert.NotSame(original, modified); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs b/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs index 21ee14eb..89b6024d 100644 --- a/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs +++ b/test/ScheduledTasks.Tests/Orchestrations/ExecuteScheduleOperationOrchestratorTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; using Moq; using Xunit; @@ -16,8 +17,8 @@ public class ExecuteScheduleOperationOrchestratorTests public ExecuteScheduleOperationOrchestratorTests() { - this.mockContext = new Mock(); - this.mockEntityClient = new Mock(); + this.mockContext = new Mock(MockBehavior.Strict); + this.mockEntityClient = new Mock(MockBehavior.Loose); this.mockContext.Setup(c => c.Entities).Returns(this.mockEntityClient.Object); this.orchestrator = new ExecuteScheduleOperationOrchestrator(); } From f43af2b862512ba537d0d9199652c1fd226f8d27 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:39:12 -0800 Subject: [PATCH 173/203] fix --- src/ScheduledTasks/Entity/Schedule.cs | 8 ++++++ .../Entity/ScheduleTests.cs | 25 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 1744abc4..6f8192f9 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -110,6 +110,14 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche this.State.RefreshScheduleRunExecutionToken(); this.logger.UpdatedSchedule(this.State.ScheduleConfiguration.ScheduleId); + + if (this.State.Status == ScheduleStatus.Active) + { + context.SignalEntity( + new EntityInstanceId(nameof(Schedule), this.State.ScheduleConfiguration.ScheduleId), + nameof(this.RunSchedule), + this.State.ExecutionToken); + } } catch (Exception ex) { diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 78a704d4..762f3535 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -5,21 +5,38 @@ using Microsoft.Extensions.Logging; using Moq; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DurableTask.ScheduledTasks.Tests.Entity; public class ScheduleTests { - readonly Mock> mockLogger; readonly Mock mockContext; readonly Schedule schedule; readonly string scheduleId = "test-schedule"; + readonly TestLogger logger; - public ScheduleTests() + public ScheduleTests(ITestOutputHelper output) { - this.mockLogger = new Mock>(MockBehavior.Loose); this.mockContext = new Mock(MockBehavior.Strict); - this.schedule = new Schedule(this.mockLogger.Object); + this.logger = new TestLogger(); + this.schedule = new Schedule(this.logger); + } + + // Simple TestLogger implementation for capturing logs + class TestLogger : ILogger + { + public List<(LogLevel Level, string Message)> Logs { get; } = new(); + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + this.Logs.Add((logLevel, message)); + } } [Fact] From 03f5b45a226fd7a8e66975a46477a2072615e203 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:44:04 -0800 Subject: [PATCH 174/203] fb --- src/ScheduledTasks/Entity/Schedule.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 6f8192f9..8d0b9a35 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -41,7 +41,7 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc } this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); - this.TryStatusTransition(nameof(this.CreateSchedule), ScheduleStatus.Active); + this.State.Status = ScheduleStatus.Active; this.State.RefreshScheduleRunExecutionToken(); this.State.ScheduleCreatedAt = this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; @@ -142,7 +142,7 @@ public void PauseSchedule(TaskEntityContext context) Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); // Transition to Paused state - this.TryStatusTransition(nameof(this.PauseSchedule), ScheduleStatus.Paused); + this.State.Status = ScheduleStatus.Paused; this.State.NextRunAt = null; this.State.RefreshScheduleRunExecutionToken(); @@ -171,7 +171,7 @@ public void ResumeSchedule(TaskEntityContext context) Verify.NotNull(this.State.ScheduleConfiguration, nameof(this.State.ScheduleConfiguration)); - this.TryStatusTransition(nameof(this.ResumeSchedule), ScheduleStatus.Active); + this.State.Status = ScheduleStatus.Active; this.State.NextRunAt = null; this.logger.ResumedSchedule(this.State.ScheduleConfiguration.ScheduleId); @@ -268,17 +268,6 @@ bool CanTransitionTo(string operationName, ScheduleStatus targetStatus) return ScheduleTransitions.IsValidTransition(operationName, this.State.Status, targetStatus); } - void TryStatusTransition(string operationName, ScheduleStatus to) - { - if (!this.CanTransitionTo(operationName, to)) - { - this.logger.ScheduleOperationError(this.State.ScheduleConfiguration!.ScheduleId, nameof(this.TryStatusTransition), $"Invalid state transition from {this.State.Status} to {to}"); - throw new ScheduleInvalidTransitionException(this.State.ScheduleConfiguration!.ScheduleId, this.State.Status, to, operationName); - } - - this.State.Status = to; - } - DateTimeOffset DetermineNextRunTime(ScheduleConfiguration scheduleConfig) { if (this.State.NextRunAt.HasValue) From 457bb888f88686c62ed8d17eca579f133166aaa5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:55:27 -0800 Subject: [PATCH 175/203] fb --- .../Models/ScheduleConfiguration.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 7b83eb3d..4a6fb88a 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -127,43 +127,50 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) Check.NotNull(updateOptions, nameof(updateOptions)); HashSet updatedFields = new HashSet(); - if (!string.IsNullOrEmpty(updateOptions.OrchestrationName)) + if (!string.IsNullOrEmpty(updateOptions.OrchestrationName) + && updateOptions.OrchestrationName != this.OrchestrationName) { this.OrchestrationName = updateOptions.OrchestrationName; updatedFields.Add(nameof(this.OrchestrationName)); } - if (!string.IsNullOrEmpty(updateOptions.OrchestrationInput)) + if (!string.IsNullOrEmpty(updateOptions.OrchestrationInput) + && updateOptions.OrchestrationInput != this.OrchestrationInput) { this.OrchestrationInput = updateOptions.OrchestrationInput; updatedFields.Add(nameof(this.OrchestrationInput)); } - if (!string.IsNullOrEmpty(updateOptions.OrchestrationInstanceId)) + if (!string.IsNullOrEmpty(updateOptions.OrchestrationInstanceId) + && updateOptions.OrchestrationInstanceId != this.OrchestrationInstanceId) { this.OrchestrationInstanceId = updateOptions.OrchestrationInstanceId; updatedFields.Add(nameof(this.OrchestrationInstanceId)); } - if (updateOptions.StartAt.HasValue) + if (updateOptions.StartAt.HasValue + && updateOptions.StartAt != this.StartAt) { this.StartAt = updateOptions.StartAt; updatedFields.Add(nameof(this.StartAt)); } - if (updateOptions.EndAt.HasValue) + if (updateOptions.EndAt.HasValue + && updateOptions.EndAt != this.EndAt) { this.EndAt = updateOptions.EndAt; updatedFields.Add(nameof(this.EndAt)); } - if (updateOptions.Interval.HasValue) + if (updateOptions.Interval.HasValue + && updateOptions.Interval != this.Interval) { this.Interval = updateOptions.Interval.Value; updatedFields.Add(nameof(this.Interval)); } - if (updateOptions.StartImmediatelyIfLate.HasValue) + if (updateOptions.StartImmediatelyIfLate.HasValue + && updateOptions.StartImmediatelyIfLate != this.StartImmediatelyIfLate) { this.StartImmediatelyIfLate = updateOptions.StartImmediatelyIfLate.Value; updatedFields.Add(nameof(this.StartImmediatelyIfLate)); From 4987b00b9739c7c21c29d66aa0fc0292d9283dc2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:00:46 -0800 Subject: [PATCH 176/203] fb --- samples/ScheduleWebApp/ScheduleController.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/samples/ScheduleWebApp/ScheduleController.cs b/samples/ScheduleWebApp/ScheduleController.cs index 87854f53..f320723b 100644 --- a/samples/ScheduleWebApp/ScheduleController.cs +++ b/samples/ScheduleWebApp/ScheduleController.cs @@ -113,22 +113,11 @@ public async Task>> ListSchedules( // add schedule result list List scheduleList = new List(); // Initialize the continuation token - string? continuationToken = null; - await foreach (Page page in schedules.AsPages(continuationToken)) + await foreach (ScheduleDescription schedule in schedules) { - scheduleList.AddRange(page.Values.ToArray()); - - // Update the continuation token for the next iteration - continuationToken = page.ContinuationToken; - - // If there's no continuation token, we've reached the end of the collection - if (continuationToken == null) - { - break; - } + scheduleList.Add(schedule); } - return this.Ok(scheduleList); } catch (Exception ex) From a610123f91b0ee82a8bc0657feb182810196ee60 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:08:39 -0800 Subject: [PATCH 177/203] f --- .../Client/ScheduledTaskClientImpl.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index a453d59c..6b1489f2 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -176,10 +176,19 @@ public override AsyncPageable ListSchedulesAsync(ScheduleQu // Return empty page if no results return new Page(new List(), null); } - catch (OperationCanceledException e) + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { - throw new OperationCanceledException( - $"The {nameof(this.ListSchedulesAsync)} operation was canceled.", e, e.CancellationToken); + // the operation was cancelled as requested. No need to log this. + throw; + } + catch (Exception ex) + { + this.logger.ClientError( + nameof(this.ListSchedulesAsync), + string.Empty, + ex); + + throw; } }); } From 49b615d803d2407781daf372f705c3c4283cb600 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:37:39 -0800 Subject: [PATCH 178/203] fix --- .../Client/ScheduledTaskClientImpl.cs | 23 +- .../Entity/ScheduleTests.cs | 239 +++++++++++------- .../ScheduledTasks.Tests.csproj | 1 + 3 files changed, 147 insertions(+), 116 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index 6b1489f2..6cd4c3b0 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -26,26 +26,13 @@ public override async Task CreateScheduleAsync(ScheduleCreationO try { - string scheduleId = creationOptions.ScheduleId; - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - - // Call the orchestrator to create the schedule - ScheduleOperationRequest request = new ScheduleOperationRequest(entityId, nameof(Schedule.CreateSchedule), creationOptions); - string instanceId = await this.durableTaskClient.ScheduleNewOrchestrationInstanceAsync( - new TaskName(nameof(ExecuteScheduleOperationOrchestrator)), - request, - cancellation); - - // Wait for the orchestration to complete - OrchestrationMetadata state = await this.durableTaskClient.WaitForInstanceCompletionAsync(instanceId, true, cancellation); + // Create schedule client instance + ScheduleClient scheduleClient = new ScheduleClientImpl(this.durableTaskClient, creationOptions.ScheduleId, this.logger); - if (state.RuntimeStatus != OrchestrationRuntimeStatus.Completed) - { - throw new InvalidOperationException($"Failed to create schedule '{scheduleId}': {state.FailureDetails?.ErrorMessage ?? string.Empty}"); - } + // Create the schedule using the client + await scheduleClient.CreateAsync(creationOptions, cancellation); - // Return a handle to the schedule - return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); + return scheduleClient; } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 762f3535..1e8aeada 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask.Entities; +using Microsoft.DurableTask.Entities.Tests; using Microsoft.Extensions.Logging; -using Moq; using Xunit; using Xunit.Abstractions; @@ -11,22 +10,20 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Entity; public class ScheduleTests { - readonly Mock mockContext; - readonly Schedule schedule; - readonly string scheduleId = "test-schedule"; - readonly TestLogger logger; + private readonly Schedule schedule; + private readonly string scheduleId = "test-schedule"; + private readonly TestLogger logger; public ScheduleTests(ITestOutputHelper output) { - this.mockContext = new Mock(MockBehavior.Strict); this.logger = new TestLogger(); - this.schedule = new Schedule(this.logger); + this.schedule = new Schedule((ILogger)this.logger); } // Simple TestLogger implementation for capturing logs - class TestLogger : ILogger + private class TestLogger : ILogger { - public List<(LogLevel Level, string Message)> Logs { get; } = new(); + public List<(LogLevel Level, string Message)> Logs { get; } = new List<(LogLevel, string)>(); public IDisposable? BeginScope(TState state) where TState : notnull => null; @@ -40,184 +37,230 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } [Fact] - public void CreateSchedule_WithValidOptions_CreatesSchedule() + public async Task CreateSchedule_WithValidOptions_CreatesSchedule() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions options = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); + // Create test operation + TestEntityOperation operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + // Act - this.schedule.CreateSchedule(this.mockContext.Object, options); + await this.schedule.RunAsync(operation); // Assert - this.mockContext.Verify(c => c.SignalEntity( - It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), - nameof(Schedule.RunSchedule), - It.IsAny(), - null), Times.Once); - } - - [Fact] - public void CreateSchedule_WithNullOptions_ThrowsArgumentNullException() - { - // Act & Assert - var ex = Assert.Throws(() => - this.schedule.CreateSchedule(this.mockContext.Object, null!)); - Assert.Contains("Schedule creation options cannot be null", ex.Message); + object? state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + ScheduleState scheduleState = Assert.IsType(state); + Assert.NotNull(scheduleState.ScheduleConfiguration); + Assert.Equal(this.scheduleId, scheduleState.ScheduleConfiguration.ScheduleId); + Assert.Equal("TestOrchestration", scheduleState.ScheduleConfiguration.OrchestrationName); + Assert.Equal(TimeSpan.FromMinutes(5), scheduleState.ScheduleConfiguration.Interval); + Assert.Equal(ScheduleStatus.Active, scheduleState.Status); } [Fact] - public void PauseSchedule_WhenActive_PausesSchedule() + public async Task PauseSchedule_WhenAlreadyPaused_ThrowsInvalidTransitionException() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); - // Act - this.schedule.PauseSchedule(this.mockContext.Object); + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); - // Assert - this.mockContext.Verify(c => c.SignalEntity( - It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), - nameof(Schedule.RunSchedule), - It.IsAny(), - null), Times.Once); - } - - [Fact] - public void PauseSchedule_WhenAlreadyPaused_ThrowsInvalidTransitionException() - { - // Arrange - var options = new ScheduleCreationOptions( - scheduleId: this.scheduleId, - orchestrationName: "TestOrchestration", - interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); - this.schedule.PauseSchedule(this.mockContext.Object); + // Pause first time + TestEntityOperation pauseOperation = new TestEntityOperation( + nameof(Schedule.PauseSchedule), + createOperation.State, + null); + await this.schedule.RunAsync(pauseOperation); // Act & Assert - var ex = Assert.Throws(() => - this.schedule.PauseSchedule(this.mockContext.Object)); + await Assert.ThrowsAsync(() => + this.schedule.RunAsync(new TestEntityOperation( + nameof(Schedule.PauseSchedule), + pauseOperation.State, + null)).AsTask()); } [Fact] - public void ResumeSchedule_WhenPaused_ResumesSchedule() + public async Task ResumeSchedule_WhenPaused_ResumesSchedule() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); - this.schedule.PauseSchedule(this.mockContext.Object); + + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Pause + TestEntityOperation pauseOperation = new TestEntityOperation( + nameof(Schedule.PauseSchedule), + createOperation.State, + null); + await this.schedule.RunAsync(pauseOperation); // Act - this.schedule.ResumeSchedule(this.mockContext.Object); + TestEntityOperation resumeOperation = new TestEntityOperation( + nameof(Schedule.ResumeSchedule), + pauseOperation.State, + null); + await this.schedule.RunAsync(resumeOperation); // Assert - this.mockContext.Verify(c => c.SignalEntity( - It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), - nameof(Schedule.RunSchedule), - It.IsAny(), - null), Times.Exactly(2)); } [Fact] - public void ResumeSchedule_WhenActive_ThrowsInvalidTransitionException() + public async Task ResumeSchedule_WhenActive_ThrowsInvalidTransitionException() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); // Act & Assert - var ex = Assert.Throws(() => - this.schedule.ResumeSchedule(this.mockContext.Object)); + await Assert.ThrowsAsync(() => + this.schedule.RunAsync(new TestEntityOperation( + nameof(Schedule.ResumeSchedule), + createOperation.State, + null)).AsTask()); } [Fact] - public void UpdateSchedule_WithValidOptions_UpdatesSchedule() + public async Task UpdateSchedule_WithValidOptions_UpdatesSchedule() { // Arrange - var createOptions = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, createOptions); - var updateOptions = new ScheduleUpdateOptions + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + ScheduleUpdateOptions updateOptions = new ScheduleUpdateOptions { Interval = TimeSpan.FromMinutes(10) }; // Act - this.schedule.UpdateSchedule(this.mockContext.Object, updateOptions); + TestEntityOperation updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); // Assert - this.mockContext.Verify(c => c.SignalEntity( - It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), - nameof(Schedule.RunSchedule), - It.IsAny(), - null), Times.Once); } [Fact] - public void UpdateSchedule_WithNullOptions_ThrowsArgumentNullException() + public async Task UpdateSchedule_WithNullOptions_ThrowsArgumentNullException() { // Arrange - var createOptions = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, createOptions); + + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); // Act & Assert - var ex = Assert.Throws(() => - this.schedule.UpdateSchedule(this.mockContext.Object, null!)); - Assert.Contains("Schedule update options cannot be null", ex.Message); + await Assert.ThrowsAsync(() => + this.schedule.RunAsync(new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + null)).AsTask()); } [Fact] - public void RunSchedule_WhenNotActive_ThrowsInvalidOperationException() + public async Task RunSchedule_WhenNotActive_ThrowsInvalidOperationException() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); - this.schedule.PauseSchedule(this.mockContext.Object); + + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Pause + TestEntityOperation pauseOperation = new TestEntityOperation( + nameof(Schedule.PauseSchedule), + createOperation.State, + null); + await this.schedule.RunAsync(pauseOperation); // Act & Assert - var ex = Assert.Throws(() => - this.schedule.RunSchedule(this.mockContext.Object, "token")); - Assert.Contains("Schedule must be in Active status to run", ex.Message); + await Assert.ThrowsAsync(() => + this.schedule.RunAsync(new TestEntityOperation( + nameof(Schedule.RunSchedule), + pauseOperation.State, + "token")).AsTask()); } [Fact] - public void RunSchedule_WithInvalidToken_DoesNotRun() + public async Task RunSchedule_WithInvalidToken_DoesNotRun() { // Arrange - var options = new ScheduleCreationOptions( + ScheduleCreationOptions createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", interval: TimeSpan.FromMinutes(5)); - this.schedule.CreateSchedule(this.mockContext.Object, options); + + // Create initial state + TestEntityOperation createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); // Act - this.schedule.RunSchedule(this.mockContext.Object, "invalid-token"); + await this.schedule.RunAsync(new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + "invalid-token")); // Assert - this.mockContext.Verify(c => c.ScheduleNewOrchestration( - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); } } \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj index 1f57ddb1..f0c9de72 100644 --- a/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj +++ b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj @@ -25,6 +25,7 @@ + \ No newline at end of file From b5e850081cf9497681dcc17b56b11adb4cefdb03 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:46:26 -0800 Subject: [PATCH 179/203] up --- .../Client/ScheduledTaskClientImpl.cs | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index 6cd4c3b0..f4fe7c5f 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -3,7 +3,6 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; -using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -57,32 +56,16 @@ public override async Task CreateScheduleAsync(ScheduleCreationO try { - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), scheduleId); - EntityMetadata? metadata = await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation); + // Get schedule client first + ScheduleClient scheduleClient = this.GetScheduleClient(scheduleId); - if (metadata == null || metadata.State.Status == ScheduleStatus.Uninitialized) - { - return null; - } - - ScheduleState state = metadata.State; - ScheduleConfiguration? config = state.ScheduleConfiguration; - - return new ScheduleDescription - { - ScheduleId = scheduleId, - OrchestrationName = config?.OrchestrationName, - OrchestrationInput = config?.OrchestrationInput, - OrchestrationInstanceId = config?.OrchestrationInstanceId, - StartAt = config?.StartAt, - EndAt = config?.EndAt, - Interval = config?.Interval, - StartImmediatelyIfLate = config?.StartImmediatelyIfLate, - Status = state.Status, - ExecutionToken = state.ExecutionToken, - LastRunAt = state.LastRunAt, - NextRunAt = state.NextRunAt, - }; + // Call DescribeAsync which handles all the entity state mapping + return await scheduleClient.DescribeAsync(cancellation); + } + catch (ScheduleNotFoundException) + { + // Return null if schedule not found + return null; } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { From 30c9a0c190da38ca638bd583d4f80f0e8543c20e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:14:01 -0800 Subject: [PATCH 180/203] fixed scheduletests --- .../Entity/ScheduleTests.cs | 122 +++++++++++++++--- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 1e8aeada..a6b4b147 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -10,9 +10,9 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Entity; public class ScheduleTests { - private readonly Schedule schedule; - private readonly string scheduleId = "test-schedule"; - private readonly TestLogger logger; + readonly Schedule schedule; + readonly string scheduleId = "test-schedule"; + readonly TestLogger logger; public ScheduleTests(ITestOutputHelper output) { @@ -21,9 +21,9 @@ public ScheduleTests(ITestOutputHelper output) } // Simple TestLogger implementation for capturing logs - private class TestLogger : ILogger + class TestLogger : ILogger { - public List<(LogLevel Level, string Message)> Logs { get; } = new List<(LogLevel, string)>(); + public List<(LogLevel Level, string Message)> Logs { get; } = new(); public IDisposable? BeginScope(TState state) where TState : notnull => null; @@ -55,7 +55,7 @@ public async Task CreateSchedule_WithValidOptions_CreatesSchedule() await this.schedule.RunAsync(operation); // Assert - object? state = operation.State.GetState(typeof(ScheduleState)); + var state = operation.State.GetState(typeof(ScheduleState)); Assert.NotNull(state); ScheduleState scheduleState = Assert.IsType(state); Assert.NotNull(scheduleState.ScheduleConfiguration); @@ -81,6 +81,12 @@ public async Task PauseSchedule_WhenAlreadyPaused_ThrowsInvalidTransitionExcepti createOptions); await this.schedule.RunAsync(createOperation); + // assert after create + var state = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + ScheduleState scheduleState = Assert.IsType(state); + Assert.Equal(ScheduleStatus.Active, scheduleState.Status); + // Pause first time TestEntityOperation pauseOperation = new TestEntityOperation( nameof(Schedule.PauseSchedule), @@ -88,11 +94,23 @@ public async Task PauseSchedule_WhenAlreadyPaused_ThrowsInvalidTransitionExcepti null); await this.schedule.RunAsync(pauseOperation); + // assert after first pause + var stateAfterFirstPause = pauseOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterFirstPause); + ScheduleState scheduleStateAfterFirstPause = Assert.IsType(stateAfterFirstPause); + Assert.Equal(ScheduleStatus.Paused, scheduleStateAfterFirstPause.Status); + + // Pause second time + TestEntityOperation pauseOperation2 = new TestEntityOperation( + nameof(Schedule.PauseSchedule), + pauseOperation.State, + null); + // Act & Assert await Assert.ThrowsAsync(() => this.schedule.RunAsync(new TestEntityOperation( nameof(Schedule.PauseSchedule), - pauseOperation.State, + pauseOperation2.State, null)).AsTask()); } @@ -112,6 +130,12 @@ public async Task ResumeSchedule_WhenPaused_ResumesSchedule() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state is active + var stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterCreate.Status); + // Pause TestEntityOperation pauseOperation = new TestEntityOperation( nameof(Schedule.PauseSchedule), @@ -119,6 +143,12 @@ public async Task ResumeSchedule_WhenPaused_ResumesSchedule() null); await this.schedule.RunAsync(pauseOperation); + // Assert paused state + var stateAfterPause = pauseOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterPause); + ScheduleState scheduleStateAfterPause = Assert.IsType(stateAfterPause); + Assert.Equal(ScheduleStatus.Paused, scheduleStateAfterPause.Status); + // Act TestEntityOperation resumeOperation = new TestEntityOperation( nameof(Schedule.ResumeSchedule), @@ -126,7 +156,11 @@ public async Task ResumeSchedule_WhenPaused_ResumesSchedule() null); await this.schedule.RunAsync(resumeOperation); - // Assert + // Assert resumed state + var stateAfterResume = resumeOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterResume); + ScheduleState scheduleStateAfterResume = Assert.IsType(stateAfterResume); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterResume.Status); } [Fact] @@ -145,6 +179,12 @@ public async Task ResumeSchedule_WhenActive_ThrowsInvalidTransitionException() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state is active + object? stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterCreate.Status); + // Act & Assert await Assert.ThrowsAsync(() => this.schedule.RunAsync(new TestEntityOperation( @@ -169,6 +209,12 @@ public async Task UpdateSchedule_WithValidOptions_UpdatesSchedule() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state + object? stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(TimeSpan.FromMinutes(5), scheduleStateAfterCreate.ScheduleConfiguration?.Interval); + ScheduleUpdateOptions updateOptions = new ScheduleUpdateOptions { Interval = TimeSpan.FromMinutes(10) @@ -181,7 +227,11 @@ public async Task UpdateSchedule_WithValidOptions_UpdatesSchedule() updateOptions); await this.schedule.RunAsync(updateOperation); - // Assert + // Assert updated state + object? stateAfterUpdate = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterUpdate); + ScheduleState scheduleStateAfterUpdate = Assert.IsType(stateAfterUpdate); + Assert.Equal(TimeSpan.FromMinutes(10), scheduleStateAfterUpdate.ScheduleConfiguration?.Interval); } [Fact] @@ -200,11 +250,17 @@ public async Task UpdateSchedule_WithNullOptions_ThrowsArgumentNullException() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state + object? stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(TimeSpan.FromMinutes(5), scheduleStateAfterCreate.ScheduleConfiguration?.Interval); + // Act & Assert - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => this.schedule.RunAsync(new TestEntityOperation( nameof(Schedule.UpdateSchedule), - createOperation.State, + stateAfterCreate, null)).AsTask()); } @@ -224,6 +280,12 @@ public async Task RunSchedule_WhenNotActive_ThrowsInvalidOperationException() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state is active + object? stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterCreate.Status); + // Pause TestEntityOperation pauseOperation = new TestEntityOperation( nameof(Schedule.PauseSchedule), @@ -231,12 +293,20 @@ public async Task RunSchedule_WhenNotActive_ThrowsInvalidOperationException() null); await this.schedule.RunAsync(pauseOperation); + // Assert paused state + object? stateAfterPause = pauseOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterPause); + ScheduleState scheduleStateAfterPause = Assert.IsType(stateAfterPause); + Assert.Equal(ScheduleStatus.Paused, scheduleStateAfterPause.Status); + + // run schedule op + TestEntityOperation runOp = new TestEntityOperation( + nameof(Schedule.RunSchedule), + scheduleStateAfterPause, + scheduleStateAfterPause.ExecutionToken); // Act & Assert await Assert.ThrowsAsync(() => - this.schedule.RunAsync(new TestEntityOperation( - nameof(Schedule.RunSchedule), - pauseOperation.State, - "token")).AsTask()); + this.schedule.RunAsync(runOp).AsTask()); } [Fact] @@ -255,12 +325,26 @@ public async Task RunSchedule_WithInvalidToken_DoesNotRun() createOptions); await this.schedule.RunAsync(createOperation); + // Assert initial state + object? stateAfterCreate = createOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterCreate); + ScheduleState scheduleStateAfterCreate = Assert.IsType(stateAfterCreate); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterCreate.Status); + string? initialToken = scheduleStateAfterCreate.ExecutionToken; + // Act - await this.schedule.RunAsync(new TestEntityOperation( + TestEntityOperation runOperation = new TestEntityOperation( nameof(Schedule.RunSchedule), createOperation.State, - "invalid-token")); - - // Assert + "invalid-token"); + await this.schedule.RunAsync(runOperation); + + // Assert state unchanged + object? stateAfterRun = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(stateAfterRun); + ScheduleState scheduleStateAfterRun = Assert.IsType(stateAfterRun); + Assert.Equal(ScheduleStatus.Active, scheduleStateAfterRun.Status); + Assert.Equal(initialToken, scheduleStateAfterRun.ExecutionToken); + Assert.Null(scheduleStateAfterRun.LastRunAt); } } \ No newline at end of file From dfdf28742216da635f5db7a8c8924784bf3b7e5b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:03:55 -0800 Subject: [PATCH 181/203] fix tests --- src/ScheduledTasks/Entity/Schedule.cs | 24 +- .../Client/ScheduleClientImplTests.cs | 12 +- .../Entity/ScheduleTests.cs | 518 +++++++++++++++++- test/TestHelpers/Logging/TestLogger.cs | 34 ++ 4 files changed, 554 insertions(+), 34 deletions(-) create mode 100644 test/TestHelpers/Logging/TestLogger.cs diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 8d0b9a35..0786c981 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -282,17 +282,25 @@ DateTimeOffset DetermineNextRunTime(ScheduleConfiguration scheduleConfig) // compute time gap between now and startat if set else with ScheduleCreatedAt TimeSpan timeSinceStart = now - startTime; - if (timeSinceStart <= TimeSpan.Zero && scheduleConfig.StartImmediatelyIfLate) + // timeSinceStart is negative that means next run time should be in future + if (timeSinceStart < TimeSpan.Zero) { - return now; + return startTime; } - else - { - // Calculate number of intervals between start time and now - int intervalsElapsed = (int)(timeSinceStart.Ticks / scheduleConfig.Interval.Ticks); - // Compute next run time based on intervals elapsed since start - return startTime + TimeSpan.FromTicks(scheduleConfig.Interval.Ticks * (intervalsElapsed + 1)); + // timeSinceStart is >= 0, this mean current time already past start time + bool isFirstRun = this.State.LastRunAt == null; + + // check edge case: if this is first run and startimmediatelyiflate is true, run immediately + if (isFirstRun && scheduleConfig.StartImmediatelyIfLate) + { + return now; } + + // Calculate number of intervals between start time and now + int intervalsElapsed = (int)(timeSinceStart.Ticks / scheduleConfig.Interval.Ticks); + + // Compute next run time based on intervals elapsed since start + return startTime + TimeSpan.FromTicks(scheduleConfig.Interval.Ticks * (intervalsElapsed + 1)); } } diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs index 5125a28b..8d652e8b 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs @@ -14,7 +14,7 @@ public class ScheduleClientImplTests { readonly Mock durableTaskClient; readonly Mock entityClient; - readonly Mock logger; + readonly ILogger logger; readonly ScheduleClientImpl client; readonly string scheduleId = "test-schedule"; @@ -22,17 +22,17 @@ public ScheduleClientImplTests() { this.durableTaskClient = new Mock("test", MockBehavior.Strict); this.entityClient = new Mock("test", MockBehavior.Strict); - this.logger = new Mock(MockBehavior.Loose); + this.logger = new TestLogger(); this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); - this.client = new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, this.logger.Object); + this.client = new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, this.logger); } [Fact] public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => - new ScheduleClientImpl(null!, this.scheduleId, this.logger.Object)); + ArgumentNullException ex = Assert.Throws(() => + new ScheduleClientImpl(null!, this.scheduleId, this.logger)); Assert.Equal("client", ex.ParamName); } @@ -43,7 +43,7 @@ public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string inv { // Act & Assert var ex = Assert.Throws(() => - new ScheduleClientImpl(this.durableTaskClient.Object, invalidScheduleId, this.logger.Object)); + new ScheduleClientImpl(this.durableTaskClient.Object, invalidScheduleId, this.logger)); Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); } diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index a6b4b147..9c051e9f 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.DurableTask.Entities.Tests; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; @@ -12,28 +11,12 @@ public class ScheduleTests { readonly Schedule schedule; readonly string scheduleId = "test-schedule"; - readonly TestLogger logger; + readonly TestLogger logger; public ScheduleTests(ITestOutputHelper output) { - this.logger = new TestLogger(); - this.schedule = new Schedule((ILogger)this.logger); - } - - // Simple TestLogger implementation for capturing logs - class TestLogger : ILogger - { - public List<(LogLevel Level, string Message)> Logs { get; } = new(); - - public IDisposable? BeginScope(TState state) where TState : notnull => null; - - public bool IsEnabled(LogLevel logLevel) => true; - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - string message = formatter(state, exception); - this.Logs.Add((logLevel, message)); - } + this.logger = new TestLogger(); + this.schedule = new Schedule(this.logger); } [Fact] @@ -347,4 +330,499 @@ public async Task RunSchedule_WithInvalidToken_DoesNotRun() Assert.Equal(initialToken, scheduleStateAfterRun.ExecutionToken); Assert.Null(scheduleStateAfterRun.LastRunAt); } + + [Fact] + public async Task CreateSchedule_WithStartAt_SetsNextRunTimeCorrectly() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(5); + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(startAt, scheduleState.ScheduleConfiguration?.StartAt); + Assert.Equal(ScheduleStatus.Active, scheduleState.Status); + } + + [Fact] + public async Task CreateSchedule_WithEndAt_SetsEndTimeCorrectly() + { + // Arrange + var endAt = DateTimeOffset.UtcNow.AddHours(1); + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = endAt + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(endAt, scheduleState.ScheduleConfiguration?.EndAt); + } + + [Fact] + public async Task CreateSchedule_WithStartImmediatelyIfLate_SetsPropertyCorrectly() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartImmediatelyIfLate = true + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.True(scheduleState.ScheduleConfiguration?.StartImmediatelyIfLate); + } + + [Fact] + public async Task UpdateSchedule_WithStartAt_UpdatesStartTimeCorrectly() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var newStartAt = DateTimeOffset.UtcNow.AddMinutes(10); + var updateOptions = new ScheduleUpdateOptions + { + StartAt = newStartAt + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(newStartAt, scheduleState.ScheduleConfiguration?.StartAt); + Assert.Null(scheduleState.NextRunAt); // NextRunAt should be reset + } + + [Fact] + public async Task UpdateSchedule_WithEndAt_UpdatesEndTimeCorrectly() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var newEndAt = DateTimeOffset.UtcNow.AddHours(2); + var updateOptions = new ScheduleUpdateOptions + { + EndAt = newEndAt + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(newEndAt, scheduleState.ScheduleConfiguration?.EndAt); + } + + [Fact] + public async Task UpdateSchedule_WithNoChanges_DoesNotUpdateState() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(typeof(ScheduleState)); + var initialScheduleState = Assert.IsType(initialState); + var initialModifiedTime = initialScheduleState.ScheduleLastModifiedAt; + + var updateOptions = new ScheduleUpdateOptions + { + Interval = TimeSpan.FromMinutes(5) // Same interval + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(initialModifiedTime, scheduleState.ScheduleLastModifiedAt); + } + + [Fact] + public async Task RunSchedule_WhenEndAtPassed_DoesNotRun() + { + // Arrange + var endAt = DateTimeOffset.UtcNow.AddMinutes(-5); // End time in the past + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = endAt + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState().ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + Assert.Null(scheduleState.NextRunAt); + } + + [Fact] + public async Task RunSchedule_WithValidToken_UpdatesLastRunAt() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(); + var validToken = initialState?.ExecutionToken; + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + validToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.NotNull(scheduleState.LastRunAt); + } + + [Fact] + public async Task CreateSchedule_WithOrchestrationInput_SetsInputCorrectly() + { + // Arrange + var orchestrationInput = "test-input"; + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInput = orchestrationInput + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(orchestrationInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task CreateSchedule_WithOrchestrationInstanceId_SetsIdCorrectly() + { + // Arrange + var instanceId = "custom-instance-id"; + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInstanceId = instanceId + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(instanceId, scheduleState.ScheduleConfiguration?.OrchestrationInstanceId); + } + + [Fact] + public async Task UpdateSchedule_WithOrchestrationInput_UpdatesInputCorrectly() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var newInput = "new-test-input"; + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = newInput + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(newInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task UpdateSchedule_WithStartImmediatelyIfLate_UpdatesPropertyCorrectly() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var updateOptions = new ScheduleUpdateOptions + { + StartImmediatelyIfLate = true + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.True(scheduleState.ScheduleConfiguration?.StartImmediatelyIfLate); + } + + [Fact] + public async Task RunSchedule_WhenStartAtInFuture_DoesNotRunImmediately() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(5); + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState().ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + Assert.NotNull(scheduleState.NextRunAt); + Assert.True(scheduleState.NextRunAt >= startAt); + } + + [Fact] + public async Task RunSchedule_WithStartImmediatelyIfLate_RunsImmediatelyWhenLate() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(-5); // Start time in the past + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt, + StartImmediatelyIfLate = true + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState()?.ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.NotNull(scheduleState.LastRunAt); + Assert.True(scheduleState.LastRunAt >= DateTimeOffset.UtcNow.AddSeconds(-1)); + } + + [Fact] + public async Task RunSchedule_WithStartImmediatelyIfLate_False_DoesNotRunImmediatelyWhenLate() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(-5); // Start time in the past + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt, + StartImmediatelyIfLate = false + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState().ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + Assert.NotNull(scheduleState.NextRunAt); + Assert.True(scheduleState.NextRunAt > DateTimeOffset.UtcNow); + } } \ No newline at end of file diff --git a/test/TestHelpers/Logging/TestLogger.cs b/test/TestHelpers/Logging/TestLogger.cs new file mode 100644 index 00000000..adbd3141 --- /dev/null +++ b/test/TestHelpers/Logging/TestLogger.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; + +public class TestLogger : ILogger +{ + public List<(LogLevel Level, string Message)> Logs { get; } = new(); + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + this.Logs.Add((logLevel, message)); + } +} + +public class TestLogger : ILogger +{ + public List<(LogLevel Level, string Message)> Logs { get; } = new(); + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + string message = formatter(state, exception); + this.Logs.Add((logLevel, message)); + } +} \ No newline at end of file From ae9750433fd60f35075891699bbf365f5b224623 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:10:49 -0800 Subject: [PATCH 182/203] fix --- test/ScheduledTasks.Tests/Entity/ScheduleTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 9c051e9f..e3d8b400 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -550,7 +550,7 @@ public async Task RunSchedule_WhenEndAtPassed_DoesNotRun() var runOperation = new TestEntityOperation( nameof(Schedule.RunSchedule), createOperation.State, - createOperation.State.GetState().ExecutionToken); + createOperation.State.GetState()?.ExecutionToken); await this.schedule.RunAsync(runOperation); // Assert @@ -568,7 +568,10 @@ public async Task RunSchedule_WithValidToken_UpdatesLastRunAt() var createOptions = new ScheduleCreationOptions( scheduleId: this.scheduleId, orchestrationName: "TestOrchestration", - interval: TimeSpan.FromMinutes(5)); + interval: TimeSpan.FromMinutes(5)) + { + StartImmediatelyIfLate = true + }; var createOperation = new TestEntityOperation( nameof(Schedule.CreateSchedule), From c780eaa0f0770bf8d2295e5e471896043ef6da2b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:15:19 -0800 Subject: [PATCH 183/203] more tests --- src/ScheduledTasks/Entity/Schedule.cs | 17 +- .../Models/ScheduleConfiguration.cs | 22 +- src/ScheduledTasks/Models/ScheduleState.cs | 22 +- .../Entity/ScheduleTests.cs | 484 +++++++++++++++++- 4 files changed, 537 insertions(+), 8 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 0786c981..94a235ba 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -40,11 +40,22 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc throw new ScheduleClientValidationException(string.Empty, "Schedule creation options cannot be null"); } + bool alreadyExists = this.State.ScheduleCreatedAt != null; + this.State.ScheduleConfiguration = ScheduleConfiguration.FromCreateOptions(scheduleCreationOptions); - this.State.Status = ScheduleStatus.Active; - this.State.RefreshScheduleRunExecutionToken(); - this.State.ScheduleCreatedAt = this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; + if (alreadyExists) + { + this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; + this.State.RefreshScheduleRunExecutionToken(); + this.State.NextRunAt = null; + } + else + { + this.State.Status = ScheduleStatus.Active; + this.State.ScheduleCreatedAt = this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; + } + this.logger.CreatedSchedule(this.State.ScheduleConfiguration.ScheduleId); // Signal to run schedule immediately after creation and let runSchedule determine if it should run immediately diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index 4a6fb88a..ed310245 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -107,7 +109,7 @@ public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions cr { Check.NotNull(createOptions, nameof(createOptions)); - return new ScheduleConfiguration(createOptions.ScheduleId, createOptions.OrchestrationName, createOptions.Interval) + ScheduleConfiguration scheduleConfig = new ScheduleConfiguration(createOptions.ScheduleId, createOptions.OrchestrationName, createOptions.Interval) { OrchestrationInput = createOptions.OrchestrationInput, OrchestrationInstanceId = createOptions.OrchestrationInstanceId, @@ -115,6 +117,10 @@ public static ScheduleConfiguration FromCreateOptions(ScheduleCreationOptions cr EndAt = createOptions.EndAt, StartImmediatelyIfLate = createOptions.StartImmediatelyIfLate, }; + + scheduleConfig.Validate(); + + return scheduleConfig; } /// @@ -127,7 +133,7 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) Check.NotNull(updateOptions, nameof(updateOptions)); HashSet updatedFields = new HashSet(); - if (!string.IsNullOrEmpty(updateOptions.OrchestrationName) + if (!string.IsNullOrEmpty(updateOptions.OrchestrationName) && updateOptions.OrchestrationName != this.OrchestrationName) { this.OrchestrationName = updateOptions.OrchestrationName; @@ -148,7 +154,7 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) updatedFields.Add(nameof(this.OrchestrationInstanceId)); } - if (updateOptions.StartAt.HasValue + if (updateOptions.StartAt.HasValue && updateOptions.StartAt != this.StartAt) { this.StartAt = updateOptions.StartAt; @@ -176,6 +182,16 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) updatedFields.Add(nameof(this.StartImmediatelyIfLate)); } + this.Validate(); return updatedFields; } + + [MemberNotNull(nameof(StartAt), nameof(EndAt))] + void Validate() + { + if (this.StartAt.HasValue && this.EndAt.HasValue && this.StartAt.Value > this.EndAt.Value) + { + throw new ArgumentException("StartAt cannot be later than EndAt.", nameof(this.StartAt)); + } + } } diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 7007c762..af1d1970 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -50,4 +50,24 @@ public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } -} + + /// + /// Gets or sets the time when this schedule was last paused. + /// + public DateTimeOffset? LastPausedAt { get; set; } + + /// + /// Clears all state fields to their default values. + /// + public void ClearState() + { + this.Status = ScheduleStatus.Uninitialized; + this.ExecutionToken = Guid.NewGuid().ToString("N"); + this.LastRunAt = null; + this.NextRunAt = null; + this.ScheduleCreatedAt = null; + this.ScheduleLastModifiedAt = null; + this.ScheduleConfiguration = null; + this.LastPausedAt = null; + } +} \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index e3d8b400..21e38621 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -746,7 +746,7 @@ public async Task RunSchedule_WhenStartAtInFuture_DoesNotRunImmediately() var runOperation = new TestEntityOperation( nameof(Schedule.RunSchedule), createOperation.State, - createOperation.State.GetState().ExecutionToken); + createOperation.State.GetState()?.ExecutionToken); await this.schedule.RunAsync(runOperation); // Assert @@ -828,4 +828,486 @@ public async Task RunSchedule_WithStartImmediatelyIfLate_False_DoesNotRunImmedia Assert.NotNull(scheduleState.NextRunAt); Assert.True(scheduleState.NextRunAt > DateTimeOffset.UtcNow); } + + [Fact] + public async Task CreateSchedule_WithEndAtBeforeStartAt_ThrowsArgumentException() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddHours(2); + var endAt = DateTimeOffset.UtcNow.AddHours(1); // Before startAt + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt, + EndAt = endAt + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act & Assert + await Assert.ThrowsAsync(() => + this.schedule.RunAsync(operation).AsTask()); + } + + [Fact] + public async Task RunSchedule_WithExpiredEndAt_DoesNotUpdateLastRunAt() + { + // Arrange + var endAt = DateTimeOffset.UtcNow.AddSeconds(-1); + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = endAt + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + initialState?.ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + } + + [Fact] + public async Task CreateSchedule_WithMaxInterval_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.MaxValue); + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(TimeSpan.MaxValue, scheduleState.ScheduleConfiguration?.Interval); + } + + [Fact] + public async Task UpdateSchedule_WithSameValues_DoesNotUpdateModifiedTime() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(); + var initialModifiedTime = initialState?.ScheduleLastModifiedAt; + + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationName = "TestOrchestration", // Same name + Interval = TimeSpan.FromMinutes(5) // Same interval + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(initialModifiedTime, scheduleState.ScheduleLastModifiedAt); + } + + [Fact] + public async Task RunSchedule_WithStartAtInFutureAndStartImmediatelyIfLate_DoesNotRunImmediately() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(5); + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = startAt, + StartImmediatelyIfLate = true // Should be ignored since StartAt is in future + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState()?.ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + Assert.Equal(startAt, scheduleState.NextRunAt); + } + + [Fact] + public async Task CreateSchedule_WithMaxDateTimeOffset_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = DateTimeOffset.MaxValue + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(DateTimeOffset.MaxValue, scheduleState.ScheduleConfiguration?.EndAt); + } + + [Fact] + public async Task CreateSchedule_WithNullEndAt_ClearsEndAt() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = DateTimeOffset.UtcNow.AddHours(1) + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var createOptions2 = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + EndAt = null + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + createOperation.State, + createOptions2); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.ScheduleConfiguration?.EndAt); + } + + [Fact] + public async Task RunSchedule_WithMultipleTokens_OnlyExecutesLatestToken() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartImmediatelyIfLate = true + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(); + var initialToken = initialState?.ExecutionToken; + + // Update to get new token + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + new ScheduleUpdateOptions { Interval = TimeSpan.FromMinutes(6) }); + await this.schedule.RunAsync(updateOperation); + + var updatedState = updateOperation.State.GetState(); + var newToken = updatedState?.ExecutionToken; + + // Try to run with old token + var oldTokenOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + updateOperation.State, + initialToken); + await this.schedule.RunAsync(oldTokenOperation); + + // Assert old token operation didn't execute + var stateAfterOldToken = oldTokenOperation.State.GetState(); + Assert.Null(stateAfterOldToken?.LastRunAt); + + // Run with new token + var newTokenOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + oldTokenOperation.State, + newToken); + await this.schedule.RunAsync(newTokenOperation); + + // Assert new token operation executed + var finalState = newTokenOperation.State.GetState(); + Assert.NotNull(finalState?.LastRunAt); + } + + [Fact] + public async Task CreateSchedule_WithLongOrchestrationName_CreatesSchedule() + { + // Arrange + string longName = new string('a', 1000); // 1000 character name + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: longName, + interval: TimeSpan.FromMinutes(5)); + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(longName, scheduleState.ScheduleConfiguration?.OrchestrationName); + } + + [Fact] + public async Task UpdateSchedule_WithLargeInput_UpdatesInput() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string largeInput = new string('x', 10000); // 10KB input + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = largeInput + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(largeInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task RunSchedule_WithPreciseInterval_CalculatesNextRunTimeCorrectly() + { + // Arrange + var startAt = DateTimeOffset.UtcNow; + var interval = TimeSpan.FromSeconds(1.5); // 1.5 seconds + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: interval) + { + StartAt = startAt + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState()?.ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.NotNull(scheduleState.NextRunAt); + var expectedNextRun = startAt.Add(interval); + Assert.Equal(expectedNextRun.Ticks, scheduleState.NextRunAt.Value.Ticks); + } + + [Fact] + public async Task CreateSchedule_WithMinDateTimeOffset_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + StartAt = DateTimeOffset.MinValue + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(DateTimeOffset.MinValue, scheduleState.ScheduleConfiguration?.StartAt); + } + + [Fact] + public async Task UpdateSchedule_WithAllFieldsNull_DoesNotModifyState() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var initialState = createOperation.State.GetState(); + var updateOptions = new ScheduleUpdateOptions(); + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(initialState?.ScheduleConfiguration?.OrchestrationName, scheduleState.ScheduleConfiguration?.OrchestrationName); + Assert.Equal(initialState?.ScheduleConfiguration?.Interval, scheduleState.ScheduleConfiguration?.Interval); + Assert.Equal(initialState?.ScheduleConfiguration?.StartAt, scheduleState.ScheduleConfiguration?.StartAt); + Assert.Equal(initialState?.ScheduleConfiguration?.EndAt, scheduleState.ScheduleConfiguration?.EndAt); + Assert.Equal(initialState?.ScheduleConfiguration?.StartImmediatelyIfLate, scheduleState.ScheduleConfiguration?.StartImmediatelyIfLate); + } + + [Fact] + public async Task RunSchedule_WithIntervalSmallerThanTimeSinceStart_CalculatesCorrectNextRunTime() + { + // Arrange + var startAt = DateTimeOffset.UtcNow.AddMinutes(-10); // 10 minutes ago + var interval = TimeSpan.FromMinutes(3); // 3 minute interval + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: interval) + { + StartAt = startAt + }; + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + createOperation.State.GetState()?.ExecutionToken); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.NotNull(scheduleState.NextRunAt); + + // Calculate expected next run time + // Number of intervals elapsed = 10 minutes / 3 minutes = 3 intervals (rounded down) + // Next run should be at start time + (intervals elapsed + 1) * interval + var expectedNextRun = startAt.AddTicks((3 + 1) * interval.Ticks); + Assert.Equal(expectedNextRun, scheduleState.NextRunAt.Value); + } } \ No newline at end of file From 4472dce9b50f0f368be2bffe2e705706d5bf52d2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:20:56 -0800 Subject: [PATCH 184/203] cleanup --- test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj index f0c9de72..33726a62 100644 --- a/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj +++ b/test/ScheduledTasks.Tests/ScheduledTasks.Tests.csproj @@ -11,16 +11,6 @@ - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - From 740b5502e08c638e07909f6c6b03fd6db519afff Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:15:09 -0800 Subject: [PATCH 185/203] save --- .../Client/ScheduleClientImplTests.cs | 104 ++++++++++++++---- .../Client/ScheduledTaskClientImplTests.cs | 39 ++++--- 2 files changed, 104 insertions(+), 39 deletions(-) diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs index 8d652e8b..e189e2c6 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs @@ -20,8 +20,8 @@ public class ScheduleClientImplTests public ScheduleClientImplTests() { - this.durableTaskClient = new Mock("test", MockBehavior.Strict); - this.entityClient = new Mock("test", MockBehavior.Strict); + this.durableTaskClient = new Mock("test"); + this.entityClient = new Mock("test"); this.logger = new TestLogger(); this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); this.client = new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, this.logger); @@ -37,14 +37,15 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() } [Theory] - [InlineData(null)] - [InlineData("")] - public void Constructor_WithInvalidScheduleId_ThrowsArgumentException(string invalidScheduleId) + [InlineData(null, typeof(ArgumentNullException), "Value cannot be null")] + [InlineData("", typeof(ArgumentException), "Parameter cannot be an empty string")] + public void Constructor_WithInvalidScheduleId_ThrowsCorrectException(string invalidScheduleId, Type expectedExceptionType, string expectedMessage) { // Act & Assert - var ex = Assert.Throws(() => + var ex = Assert.Throws(expectedExceptionType, () => new ScheduleClientImpl(this.durableTaskClient.Object, invalidScheduleId, this.logger)); - Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + + Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -66,11 +67,17 @@ public async Task DescribeAsync_WhenExists_ReturnsDescription() ScheduleConfiguration = new ScheduleConfiguration(this.scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) }; + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), this.scheduleId); + // get key and id + var key = entityInstanceId.Key; + var id = entityInstanceId.Name; + this.entityClient .Setup(c => c.GetEntityAsync( - It.Is(id => id.Name == nameof(Schedule) && id.Key == this.scheduleId), + It.Is(id => id.Name == entityInstanceId.Name && id.Key == entityInstanceId.Key), It.IsAny())) - .ReturnsAsync(new EntityMetadata(new EntityInstanceId(nameof(Schedule), this.scheduleId), state)); + .ReturnsAsync(new EntityMetadata(entityInstanceId, state)); // Act var description = await this.client.DescribeAsync(); @@ -112,23 +119,64 @@ public async Task DeleteAsync_ExecutesDeleteOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { + RuntimeStatus = OrchestrationRuntimeStatus.Completed + }); // Act await this.client.DeleteAsync(); + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), this.scheduleId); // Assert this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == this.scheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == "delete"), - It.IsAny()), + It.IsAny()), // Ensure all arguments match + Times.Once); + + + // Verify that we waited for completion + this.durableTaskClient.Verify( + c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny()), Times.Once); } + [Fact] + public async Task DeleteAsync_WhenOrchestrationFails_ThrowsException() + { + // Arrange + string instanceId = "test-instance"; + string errorMessage = "Test error message"; + + this.durableTaskClient + .Setup(c => c.ScheduleNewOrchestrationInstanceAsync( + It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(instanceId); + + this.durableTaskClient + .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { + RuntimeStatus = OrchestrationRuntimeStatus.Failed, + FailureDetails = new TaskFailureDetails("TestError", errorMessage, null, null) + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => this.client.DeleteAsync()); + + Assert.Contains($"Failed to delete schedule '{this.scheduleId}'", exception.Message); + Assert.Contains(errorMessage, exception.Message); + } + [Fact] public async Task PauseAsync_ExecutesPauseOperation() { @@ -144,18 +192,22 @@ public async Task PauseAsync_ExecutesPauseOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + RuntimeStatus = OrchestrationRuntimeStatus.Completed + }); // Act await this.client.PauseAsync(); + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), this.scheduleId); // Assert this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == this.scheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == nameof(Schedule.PauseSchedule)), It.IsAny()), Times.Once); @@ -176,18 +228,22 @@ public async Task ResumeAsync_ExecutesResumeOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + RuntimeStatus = OrchestrationRuntimeStatus.Completed + }); // Act await this.client.ResumeAsync(); + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), this.scheduleId); // Assert this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == this.scheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == nameof(Schedule.ResumeSchedule)), It.IsAny()), Times.Once); @@ -213,18 +269,22 @@ public async Task UpdateAsync_ExecutesUpdateOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + RuntimeStatus = OrchestrationRuntimeStatus.Completed + }); // Act await this.client.UpdateAsync(updateOptions); + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), this.scheduleId); // Assert this.durableTaskClient.Verify( c => c.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == this.scheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == nameof(Schedule.UpdateSchedule)), It.IsAny()), Times.Once); diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs index 1de5fc82..cd029a92 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; @@ -15,23 +14,23 @@ public class ScheduledTaskClientImplTests { readonly Mock durableTaskClient; readonly Mock entityClient; - readonly Mock> logger; + readonly ILogger logger; readonly ScheduledTaskClientImpl client; public ScheduledTaskClientImplTests() { - this.durableTaskClient = new Mock("test", MockBehavior.Strict); - this.entityClient = new Mock("test", MockBehavior.Strict); - this.logger = new Mock>(MockBehavior.Loose); + this.durableTaskClient = new Mock("test"); + this.entityClient = new Mock("test"); + this.logger = new TestLogger(); this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); - this.client = new ScheduledTaskClientImpl(this.durableTaskClient.Object, this.logger.Object); + this.client = new ScheduledTaskClientImpl(this.durableTaskClient.Object, this.logger); } [Fact] public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.logger.Object)); + var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.logger)); Assert.Equal("durableTaskClient", ex.ParamName); } @@ -58,13 +57,13 @@ public void GetScheduleClient_ReturnsValidClient() } [Theory] - [InlineData(null)] - [InlineData("")] - public void GetScheduleClient_WithInvalidId_ThrowsArgumentException(string scheduleId) + [InlineData(null, typeof(ArgumentNullException), "Value cannot be null")] + [InlineData("", typeof(ArgumentException), "Parameter cannot be an empty string")] + public void GetScheduleClient_WithInvalidId_ThrowsCorrectException(string scheduleId, Type expectedExceptionType, string expectedMessage) { // Act & Assert - var ex = Assert.Throws(() => this.client.GetScheduleClient(scheduleId)); - Assert.Contains("scheduleId cannot be null or empty", ex.Message, StringComparison.OrdinalIgnoreCase); + var ex = Assert.Throws(expectedExceptionType, () => this.client.GetScheduleClient(scheduleId)); + Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -74,21 +73,27 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() var options = new ScheduleCreationOptions("test-schedule", "test-orchestration", TimeSpan.FromMinutes(5)); string instanceId = "test-instance"; + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), options.ScheduleId); + this.durableTaskClient .Setup(x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == options.ScheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == nameof(Schedule.CreateSchedule) && - r.Input == options), + r.Input != null && ((ScheduleCreationOptions)r.Input).Equals(options)), null, default)) .ReturnsAsync(instanceId); this.durableTaskClient .Setup(x => x.WaitForInstanceCompletionAsync(instanceId, true, default)) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId)); + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { + RuntimeStatus = OrchestrationRuntimeStatus.Completed + }); // Act var scheduleClient = await this.client.CreateScheduleAsync(options); @@ -104,7 +109,7 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() r.EntityId.Name == nameof(Schedule) && r.EntityId.Key == options.ScheduleId && r.OperationName == nameof(Schedule.CreateSchedule) && - r.Input == options), + r.Input != null && ((ScheduleCreationOptions)r.Input).Equals(options)), null, default), Times.Once); From 24f5f6421497b167141dfeabb614d2c5781cdd20 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:24:48 -0800 Subject: [PATCH 186/203] unit test fix --- .../Client/ScheduledTaskClientImpl.cs | 21 +++- .../Client/ScheduledTaskClientImplTests.cs | 116 +++++++++++++++--- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index f4fe7c5f..acc6eaa8 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -115,9 +115,24 @@ public override AsyncPageable ListSchedulesAsync(ScheduleQu { List schedules = entityPage.Values .Where(metadata => - (!filter?.Status.HasValue ?? true || metadata.State.Status == filter.Status.Value) && - (filter?.CreatedFrom.HasValue != true || metadata.State.ScheduleCreatedAt > filter.CreatedFrom) && - (filter?.CreatedTo.HasValue != true || metadata.State.ScheduleCreatedAt < filter.CreatedTo)) + { + // If there's no filter, return all items + if (filter == null) + { + return true; + } + + // Check status filter if specified + bool statusMatches = !filter.Status.HasValue || metadata.State.Status == filter.Status.Value; + + // Check created from date filter if specified + bool createdFromMatches = !filter.CreatedFrom.HasValue || metadata.State.ScheduleCreatedAt > filter.CreatedFrom.Value; + + // Check created to date filter if specified + bool createdToMatches = !filter.CreatedTo.HasValue || metadata.State.ScheduleCreatedAt < filter.CreatedTo.Value; + + return statusMatches && createdFromMatches && createdToMatches; + }) .Select(metadata => { ScheduleState state = metadata.State; diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs index cd029a92..29ef569a 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -79,12 +79,7 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() this.durableTaskClient .Setup(x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), - It.Is(r => - r.EntityId.Name == entityInstanceId.Name && - r.EntityId.Key == entityInstanceId.Key && - r.OperationName == nameof(Schedule.CreateSchedule) && - r.Input != null && ((ScheduleCreationOptions)r.Input).Equals(options)), - null, + It.IsAny(), default)) .ReturnsAsync(instanceId); @@ -106,11 +101,12 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), It.Is(r => - r.EntityId.Name == nameof(Schedule) && - r.EntityId.Key == options.ScheduleId && + r.EntityId.Name == entityInstanceId.Name && + r.EntityId.Key == entityInstanceId.Key && r.OperationName == nameof(Schedule.CreateSchedule) && - r.Input != null && ((ScheduleCreationOptions)r.Input).Equals(options)), - null, + ((ScheduleCreationOptions)r.Input).ScheduleId == options.ScheduleId && + ((ScheduleCreationOptions)r.Input).OrchestrationName == options.OrchestrationName && + ((ScheduleCreationOptions)r.Input).Interval == options.Interval), default), Times.Once); } @@ -128,28 +124,65 @@ public async Task CreateScheduleAsync_WithNullOptions_ThrowsArgumentNullExceptio public async Task GetScheduleAsync_WhenExists_ReturnsDescription() { // Arrange + // create now + var now = DateTimeOffset.UtcNow; string scheduleId = "test-schedule"; + var config = new ScheduleConfiguration(scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) + { + StartAt = now.AddMinutes(-10), + EndAt = now.AddDays(1), + StartImmediatelyIfLate = true, + OrchestrationInput = "test-input", + OrchestrationInstanceId = "test-instance" + }; + var state = new ScheduleState { Status = ScheduleStatus.Active, - ScheduleConfiguration = new ScheduleConfiguration(scheduleId, "test-orchestration", TimeSpan.FromMinutes(5)) + ScheduleConfiguration = config, + ExecutionToken = "test-token", + LastRunAt = now.AddMinutes(-5), + NextRunAt = now.AddMinutes(5), + ScheduleCreatedAt = now.AddDays(-1) }; + var metadata = new EntityMetadata( + new EntityInstanceId(nameof(Schedule), scheduleId), + state); + + this.durableTaskClient + .Setup(x => x.Entities) + .Returns(this.entityClient.Object); + + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), scheduleId); + this.entityClient .Setup(x => x.GetEntityAsync( - It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), - true, + It.Is(id => id.Name == entityInstanceId.Name && id.Key == entityInstanceId.Key), default)) - .ReturnsAsync(new EntityMetadata(new EntityInstanceId(nameof(Schedule), scheduleId), state)); + .ReturnsAsync(metadata); // Act var description = await this.client.GetScheduleAsync(scheduleId); + //verify getentityasync is called + this.entityClient.Verify(x => x.GetEntityAsync(entityInstanceId, default), Times.Once); + // Assert Assert.NotNull(description); Assert.Equal(scheduleId, description.ScheduleId); - Assert.Equal(state.Status, description.Status); - Assert.Equal(state.ScheduleConfiguration.OrchestrationName, description.OrchestrationName); + Assert.Equal(ScheduleStatus.Active, description.Status); + Assert.Equal(config.OrchestrationName, description.OrchestrationName); + Assert.Equal(config.OrchestrationInput, description.OrchestrationInput); + Assert.Equal(config.OrchestrationInstanceId, description.OrchestrationInstanceId); + Assert.Equal(config.StartAt, description.StartAt); + Assert.Equal(config.EndAt, description.EndAt); + Assert.Equal(config.Interval, description.Interval); + Assert.Equal(config.StartImmediatelyIfLate, description.StartImmediatelyIfLate); + Assert.Equal("test-token", description.ExecutionToken); + Assert.Equal(now.AddMinutes(-5), description.LastRunAt); + Assert.Equal(now.AddMinutes(5), description.NextRunAt); } [Fact] @@ -158,12 +191,15 @@ public async Task GetScheduleAsync_WhenNotExists_ReturnsNull() // Arrange string scheduleId = "test-schedule"; + // create entity instance id + var entityInstanceId = new EntityInstanceId(nameof(Schedule), scheduleId); + this.entityClient .Setup(x => x.GetEntityAsync( - It.Is(id => id.Name == nameof(Schedule) && id.Key == scheduleId), + It.Is(id => id.Name == entityInstanceId.Name && id.Key == entityInstanceId.Key), true, default)) - .ReturnsAsync((EntityMetadata)null); + .ReturnsAsync((EntityMetadata?)null); // Act var description = await this.client.GetScheduleAsync(scheduleId); @@ -191,6 +227,17 @@ public async Task ListSchedulesAsync_ReturnsSchedules() { Status = ScheduleStatus.Active, ScheduleConfiguration = new ScheduleConfiguration("test-1", "test-orchestration", TimeSpan.FromMinutes(5)) + { + StartAt = DateTimeOffset.UtcNow.AddMinutes(-10), + EndAt = DateTimeOffset.UtcNow.AddDays(1), + StartImmediatelyIfLate = true, + OrchestrationInput = "test-input-1", + OrchestrationInstanceId = "test-instance-1" + }, + ExecutionToken = "test-token-1", + LastRunAt = DateTimeOffset.UtcNow.AddMinutes(-5), + NextRunAt = DateTimeOffset.UtcNow.AddMinutes(5), + ScheduleCreatedAt = DateTimeOffset.UtcNow.AddDays(-1) }), new EntityMetadata( new EntityInstanceId(nameof(Schedule), "test-2"), @@ -198,13 +245,32 @@ public async Task ListSchedulesAsync_ReturnsSchedules() { Status = ScheduleStatus.Active, ScheduleConfiguration = new ScheduleConfiguration("test-2", "test-orchestration", TimeSpan.FromMinutes(5)) + { + StartAt = DateTimeOffset.UtcNow.AddMinutes(-8), + EndAt = DateTimeOffset.UtcNow.AddDays(2), + StartImmediatelyIfLate = true, + OrchestrationInput = "test-input-2", + OrchestrationInstanceId = "test-instance-2" + }, + ExecutionToken = "test-token-2", + LastRunAt = DateTimeOffset.UtcNow.AddMinutes(-3), + NextRunAt = DateTimeOffset.UtcNow.AddMinutes(7), + ScheduleCreatedAt = DateTimeOffset.UtcNow.AddDays(-2) }) }; + this.durableTaskClient + .Setup(x => x.Entities) + .Returns(this.entityClient.Object); + this.entityClient - .Setup(x => x.GetAllEntitiesAsync(It.IsAny())) + .Setup(x => x.GetAllEntitiesAsync( + It.IsAny())) .Returns(Pageable.Create>((continuation, pageSize, cancellation) => - Task.FromResult(new Page>(states.ToList(), null)))); + { + var page = new Page>(states, continuation); + return Task.FromResult(page); + })); // Act var schedules = new List(); @@ -214,8 +280,18 @@ public async Task ListSchedulesAsync_ReturnsSchedules() } // Assert + // verify getallentitiesasync is called + this.entityClient.Verify(x => x.GetAllEntitiesAsync( + It.Is(q => + q.InstanceIdStartsWith == "@test" && + q.IncludeState == true && + q.PageSize == query.PageSize)), Times.Once); + Assert.Equal(2, schedules.Count); Assert.All(schedules, s => Assert.StartsWith("test-", s.ScheduleId)); Assert.All(schedules, s => Assert.Equal(ScheduleStatus.Active, s.Status)); + Assert.All(schedules, s => Assert.NotNull(s.ExecutionToken)); + Assert.All(schedules, s => Assert.NotNull(s.LastRunAt)); + Assert.All(schedules, s => Assert.NotNull(s.NextRunAt)); } } \ No newline at end of file From 368d9267a9a8006d2bc2122e9e01564c0f51a49a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:27:12 -0800 Subject: [PATCH 187/203] cleanup comments --- src/ScheduledTasks/Logging/Logs.Client.cs | 3 --- src/ScheduledTasks/Logging/Logs.Entity.cs | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/ScheduledTasks/Logging/Logs.Client.cs b/src/ScheduledTasks/Logging/Logs.Client.cs index 6a289959..1ee00068 100644 --- a/src/ScheduledTasks/Logging/Logs.Client.cs +++ b/src/ScheduledTasks/Logging/Logs.Client.cs @@ -8,9 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Log messages. /// -/// -/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. -/// static partial class Logs { [LoggerMessage(EventId = 80, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] diff --git a/src/ScheduledTasks/Logging/Logs.Entity.cs b/src/ScheduledTasks/Logging/Logs.Entity.cs index 0fb81c6e..fba047e7 100644 --- a/src/ScheduledTasks/Logging/Logs.Entity.cs +++ b/src/ScheduledTasks/Logging/Logs.Entity.cs @@ -8,9 +8,6 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Log messages. /// -/// -/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs. -/// static partial class Logs { [LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "Schedule '{scheduleId}' is being created")] From 9802725e4e025b0f93cf0bf37e9a9b8bdd4052c0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:55:34 -0800 Subject: [PATCH 188/203] more tests --- .../Entity/ScheduleTests.cs | 485 +++++++++++++++++- 1 file changed, 483 insertions(+), 2 deletions(-) diff --git a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs index 21e38621..ef7ae7d0 100644 --- a/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs +++ b/test/ScheduledTasks.Tests/Entity/ScheduleTests.cs @@ -288,8 +288,10 @@ public async Task RunSchedule_WhenNotActive_ThrowsInvalidOperationException() scheduleStateAfterPause, scheduleStateAfterPause.ExecutionToken); // Act & Assert - await Assert.ThrowsAsync(() => + var exception = await Assert.ThrowsAsync(() => this.schedule.RunAsync(runOp).AsTask()); + // check exception message Schedule must be in Active status to run. + Assert.Contains("Schedule must be in Active status to run.", exception.Message); } [Fact] @@ -817,7 +819,7 @@ public async Task RunSchedule_WithStartImmediatelyIfLate_False_DoesNotRunImmedia var runOperation = new TestEntityOperation( nameof(Schedule.RunSchedule), createOperation.State, - createOperation.State.GetState().ExecutionToken); + createOperation.State.GetState()?.ExecutionToken); await this.schedule.RunAsync(runOperation); // Assert @@ -1310,4 +1312,483 @@ public async Task RunSchedule_WithIntervalSmallerThanTimeSinceStart_CalculatesCo var expectedNextRun = startAt.AddTicks((3 + 1) * interval.Ticks); Assert.Equal(expectedNextRun, scheduleState.NextRunAt.Value); } + + [Fact] + public async Task UpdateSchedule_WithEmptyOrchestrationName_NothingChange() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationName = "" + }; + + // Act & Assert + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // assert nothing changed + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal("TestOrchestration", scheduleState.ScheduleConfiguration?.OrchestrationName); + Assert.Equal(TimeSpan.FromMinutes(5), scheduleState.ScheduleConfiguration?.Interval); + Assert.Equal(createOperation.State.GetState()?.ScheduleConfiguration?.StartAt, scheduleState.ScheduleConfiguration?.StartAt); + Assert.Null(scheduleState.ScheduleConfiguration?.EndAt); + Assert.False(scheduleState.ScheduleConfiguration?.StartImmediatelyIfLate); + Assert.Equal(createOperation.State.GetState()?.ExecutionToken, scheduleState.ExecutionToken); + Assert.Equal(ScheduleStatus.Active, scheduleState.Status); + Assert.Equal(createOperation.State.GetState()?.LastRunAt, scheduleState.LastRunAt); + Assert.Equal(createOperation.State.GetState()?.NextRunAt, scheduleState.NextRunAt); + Assert.Equal(createOperation.State.GetState()?.ScheduleConfiguration?.OrchestrationInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + Assert.Equal(createOperation.State.GetState()?.ScheduleConfiguration?.OrchestrationInstanceId, scheduleState.ScheduleConfiguration?.OrchestrationInstanceId); + Assert.Equal(createOperation.State.GetState()?.ScheduleConfiguration?.OrchestrationName, scheduleState.ScheduleConfiguration?.OrchestrationName); + } + + [Fact] + public async Task RunSchedule_WithEmptyToken_DoesNotRun() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + ""); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + + Assert.Contains($"Schedule '{this.scheduleId}' run cancelled with execution token ''", this.logger.Logs.Last().Message); + } + + [Fact] + public async Task CreateSchedule_WithSpecialCharactersInScheduleId_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: "test@schedule#123$%^", + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal("test@schedule#123$%^", scheduleState.ScheduleConfiguration?.ScheduleId); + } + + [Fact] + public async Task CreateSchedule_WithUnicodeCharactersInScheduleId_CreatesSchedule() + { + // Arrange + var options = new ScheduleCreationOptions( + scheduleId: "测试时间表", + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal("测试时间表", scheduleState.ScheduleConfiguration?.ScheduleId); + } + + [Fact] + public async Task UpdateSchedule_WithVeryLongOrchestrationInstanceId_UpdatesSuccessfully() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string longInstanceId = new string('x', 1000); + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInstanceId = longInstanceId + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(longInstanceId, scheduleState.ScheduleConfiguration?.OrchestrationInstanceId); + } + + [Fact] + public async Task RunSchedule_WithWhitespaceToken_DoesNotRun() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + // Act + var runOperation = new TestEntityOperation( + nameof(Schedule.RunSchedule), + createOperation.State, + " "); + await this.schedule.RunAsync(runOperation); + + // Assert + var state = runOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Null(scheduleState.LastRunAt); + Assert.Contains($"Schedule '{this.scheduleId}' run cancelled with execution token ' '", this.logger.Logs.Last().Message); + } + + [Fact] + public async Task CreateSchedule_WithMaxLengthScheduleId_CreatesSchedule() + { + // Arrange + string maxLengthId = new string('x', 1000); + var options = new ScheduleCreationOptions( + scheduleId: maxLengthId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(maxLengthId, scheduleState.ScheduleConfiguration?.ScheduleId); + } + + [Fact] + public async Task CreateSchedule_WithJsonSpecialCharactersInInput_CreatesSchedule() + { + // Arrange + string specialInput = "{\"key\":\"value\",\"array\":[1,2,3],\"nested\":{\"prop\":true}}"; + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInput = specialInput + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(specialInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task UpdateSchedule_WithJsonSpecialCharactersInInput_UpdatesSuccessfully() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string specialInput = "{\"key\":\"value\",\"array\":[1,2,3],\"nested\":{\"prop\":true}}"; + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = specialInput + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(specialInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task CreateSchedule_WithHtmlSpecialCharactersInInput_CreatesSchedule() + { + // Arrange + string htmlInput = "
Hello & World
"; + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInput = htmlInput + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(htmlInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task UpdateSchedule_WithHtmlSpecialCharactersInInput_UpdatesSuccessfully() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string htmlInput = "
Hello & World
"; + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = htmlInput + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(htmlInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task CreateSchedule_WithMultilineInput_CreatesSchedule() + { + // Arrange + string multilineInput = @"Line 1 +Line 2 +Line 3 +With special chars: !@#$%^&*()"; + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInput = multilineInput + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(multilineInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task UpdateSchedule_WithMultilineInput_UpdatesSuccessfully() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string multilineInput = @"Line 1 +Line 2 +Line 3 +With special chars: !@#$%^&*()"; + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = multilineInput + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(multilineInput, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task CreateSchedule_WithBase64EncodedInput_CreatesSchedule() + { + // Arrange + string base64Input = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("Hello World")); + var options = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)) + { + OrchestrationInput = base64Input + }; + + // Create test operation + var operation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + options); + + // Act + await this.schedule.RunAsync(operation); + + // Assert + var state = operation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(base64Input, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } + + [Fact] + public async Task UpdateSchedule_WithBase64EncodedInput_UpdatesSuccessfully() + { + // Arrange + var createOptions = new ScheduleCreationOptions( + scheduleId: this.scheduleId, + orchestrationName: "TestOrchestration", + interval: TimeSpan.FromMinutes(5)); + + var createOperation = new TestEntityOperation( + nameof(Schedule.CreateSchedule), + new TestEntityState(null), + createOptions); + await this.schedule.RunAsync(createOperation); + + string base64Input = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("Hello World")); + var updateOptions = new ScheduleUpdateOptions + { + OrchestrationInput = base64Input + }; + + // Act + var updateOperation = new TestEntityOperation( + nameof(Schedule.UpdateSchedule), + createOperation.State, + updateOptions); + await this.schedule.RunAsync(updateOperation); + + // Assert + var state = updateOperation.State.GetState(typeof(ScheduleState)); + Assert.NotNull(state); + var scheduleState = Assert.IsType(state); + Assert.Equal(base64Input, scheduleState.ScheduleConfiguration?.OrchestrationInput); + } } \ No newline at end of file From 30b987766d507ca25a959a6dc91a446a32da4502 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:09:36 -0800 Subject: [PATCH 189/203] cleanup --- src/ScheduledTasks/Models/ScheduleConfiguration.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleConfiguration.cs b/src/ScheduledTasks/Models/ScheduleConfiguration.cs index ed310245..8f3d1c3c 100644 --- a/src/ScheduledTasks/Models/ScheduleConfiguration.cs +++ b/src/ScheduledTasks/Models/ScheduleConfiguration.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Diagnostics.CodeAnalysis; - namespace Microsoft.DurableTask.ScheduledTasks; /// @@ -186,7 +184,6 @@ public HashSet Update(ScheduleUpdateOptions updateOptions) return updatedFields; } - [MemberNotNull(nameof(StartAt), nameof(EndAt))] void Validate() { if (this.StartAt.HasValue && this.EndAt.HasValue && this.StartAt.Value > this.EndAt.Value) From ed1bfd8416f0d07077d04d79121470180cd894b9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:30:24 -0800 Subject: [PATCH 190/203] cleanup --- src/ScheduledTasks/Models/ScheduleState.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index af1d1970..91a03fe2 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -51,11 +51,6 @@ public void RefreshScheduleRunExecutionToken() this.ExecutionToken = Guid.NewGuid().ToString("N"); } - /// - /// Gets or sets the time when this schedule was last paused. - /// - public DateTimeOffset? LastPausedAt { get; set; } - /// /// Clears all state fields to their default values. /// @@ -68,6 +63,5 @@ public void ClearState() this.ScheduleCreatedAt = null; this.ScheduleLastModifiedAt = null; this.ScheduleConfiguration = null; - this.LastPausedAt = null; } } \ No newline at end of file From e2ac9d8c2bc827951ddf83ae66f60234a8612d21 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:45:54 -0800 Subject: [PATCH 191/203] link orch with schedule --- src/ScheduledTasks/Entity/Schedule.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 94a235ba..07470551 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -237,7 +237,7 @@ public void RunSchedule(TaskEntityContext context, string executionToken) if (this.State.NextRunAt!.Value <= currentTime) { - this.StartOrchestrationIfNotRunning(context); + this.StartOrchestration(context, currentTime); this.State.LastRunAt = currentTime; this.State.NextRunAt = null; this.State.NextRunAt = this.DetermineNextRunTime(scheduleConfig); @@ -252,13 +252,25 @@ public void RunSchedule(TaskEntityContext context, string executionToken) new SignalEntityOptions { SignalTime = this.State.NextRunAt.Value }); } - void StartOrchestrationIfNotRunning(TaskEntityContext context) + void StartOrchestration(TaskEntityContext context, DateTimeOffset scheduledRunTime) { try { - StartOrchestrationOptions? startOrchestrationOptions = string.IsNullOrEmpty(this.State.ScheduleConfiguration?.OrchestrationInstanceId) - ? null - : new StartOrchestrationOptions(this.State.ScheduleConfiguration.OrchestrationInstanceId); + string? instanceId = this.State.ScheduleConfiguration?.OrchestrationInstanceId; + StartOrchestrationOptions startOrchestrationOptions; + + if (string.IsNullOrEmpty(instanceId)) + { + // Generate unique instance ID based on schedule name and current time + instanceId = $"{this.State.ScheduleConfiguration!.ScheduleId}-{scheduledRunTime:o}"; + startOrchestrationOptions = new StartOrchestrationOptions(instanceId); + } + else + { + // Use configured instance ID which will prevent concurrent runs + startOrchestrationOptions = new StartOrchestrationOptions(instanceId); + } + context.ScheduleNewOrchestration( new TaskName(this.State.ScheduleConfiguration!.OrchestrationName), this.State.ScheduleConfiguration.OrchestrationInput, @@ -268,7 +280,7 @@ void StartOrchestrationIfNotRunning(TaskEntityContext context) { this.logger.ScheduleOperationError( this.State.ScheduleConfiguration!.ScheduleId, - nameof(this.StartOrchestrationIfNotRunning), + nameof(this.StartOrchestration), "Failed to start orchestration", ex); } From a0c54089197093973fb1698a0b48712f594b3382 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:55:49 -0800 Subject: [PATCH 192/203] fb --- .../ScheduleConsoleApp/ScheduleOperations.cs | 4 ++-- .../Client/ScheduleClientImpl.cs | 3 +-- .../Client/ScheduledTaskClientImpl.cs | 3 ++- src/ScheduledTasks/Entity/Schedule.cs | 19 +++++++++++++++++++ src/ScheduledTasks/Models/ScheduleState.cs | 14 -------------- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleOperations.cs index 38546bf7..c68135f1 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleOperations.cs @@ -34,8 +34,8 @@ async Task DeleteExistingSchedulesAsync() // Delete each existing schedule await foreach (ScheduleDescription schedule in schedules) { - ScheduleClient handle = this.scheduledTaskClient.GetScheduleClient(schedule.ScheduleId); - await handle.DeleteAsync(); + ScheduleClient scheduleClient1 = this.scheduledTaskClient.GetScheduleClient(schedule.ScheduleId); + await scheduleClient1.DeleteAsync(); Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } } diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index 10d03465..82ee5ef7 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -80,9 +80,8 @@ public override async Task DescribeAsync(CancellationToken { Check.NotNullOrEmpty(this.ScheduleId, nameof(this.ScheduleId)); - EntityInstanceId entityId = new EntityInstanceId(nameof(Schedule), this.ScheduleId); EntityMetadata? metadata = - await this.durableTaskClient.Entities.GetEntityAsync(entityId, cancellation: cancellation); + await this.durableTaskClient.Entities.GetEntityAsync(this.EntityId, cancellation: cancellation); if (metadata == null) { throw new ScheduleNotFoundException(this.ScheduleId); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index acc6eaa8..3ec8707d 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -91,6 +91,7 @@ public override ScheduleClient GetScheduleClient(string scheduleId) } /// + // TODO: This may returned underfilled or empty pages, since you are applying a filter to the pages that are returned by the entity query. We can probably live with that, given that I don't think there is a simple solution. public override AsyncPageable ListSchedulesAsync(ScheduleQuery? filter = null) { // Create an async pageable using the Pageable.Create helper @@ -101,7 +102,7 @@ public override AsyncPageable ListSchedulesAsync(ScheduleQu // TODO: map to entity query last modified from/to filters EntityQuery query = new EntityQuery { - InstanceIdStartsWith = filter?.ScheduleIdPrefix ?? nameof(Schedule), + InstanceIdStartsWith = $"@{nameof(Schedule)}@{filter?.ScheduleIdPrefix ?? string.Empty}", IncludeState = true, PageSize = filter?.PageSize ?? ScheduleQuery.DefaultPageSize, ContinuationToken = continuationToken, diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index 07470551..f0098412 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -109,6 +109,7 @@ public void UpdateSchedule(TaskEntityContext context, ScheduleUpdateOptions sche { case nameof(this.State.ScheduleConfiguration.StartAt): case nameof(this.State.ScheduleConfiguration.Interval): + case nameof(this.State.ScheduleConfiguration.StartImmediatelyIfLate): this.State.NextRunAt = null; break; @@ -204,6 +205,13 @@ public void ResumeSchedule(TaskEntityContext context) /// Thrown when the schedule is not active or interval is not specified. public void RunSchedule(TaskEntityContext context, string executionToken) { + if (this.State.Status == ScheduleStatus.Uninitialized) + { + // this signal is no longer useful since the schedule has been deleted. + this.State = null!; // delete again, otherwise an uninitialized schedule will stick around + return; + } + ScheduleConfiguration scheduleConfig = this.State.ScheduleConfiguration ?? throw new InvalidOperationException("Schedule configuration is missing."); @@ -228,6 +236,12 @@ public void RunSchedule(TaskEntityContext context, string executionToken) { this.logger.ScheduleRunCancelled(scheduleConfig.ScheduleId, executionToken); this.State.NextRunAt = null; + + context.SignalEntity( + new EntityInstanceId(nameof(Schedule), scheduleConfig.ScheduleId), + "delete", + this.State.ExecutionToken); + return; } @@ -271,6 +285,11 @@ void StartOrchestration(TaskEntityContext context, DateTimeOffset scheduledRunTi startOrchestrationOptions = new StartOrchestrationOptions(instanceId); } + this.logger.ScheduleOperationInfo( + this.State.ScheduleConfiguration!.ScheduleId, + nameof(this.StartOrchestration), + $"Starting new orchestration with instance ID: {instanceId}"); + context.ScheduleNewOrchestration( new TaskName(this.State.ScheduleConfiguration!.OrchestrationName), this.State.ScheduleConfiguration.OrchestrationInput, diff --git a/src/ScheduledTasks/Models/ScheduleState.cs b/src/ScheduledTasks/Models/ScheduleState.cs index 91a03fe2..48783de4 100644 --- a/src/ScheduledTasks/Models/ScheduleState.cs +++ b/src/ScheduledTasks/Models/ScheduleState.cs @@ -50,18 +50,4 @@ public void RefreshScheduleRunExecutionToken() { this.ExecutionToken = Guid.NewGuid().ToString("N"); } - - /// - /// Clears all state fields to their default values. - /// - public void ClearState() - { - this.Status = ScheduleStatus.Uninitialized; - this.ExecutionToken = Guid.NewGuid().ToString("N"); - this.LastRunAt = null; - this.NextRunAt = null; - this.ScheduleCreatedAt = null; - this.ScheduleLastModifiedAt = null; - this.ScheduleConfiguration = null; - } } \ No newline at end of file From ac1924bb4155f0351c86a7e2526036404f0d848e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:16:48 -0800 Subject: [PATCH 193/203] todo --- src/ScheduledTasks/Client/ScheduleClientImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index 82ee5ef7..42336a0d 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -11,6 +11,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Represents a handle to a scheduled task, providing operations for managing the schedule. /// +// TODO: Isolate system entity from user entities class ScheduleClientImpl : ScheduleClient { readonly DurableTaskClient durableTaskClient; From 1144044178df22927196613a1418be2f8156060a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:27:17 -0800 Subject: [PATCH 194/203] fix test --- .../ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs index 29ef569a..ea9fea0b 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs @@ -283,7 +283,7 @@ public async Task ListSchedulesAsync_ReturnsSchedules() // verify getallentitiesasync is called this.entityClient.Verify(x => x.GetAllEntitiesAsync( It.Is(q => - q.InstanceIdStartsWith == "@test" && + q.InstanceIdStartsWith == $"@schedule@test" && q.IncludeState == true && q.PageSize == query.PageSize)), Times.Once); From e2b5c555831c95914a9f3ba33218304cb1ba68eb Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 26 Feb 2025 12:38:36 -0800 Subject: [PATCH 195/203] Update src/ScheduledTasks/Client/ScheduledTaskClient.cs Co-authored-by: Naiyuan Tian <110135109+nytian@users.noreply.github.com> --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 537aac77..78682436 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -35,6 +35,6 @@ public abstract class ScheduledTaskClient /// /// The options for creating the schedule. /// Optional cancellation token. - /// A handle to the created schedule. + /// A ScheduleClient for the created schedule. public abstract Task CreateScheduleAsync(ScheduleCreationOptions creationOptions, CancellationToken cancellation = default); } From 2b3b6bbd98a9ecebc8aff33b9b8a6bd99c7494b5 Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 26 Feb 2025 12:38:44 -0800 Subject: [PATCH 196/203] Update src/ScheduledTasks/Client/ScheduledTaskClient.cs Co-authored-by: Naiyuan Tian <110135109+nytian@users.noreply.github.com> --- src/ScheduledTasks/Client/ScheduledTaskClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduledTaskClient.cs b/src/ScheduledTasks/Client/ScheduledTaskClient.cs index 78682436..edc7658a 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClient.cs @@ -4,7 +4,9 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Base class for managing scheduled tasks in a Durable Task application. +/// Client for managing scheduled tasks. +/// Provides methods to retrieve a ScheduleClient, list all schedules, +/// and get details of a specific schedule. /// public abstract class ScheduledTaskClient { From ee0a1c795f29c19b20115e683630b31a4e21cadf Mon Sep 17 00:00:00 2001 From: wangbill Date: Wed, 26 Feb 2025 12:38:52 -0800 Subject: [PATCH 197/203] Update src/ScheduledTasks/Client/ScheduleClient.cs Co-authored-by: Naiyuan Tian <110135109+nytian@users.noreply.github.com> --- src/ScheduledTasks/Client/ScheduleClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ScheduledTasks/Client/ScheduleClient.cs b/src/ScheduledTasks/Client/ScheduleClient.cs index 979c0765..cdab022e 100644 --- a/src/ScheduledTasks/Client/ScheduleClient.cs +++ b/src/ScheduledTasks/Client/ScheduleClient.cs @@ -4,7 +4,8 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// -/// Represents a handle to a schedule, allowing operations on a specific schedule instance. +/// Client for managing a specific schedule instance. +/// Provides methods to list, create, pause, resume, and manage schedules. /// public abstract class ScheduleClient { From 65db0e26164ff8f2952fcefa50c35df107a57bf7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:37:33 -0800 Subject: [PATCH 198/203] refresh only active --- src/ScheduledTasks/Entity/Schedule.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index f0098412..fbf46f37 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -47,8 +47,11 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (alreadyExists) { this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; - this.State.RefreshScheduleRunExecutionToken(); - this.State.NextRunAt = null; + if (this.State.Status == ScheduleStatus.Active) + { + this.State.RefreshScheduleRunExecutionToken(); + this.State.NextRunAt = null; + } } else { From f63d58ccedb7a45b645e92863e64820a12398a0d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:59:47 -0800 Subject: [PATCH 199/203] Revert "refresh only active" This reverts commit 65db0e26164ff8f2952fcefa50c35df107a57bf7. --- src/ScheduledTasks/Entity/Schedule.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index fbf46f37..f0098412 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -47,11 +47,8 @@ public void CreateSchedule(TaskEntityContext context, ScheduleCreationOptions sc if (alreadyExists) { this.State.ScheduleLastModifiedAt = DateTimeOffset.UtcNow; - if (this.State.Status == ScheduleStatus.Active) - { - this.State.RefreshScheduleRunExecutionToken(); - this.State.NextRunAt = null; - } + this.State.RefreshScheduleRunExecutionToken(); + this.State.NextRunAt = null; } else { From da7bc009e076216ef1569ef82a19d61e4ffe7054 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:55:25 -0800 Subject: [PATCH 200/203] fb --- samples/ScheduleConsoleApp/Program.cs | 3 +- ...{ScheduleOperations.cs => ScheduleDemo.cs} | 27 +++++++++-------- .../Client/ScheduleClientImpl.cs | 30 ++++--------------- .../Client/ScheduledTaskClientImpl.cs | 15 ++-------- src/ScheduledTasks/Entity/Schedule.cs | 4 +-- 5 files changed, 27 insertions(+), 52 deletions(-) rename samples/ScheduleConsoleApp/{ScheduleOperations.cs => ScheduleDemo.cs} (68%) diff --git a/samples/ScheduleConsoleApp/Program.cs b/samples/ScheduleConsoleApp/Program.cs index a91440a2..f2ad4346 100644 --- a/samples/ScheduleConsoleApp/Program.cs +++ b/samples/ScheduleConsoleApp/Program.cs @@ -52,7 +52,6 @@ // Run the schedule operations ScheduledTaskClient scheduledTaskClient = host.Services.GetRequiredService(); -ScheduleOperations scheduleOperations = new ScheduleOperations(scheduledTaskClient); -await scheduleOperations.RunAsync(); +await ScheduleDemo.RunDemoAsync(scheduledTaskClient); await host.StopAsync(); \ No newline at end of file diff --git a/samples/ScheduleConsoleApp/ScheduleOperations.cs b/samples/ScheduleConsoleApp/ScheduleDemo.cs similarity index 68% rename from samples/ScheduleConsoleApp/ScheduleOperations.cs rename to samples/ScheduleConsoleApp/ScheduleDemo.cs index c68135f1..712479d3 100644 --- a/samples/ScheduleConsoleApp/ScheduleOperations.cs +++ b/samples/ScheduleConsoleApp/ScheduleDemo.cs @@ -6,16 +6,19 @@ namespace ScheduleConsoleApp; -class ScheduleOperations(ScheduledTaskClient scheduledTaskClient) +/// +/// Demonstrates various schedule operations in a sample application. +/// +static class ScheduleDemo { - readonly ScheduledTaskClient scheduledTaskClient = scheduledTaskClient ?? throw new ArgumentNullException(nameof(scheduledTaskClient)); - - public async Task RunAsync() + public static async Task RunDemoAsync(ScheduledTaskClient scheduledTaskClient) { + ArgumentNullException.ThrowIfNull(scheduledTaskClient); + try { - await this.DeleteExistingSchedulesAsync(); - await this.CreateAndManageScheduleAsync(); + await DeleteExistingSchedulesAsync(scheduledTaskClient); + await CreateAndManageScheduleAsync(scheduledTaskClient); } catch (Exception ex) { @@ -23,24 +26,24 @@ public async Task RunAsync() } } - async Task DeleteExistingSchedulesAsync() + static async Task DeleteExistingSchedulesAsync(ScheduledTaskClient scheduledTaskClient) { // Define the initial query with the desired page size ScheduleQuery query = new ScheduleQuery { PageSize = 100 }; // Retrieve the pageable collection of schedule IDs - AsyncPageable schedules = this.scheduledTaskClient.ListSchedulesAsync(query); + AsyncPageable schedules = scheduledTaskClient.ListSchedulesAsync(query); // Delete each existing schedule await foreach (ScheduleDescription schedule in schedules) { - ScheduleClient scheduleClient1 = this.scheduledTaskClient.GetScheduleClient(schedule.ScheduleId); - await scheduleClient1.DeleteAsync(); + ScheduleClient scheduleClient = scheduledTaskClient.GetScheduleClient(schedule.ScheduleId); + await scheduleClient.DeleteAsync(); Console.WriteLine($"Deleted schedule {schedule.ScheduleId}"); } } - async Task CreateAndManageScheduleAsync() + static async Task CreateAndManageScheduleAsync(ScheduledTaskClient scheduledTaskClient) { // Create schedule options that runs every 4 seconds ScheduleCreationOptions scheduleOptions = new ScheduleCreationOptions( @@ -53,7 +56,7 @@ async Task CreateAndManageScheduleAsync() }; // Create the schedule and get a handle to it - ScheduleClient scheduleClient = await this.scheduledTaskClient.CreateScheduleAsync(scheduleOptions); + ScheduleClient scheduleClient = await scheduledTaskClient.CreateScheduleAsync(scheduleOptions); // Get and print the initial schedule description await PrintScheduleDescriptionAsync(scheduleClient); diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index 42336a0d..abd2f0ef 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -65,10 +65,7 @@ public override async Task CreateAsync(ScheduleCreationOptions creationOptions, } catch (Exception ex) { - this.logger.ClientError( - nameof(this.CreateAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.CreateAsync), this.ScheduleId, ex); throw; } @@ -115,10 +112,7 @@ public override async Task DescribeAsync(CancellationToken } catch (Exception ex) { - this.logger.ClientError( - nameof(this.DescribeAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.DescribeAsync), this.ScheduleId, ex); throw; } @@ -152,10 +146,7 @@ public override async Task PauseAsync(CancellationToken cancellation = default) } catch (Exception ex) { - this.logger.ClientError( - nameof(this.PauseAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.PauseAsync), this.ScheduleId, ex); throw; } @@ -189,10 +180,7 @@ public override async Task ResumeAsync(CancellationToken cancellation = default) } catch (Exception ex) { - this.logger.ClientError( - nameof(this.ResumeAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.ResumeAsync), this.ScheduleId, ex); throw; } @@ -227,10 +215,7 @@ public override async Task UpdateAsync(ScheduleUpdateOptions updateOptions, Canc } catch (Exception ex) { - this.logger.ClientError( - nameof(this.UpdateAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.UpdateAsync), this.ScheduleId, ex); throw; } @@ -264,10 +249,7 @@ public override async Task DeleteAsync(CancellationToken cancellation = default) } catch (Exception ex) { - this.logger.ClientError( - nameof(this.DeleteAsync), - this.ScheduleId, - ex); + this.logger.ClientError(nameof(this.DeleteAsync), this.ScheduleId, ex); throw; } diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs index 3ec8707d..4c8295a5 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs @@ -40,10 +40,7 @@ public override async Task CreateScheduleAsync(ScheduleCreationO } catch (Exception ex) { - this.logger.ClientError( - nameof(this.CreateScheduleAsync), - creationOptions.ScheduleId, - ex); + this.logger.ClientError(nameof(this.CreateScheduleAsync), creationOptions.ScheduleId, ex); throw; } @@ -74,10 +71,7 @@ public override async Task CreateScheduleAsync(ScheduleCreationO } catch (Exception ex) { - this.logger.ClientError( - nameof(this.GetScheduleAsync), - scheduleId, - ex); + this.logger.ClientError(nameof(this.GetScheduleAsync), scheduleId, ex); throw; } @@ -169,10 +163,7 @@ public override AsyncPageable ListSchedulesAsync(ScheduleQu } catch (Exception ex) { - this.logger.ClientError( - nameof(this.ListSchedulesAsync), - string.Empty, - ex); + this.logger.ClientError(nameof(this.ListSchedulesAsync), string.Empty, ex); throw; } diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index f0098412..e907f942 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -251,8 +251,8 @@ public void RunSchedule(TaskEntityContext context, string executionToken) if (this.State.NextRunAt!.Value <= currentTime) { - this.StartOrchestration(context, currentTime); - this.State.LastRunAt = currentTime; + this.StartOrchestration(context, this.State.NextRunAt!.Value); + this.State.LastRunAt = this.State.NextRunAt!.Value; this.State.NextRunAt = null; this.State.NextRunAt = this.DetermineNextRunTime(scheduleConfig); } From 4ac5908c247563b92d6d7f50f4be8273ebc6b7a0 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 27 Feb 2025 19:24:09 -0800 Subject: [PATCH 201/203] Update ScheduleClientImpl.cs --- src/ScheduledTasks/Client/ScheduleClientImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/ScheduleClientImpl.cs index abd2f0ef..ef73337e 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/ScheduleClientImpl.cs @@ -12,6 +12,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// Represents a handle to a scheduled task, providing operations for managing the schedule. ///
// TODO: Isolate system entity from user entities +// TODO: Rename to DefaultScheduleClient class ScheduleClientImpl : ScheduleClient { readonly DurableTaskClient durableTaskClient; From bd3d662d6e71cab9707a07acd67b167d2a8daf34 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:20:20 -0800 Subject: [PATCH 202/203] rename schedule clients to default prefix --- ...eduleClientImpl.cs => DefaultScheduleClient.cs} | 7 +++---- ...ClientImpl.cs => DefaultScheduledTaskClient.cs} | 6 +++--- .../DurableTaskClientBuilderExtensions.cs | 4 ++-- ...tImplTests.cs => DefaultScheduleClientTests.cs} | 14 +++++++------- ...Tests.cs => DefaultScheduledTaskClientTests.cs} | 12 ++++++------ 5 files changed, 21 insertions(+), 22 deletions(-) rename src/ScheduledTasks/Client/{ScheduleClientImpl.cs => DefaultScheduleClient.cs} (97%) rename src/ScheduledTasks/Client/{ScheduledTaskClientImpl.cs => DefaultScheduledTaskClient.cs} (95%) rename test/ScheduledTasks.Tests/Client/{ScheduleClientImplTests.cs => DefaultScheduleClientTests.cs} (95%) rename test/ScheduledTasks.Tests/Client/{ScheduledTaskClientImplTests.cs => DefaultScheduledTaskClientTests.cs} (97%) diff --git a/src/ScheduledTasks/Client/ScheduleClientImpl.cs b/src/ScheduledTasks/Client/DefaultScheduleClient.cs similarity index 97% rename from src/ScheduledTasks/Client/ScheduleClientImpl.cs rename to src/ScheduledTasks/Client/DefaultScheduleClient.cs index ef73337e..1a3bb44e 100644 --- a/src/ScheduledTasks/Client/ScheduleClientImpl.cs +++ b/src/ScheduledTasks/Client/DefaultScheduleClient.cs @@ -12,20 +12,19 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// Represents a handle to a scheduled task, providing operations for managing the schedule. ///
// TODO: Isolate system entity from user entities -// TODO: Rename to DefaultScheduleClient -class ScheduleClientImpl : ScheduleClient +class DefaultScheduleClient : ScheduleClient { readonly DurableTaskClient durableTaskClient; readonly ILogger logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The durable task client. /// The ID of the schedule. /// The logger. /// Thrown if or is null. - public ScheduleClientImpl(DurableTaskClient client, string scheduleId, ILogger logger) + public DefaultScheduleClient(DurableTaskClient client, string scheduleId, ILogger logger) : base(scheduleId) { this.durableTaskClient = client ?? throw new ArgumentNullException(nameof(client)); diff --git a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs b/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs similarity index 95% rename from src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs rename to src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs index 4c8295a5..adb58d4d 100644 --- a/src/ScheduledTasks/Client/ScheduledTaskClientImpl.cs +++ b/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs @@ -11,7 +11,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// Client for managing scheduled tasks in a Durable Task application. /// #pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public class ScheduledTaskClientImpl(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient +public class DefaultScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient #pragma warning restore CA1711 // Identifiers should not have incorrect suffix { readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); @@ -26,7 +26,7 @@ public override async Task CreateScheduleAsync(ScheduleCreationO try { // Create schedule client instance - ScheduleClient scheduleClient = new ScheduleClientImpl(this.durableTaskClient, creationOptions.ScheduleId, this.logger); + ScheduleClient scheduleClient = new DefaultScheduleClient(this.durableTaskClient, creationOptions.ScheduleId, this.logger); // Create the schedule using the client await scheduleClient.CreateAsync(creationOptions, cancellation); @@ -81,7 +81,7 @@ public override async Task CreateScheduleAsync(ScheduleCreationO public override ScheduleClient GetScheduleClient(string scheduleId) { Check.NotNullOrEmpty(scheduleId, nameof(scheduleId)); - return new ScheduleClientImpl(this.durableTaskClient, scheduleId, this.logger); + return new DefaultScheduleClient(this.durableTaskClient, scheduleId, this.logger); } /// diff --git a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs index 33a35aca..1ea7b6bd 100644 --- a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs @@ -22,8 +22,8 @@ public static IDurableTaskClientBuilder UseScheduledTasks(this IDurableTaskClien builder.Services.AddTransient(sp => { DurableTaskClient client = sp.GetRequiredService(); - ILogger logger = sp.GetRequiredService>(); - return new ScheduledTaskClientImpl(client, logger); + ILogger logger = sp.GetRequiredService>(); + return new DefaultScheduledTaskClient(client, logger); }); return builder; diff --git a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs b/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs similarity index 95% rename from test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs rename to test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs index e189e2c6..70cc60fc 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduleClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs @@ -10,21 +10,21 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; -public class ScheduleClientImplTests +public class DefaultScheduleClientTests { readonly Mock durableTaskClient; readonly Mock entityClient; readonly ILogger logger; - readonly ScheduleClientImpl client; + readonly DefaultScheduleClient client; readonly string scheduleId = "test-schedule"; - public ScheduleClientImplTests() + public DefaultScheduleClientTests() { this.durableTaskClient = new Mock("test"); this.entityClient = new Mock("test"); this.logger = new TestLogger(); this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); - this.client = new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, this.logger); + this.client = new DefaultScheduleClient(this.durableTaskClient.Object, this.scheduleId, this.logger); } [Fact] @@ -32,7 +32,7 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert ArgumentNullException ex = Assert.Throws(() => - new ScheduleClientImpl(null!, this.scheduleId, this.logger)); + new DefaultScheduleClient(null!, this.scheduleId, this.logger)); Assert.Equal("client", ex.ParamName); } @@ -43,7 +43,7 @@ public void Constructor_WithInvalidScheduleId_ThrowsCorrectException(string inva { // Act & Assert var ex = Assert.Throws(expectedExceptionType, () => - new ScheduleClientImpl(this.durableTaskClient.Object, invalidScheduleId, this.logger)); + new DefaultScheduleClient(this.durableTaskClient.Object, invalidScheduleId, this.logger)); Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -53,7 +53,7 @@ public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert var ex = Assert.Throws(() => - new ScheduleClientImpl(this.durableTaskClient.Object, this.scheduleId, null!)); + new DefaultScheduleClient(this.durableTaskClient.Object, this.scheduleId, null!)); Assert.Equal("logger", ex.ParamName); } diff --git a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs b/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs similarity index 97% rename from test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs rename to test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs index ea9fea0b..4eeada24 100644 --- a/test/ScheduledTasks.Tests/Client/ScheduledTaskClientImplTests.cs +++ b/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs @@ -10,27 +10,27 @@ namespace Microsoft.DurableTask.ScheduledTasks.Tests.Client; -public class ScheduledTaskClientImplTests +public class DefaultScheduledTaskClientTests { readonly Mock durableTaskClient; readonly Mock entityClient; readonly ILogger logger; - readonly ScheduledTaskClientImpl client; + readonly DefaultScheduledTaskClient client; - public ScheduledTaskClientImplTests() + public DefaultScheduledTaskClientTests() { this.durableTaskClient = new Mock("test"); this.entityClient = new Mock("test"); this.logger = new TestLogger(); this.durableTaskClient.Setup(x => x.Entities).Returns(this.entityClient.Object); - this.client = new ScheduledTaskClientImpl(this.durableTaskClient.Object, this.logger); + this.client = new DefaultScheduledTaskClient(this.durableTaskClient.Object, this.logger); } [Fact] public void Constructor_WithNullClient_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => new ScheduledTaskClientImpl(null!, this.logger)); + var ex = Assert.Throws(() => new DefaultScheduledTaskClient(null!, this.logger)); Assert.Equal("durableTaskClient", ex.ParamName); } @@ -38,7 +38,7 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() public void Constructor_WithNullLogger_ThrowsArgumentNullException() { // Act & Assert - var ex = Assert.Throws(() => new ScheduledTaskClientImpl(this.durableTaskClient.Object, null!)); + var ex = Assert.Throws(() => new DefaultScheduledTaskClient(this.durableTaskClient.Object, null!)); Assert.Equal("logger", ex.ParamName); } From 98767c8ef1ffc1f8daa779ffc0f3e3a898384415 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 2 Mar 2025 23:17:04 -0800 Subject: [PATCH 203/203] fb --- .../Client/DefaultScheduledTaskClient.cs | 4 +--- src/ScheduledTasks/Entity/Schedule.cs | 6 +++--- .../DurableTaskClientBuilderExtensions.cs | 9 +-------- ...ns.cs => DurableTaskWorkerBuilderExtensions.cs} | 2 +- src/ScheduledTasks/Logging/Logs.Client.cs | 14 +++++++------- 5 files changed, 13 insertions(+), 22 deletions(-) rename src/ScheduledTasks/Extension/{DurableTaskSchedulerWorkerExtensions.cs => DurableTaskWorkerBuilderExtensions.cs} (93%) diff --git a/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs b/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs index adb58d4d..9ef85d0f 100644 --- a/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs +++ b/src/ScheduledTasks/Client/DefaultScheduledTaskClient.cs @@ -10,9 +10,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Client for managing scheduled tasks in a Durable Task application. /// -#pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public class DefaultScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient -#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +class DefaultScheduledTaskClient(DurableTaskClient durableTaskClient, ILogger logger) : ScheduledTaskClient { readonly DurableTaskClient durableTaskClient = Check.NotNull(durableTaskClient, nameof(durableTaskClient)); readonly ILogger logger = Check.NotNull(logger, nameof(logger)); diff --git a/src/ScheduledTasks/Entity/Schedule.cs b/src/ScheduledTasks/Entity/Schedule.cs index e907f942..516702e1 100644 --- a/src/ScheduledTasks/Entity/Schedule.cs +++ b/src/ScheduledTasks/Entity/Schedule.cs @@ -286,9 +286,9 @@ void StartOrchestration(TaskEntityContext context, DateTimeOffset scheduledRunTi } this.logger.ScheduleOperationInfo( - this.State.ScheduleConfiguration!.ScheduleId, + this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.StartOrchestration), - $"Starting new orchestration with instance ID: {instanceId}"); + $"Starting new orchestration named '{this.State.ScheduleConfiguration?.OrchestrationName ?? string.Empty}' with instance ID: {instanceId}"); context.ScheduleNewOrchestration( new TaskName(this.State.ScheduleConfiguration!.OrchestrationName), @@ -298,7 +298,7 @@ void StartOrchestration(TaskEntityContext context, DateTimeOffset scheduledRunTi catch (Exception ex) { this.logger.ScheduleOperationError( - this.State.ScheduleConfiguration!.ScheduleId, + this.State.ScheduleConfiguration?.ScheduleId ?? string.Empty, nameof(this.StartOrchestration), "Failed to start orchestration", ex); diff --git a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs index 1ea7b6bd..159ec31a 100644 --- a/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskClientBuilderExtensions.cs @@ -3,7 +3,6 @@ using Microsoft.DurableTask.Client; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Microsoft.DurableTask.ScheduledTasks; @@ -19,13 +18,7 @@ public static class DurableTaskClientBuilderExtensions /// The original builder, for call chaining. public static IDurableTaskClientBuilder UseScheduledTasks(this IDurableTaskClientBuilder builder) { - builder.Services.AddTransient(sp => - { - DurableTaskClient client = sp.GetRequiredService(); - ILogger logger = sp.GetRequiredService>(); - return new DefaultScheduledTaskClient(client, logger); - }); - + builder.Services.AddSingleton(); return builder; } } diff --git a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs b/src/ScheduledTasks/Extension/DurableTaskWorkerBuilderExtensions.cs similarity index 93% rename from src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs rename to src/ScheduledTasks/Extension/DurableTaskWorkerBuilderExtensions.cs index aa75fa6b..0dbdafb6 100644 --- a/src/ScheduledTasks/Extension/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/ScheduledTasks/Extension/DurableTaskWorkerBuilderExtensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.DurableTask.ScheduledTasks; /// /// Extension methods for configuring Durable Task workers to use the Azure Durable Task Scheduler service. /// -public static class DurableTaskSchedulerWorkerExtensions +public static class DurableTaskWorkerBuilderExtensions { /// /// Adds scheduled tasks support to the worker builder. diff --git a/src/ScheduledTasks/Logging/Logs.Client.cs b/src/ScheduledTasks/Logging/Logs.Client.cs index 1ee00068..440e13db 100644 --- a/src/ScheduledTasks/Logging/Logs.Client.cs +++ b/src/ScheduledTasks/Logging/Logs.Client.cs @@ -13,24 +13,24 @@ static partial class Logs [LoggerMessage(EventId = 80, Level = LogLevel.Information, Message = "Creating schedule with options: {scheduleConfigCreateOptions}")] public static partial void ClientCreatingSchedule(this ILogger logger, ScheduleCreationOptions scheduleConfigCreateOptions); - [LoggerMessage(EventId = 82, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] + [LoggerMessage(EventId = 81, Level = LogLevel.Information, Message = "Pausing schedule '{scheduleId}'")] public static partial void ClientPausingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 83, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] + [LoggerMessage(EventId = 82, Level = LogLevel.Information, Message = "Resuming schedule '{scheduleId}'")] public static partial void ClientResumingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 84, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}'")] + [LoggerMessage(EventId = 83, Level = LogLevel.Information, Message = "Updating schedule '{scheduleId}'")] public static partial void ClientUpdatingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 85, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] + [LoggerMessage(EventId = 84, Level = LogLevel.Information, Message = "Deleting schedule '{scheduleId}'")] public static partial void ClientDeletingSchedule(this ILogger logger, string scheduleId); - [LoggerMessage(EventId = 86, Level = LogLevel.Information, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 85, Level = LogLevel.Information, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientInfo(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 87, Level = LogLevel.Warning, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 86, Level = LogLevel.Warning, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientWarning(this ILogger logger, string message, string scheduleId); - [LoggerMessage(EventId = 88, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] + [LoggerMessage(EventId = 87, Level = LogLevel.Error, Message = "{message} (ScheduleId: {scheduleId})")] public static partial void ClientError(this ILogger logger, string message, string scheduleId, Exception? exception = null); }