From 3301b03da6565a78840a79e8a4a2bf6d89bf777b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 6 Jul 2024 17:13:05 -0500 Subject: [PATCH 01/64] Added .NET client for new Dapr Jobs API support Signed-off-by: Whit Waldo --- README.md | 1 + all.sln | 14 +++ src/Dapr.Jobs/AssemblyInfo.cs | 16 +++ src/Dapr.Jobs/Dapr.Jobs.csproj | 23 ++++ src/Dapr.Jobs/DaprJobsClient.cs | 106 ++++++++++++++++++ src/Dapr.Jobs/DaprJobsServiceException.cs | 38 +++++++ .../JobsServiceCollectionExtensions.cs | 50 +++++++++ src/Dapr.Jobs/MalformedJobException.cs | 38 +++++++ src/Dapr.Jobs/ScheduleJobOptions.cs | 31 +++++ src/Dapr.Jobs/TimeSpanExtensions.cs | 48 ++++++++ test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj | 21 ++++ test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs | 75 +++++++++++++ 12 files changed, 461 insertions(+) create mode 100644 src/Dapr.Jobs/AssemblyInfo.cs create mode 100644 src/Dapr.Jobs/Dapr.Jobs.csproj create mode 100644 src/Dapr.Jobs/DaprJobsClient.cs create mode 100644 src/Dapr.Jobs/DaprJobsServiceException.cs create mode 100644 src/Dapr.Jobs/JobsServiceCollectionExtensions.cs create mode 100644 src/Dapr.Jobs/MalformedJobException.cs create mode 100644 src/Dapr.Jobs/ScheduleJobOptions.cs create mode 100644 src/Dapr.Jobs/TimeSpanExtensions.cs create mode 100644 test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj create mode 100644 test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs diff --git a/README.md b/README.md index 948516fe..b55dc89f 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ This repo builds the following packages: - Dapr.Actors.AspNetCore - Dapr.Extensions.Configuration - Dapr.Workflow +- Dapr.Jobs ### Prerequisites diff --git a/all.sln b/all.sln index 22804785..8fd6b28d 100644 --- a/all.sln +++ b/all.sln @@ -118,6 +118,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{D34F9326-8D8C-43C4-975B-7201A9C97E6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{EDEE625E-6815-40E1-935F-35129771A0F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -290,6 +294,14 @@ Global {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Release|Any CPU.Build.0 = Release|Any CPU + {EDEE625E-6815-40E1-935F-35129771A0F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDEE625E-6815-40E1-935F-35129771A0F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,6 +355,8 @@ Global {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {D34F9326-8D8C-43C4-975B-7201A9C97E6E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {EDEE625E-6815-40E1-935F-35129771A0F8} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Jobs/AssemblyInfo.cs b/src/Dapr.Jobs/AssemblyInfo.cs new file mode 100644 index 00000000..870a8dde --- /dev/null +++ b/src/Dapr.Jobs/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj new file mode 100644 index 00000000..675c3f5a --- /dev/null +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -0,0 +1,23 @@ + + + + net6;net7;net8 + enable + enable + Dapr.Jobs + Dapr Jobs Authoring SDK + Dapr Jobs SDK for scheduling jobs and tasks with Dapr + 0.1.0 + alpha + + + + + + + + + + + + diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs new file mode 100644 index 00000000..d36d57fd --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -0,0 +1,106 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Net; +using System.Net.Http.Json; + +namespace Dapr.Jobs; + +/// +/// Defines client operations for managing Dapr jobs. +/// +public class DaprJobsClient +{ + private readonly HttpClient httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// The used to communicate with the Dapr sidecar. + public DaprJobsClient(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + /// + /// Schedules a job with a name. + /// + /// The name of the job being scheduled. + /// A string value providing any related content. Content is returned when the reminder expires. + /// Specifies the time after which this job is invoked. + public async Task ScheduleJobAsync(string name, object jsonSerializableData, TimeSpan dueTime) + { + ArgumentNullException.ThrowIfNull(name, nameof(name)); + ArgumentNullException.ThrowIfNull(jsonSerializableData, nameof(jsonSerializableData)); + ArgumentNullException.ThrowIfNull(dueTime, nameof(dueTime)); + + var options = + new ScheduleJobOptions(new ScheduleJobInnerOptions(jsonSerializableData, dueTime.ToDurationString())); + + var response = await httpClient.PostAsJsonAsync($"/{name}", options); + + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + throw new MalformedJobException(response); + case HttpStatusCode.InternalServerError: + throw new DaprJobsServiceException(response); + default: + return; + } + } + + /// + /// Gets a job from its name. + /// + /// The name of the scheduled job being retrieved. + /// The job data. + public async Task GetJobDataAsync(string name) + { + ArgumentNullException.ThrowIfNull(name, nameof(name)); + + var response = await httpClient.GetAsync($"/{name}"); + + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + throw new MalformedJobException(response); + case HttpStatusCode.InternalServerError: + throw new DaprJobsServiceException(response); + default: + return await response.Content.ReadAsStringAsync(); + } + } + + /// + /// Deletes a named job. + /// + /// The name of the job being deleted. + /// + public async Task DeleteJobAsync(string name) + { + ArgumentNullException.ThrowIfNull(name, nameof(name)); + + var response = await httpClient.DeleteAsync($"/{name}"); + + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + throw new MalformedJobException(response); + case HttpStatusCode.InternalServerError: + throw new DaprJobsServiceException(response); + default: + return; + } + } +} diff --git a/src/Dapr.Jobs/DaprJobsServiceException.cs b/src/Dapr.Jobs/DaprJobsServiceException.cs new file mode 100644 index 00000000..e88d0719 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsServiceException.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs; + +/// +/// The exception type thrown when an exception is encountered using the Dapr Jobs service. +/// +[Serializable] +public class DaprJobsServiceException : Exception +{ + /// + /// Initializes a new for a non-successful HTTP request. + /// + /// + public DaprJobsServiceException(HttpResponseMessage? response) : base(FormatExceptionForFailedRequest()) + { + Response = response; + } + + /// + /// Gets the of the request that failed. Will be null if the + /// failure was not related to an HTTP request or preventing the response from being received. + /// + public HttpResponseMessage? Response { get; } + + private static string FormatExceptionForFailedRequest() => "An exception occurred while interacting with the Jobs API"; +} diff --git a/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs new file mode 100644 index 00000000..9a2bf5fd --- /dev/null +++ b/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Jobs; + +/// +/// Contains extension methods for using Dapr Jobs with dependency injection. +/// +public static class JobsServiceCollectionExtensions +{ + /// + /// Adds Dapr Jobs support to the service collection. + /// + /// The . + public static IServiceCollection AddDaprJobs(this IServiceCollection serviceCollection) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + serviceCollection.AddHttpClient(httpClient => + { + var jobsHttpEndpoint = new Uri($"{DaprDefaults.GetDefaultHttpEndpoint()}/v1.0-alpha1/jobs/"); + if (jobsHttpEndpoint.Scheme != "http" && jobsHttpEndpoint.Scheme != "https") + throw new InvalidOperationException("The HTTP endpoint must use http or https"); + + httpClient.BaseAddress = jobsHttpEndpoint; + httpClient.DefaultRequestHeaders.Accept.Add( + new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); + + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + httpClient.DefaultRequestHeaders.Add("dapr-api-token", daprApiToken); + } + }); + + return serviceCollection; + } +} diff --git a/src/Dapr.Jobs/MalformedJobException.cs b/src/Dapr.Jobs/MalformedJobException.cs new file mode 100644 index 00000000..1eba8922 --- /dev/null +++ b/src/Dapr.Jobs/MalformedJobException.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs; + +/// +/// The exception type thrown when an malformed request is made to the Dapr Jobs service. +/// +[Serializable] +public class MalformedJobException : Exception +{ + /// + /// Initializes a new for a non-successful HTTP request. + /// + /// + public MalformedJobException(HttpResponseMessage? response) : base(FormatExceptionForFailedRequest()) + { + Response = response; + } + + /// + /// Gets the of the request that failed. Will be null if the + /// failure was not related to an HTTP request or preventing the response from being received. + /// + public HttpResponseMessage? Response { get; } + + private static string FormatExceptionForFailedRequest() => "The request made to the Jobs API was malformed"; +} diff --git a/src/Dapr.Jobs/ScheduleJobOptions.cs b/src/Dapr.Jobs/ScheduleJobOptions.cs new file mode 100644 index 00000000..5c77b0a7 --- /dev/null +++ b/src/Dapr.Jobs/ScheduleJobOptions.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Dapr.Jobs; + +/// +/// The options used to schedule a new job. +/// +/// The internal job options. +internal record ScheduleJobOptions([property: JsonPropertyName("job")] ScheduleJobInnerOptions JobData); + +/// +/// The payload used to schedule a new job. +/// +/// The JSON serializable data payload. +/// The +internal record ScheduleJobInnerOptions( + [property: JsonPropertyName("data")] object JsonSerializableData, + [property: JsonPropertyName("dueTime")] string DueTime); diff --git a/src/Dapr.Jobs/TimeSpanExtensions.cs b/src/Dapr.Jobs/TimeSpanExtensions.cs new file mode 100644 index 00000000..38840b41 --- /dev/null +++ b/src/Dapr.Jobs/TimeSpanExtensions.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; + +namespace Dapr.Jobs; + +/// +/// Provides extension methods used with . +/// +internal static class TimeSpanExtensions +{ + /// + /// Creates a duration string that matches the specification at https://pkg.go.dev/time#ParseDuration per the + /// Jobs API specification https://v1-14.docs.dapr.io/reference/api/jobs_api/#schedule-a-job. + /// + /// The timespan being evaluated. + /// + public static string ToDurationString(this TimeSpan timespan) + { + var sb = new StringBuilder(); + + //Hours is the largest unit of measure in the duration string + if (timespan.Hours > 0) + sb.Append($"{timespan.Hours}h"); + + if (timespan.Minutes > 0) + sb.Append($"{timespan.Minutes}m"); + + if (timespan.Seconds > 0) + sb.Append($"{timespan.Seconds}s"); + + if (timespan.Milliseconds > 0) + sb.Append($"{timespan.Milliseconds}ms"); + + return sb.ToString(); + } +} diff --git a/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj new file mode 100644 index 00000000..1dd8858e --- /dev/null +++ b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj @@ -0,0 +1,21 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs b/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs new file mode 100644 index 00000000..3f75e03c --- /dev/null +++ b/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class TimeSpanExtensionsTest +{ + [Fact] + public void ToDurationString_ValidateHours() + { + var fourHours = TimeSpan.FromHours(4); + var result = fourHours.ToDurationString(); + + Assert.Equal("4h", result); + } + + [Fact] + public void ToDurationString_ValidateMinutes() + { + var elevenMinutes = TimeSpan.FromMinutes(11); + var result = elevenMinutes.ToDurationString(); + + Assert.Equal("11m", result); + } + + [Fact] + public void ToDurationString_ValidateSeconds() + { + var fortySeconds = TimeSpan.FromSeconds(40); + var result = fortySeconds.ToDurationString(); + + Assert.Equal("40s", result); + } + + [Fact] + public void ToDurationString_ValidateMilliseconds() + { + var tenMilliseconds = TimeSpan.FromMilliseconds(10); + var result = tenMilliseconds.ToDurationString(); + + Assert.Equal("10ms", result); + } + + [Fact] + public void ToDurationString_HoursAndMinutes() + { + var ninetyMinutes = TimeSpan.FromMinutes(90); + var result = ninetyMinutes.ToDurationString(); + + Assert.Equal("1h30m", result); + } + + [Fact] + public void ToDurationString_Combined() + { + var time = TimeSpan.FromHours(2) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(24) + + TimeSpan.FromMilliseconds(28); + var result = time.ToDurationString(); + + Assert.Equal("2h4m24s28ms", result); + } +} From 5732ca5f84755fe4253679da821a038e9e7d919b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 8 Jul 2024 16:19:49 -0500 Subject: [PATCH 02/64] Removed explicit .NET 7 targeting Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Dapr.Jobs.csproj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index 675c3f5a..751d03b0 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -1,7 +1,7 @@  - net6;net7;net8 + net6;net8 enable enable Dapr.Jobs @@ -12,8 +12,9 @@ - - + + + From debf7ad3c27a1e6cb1dce063946cc83073cc9df5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 8 Jul 2024 16:20:49 -0500 Subject: [PATCH 03/64] Using `AddDaprJobsClient` for consistency with other SDKs Signed-off-by: Whit Waldo --- src/Dapr.Jobs/JobsServiceCollectionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs index 9a2bf5fd..fad93725 100644 --- a/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs @@ -21,10 +21,10 @@ namespace Dapr.Jobs; public static class JobsServiceCollectionExtensions { /// - /// Adds Dapr Jobs support to the service collection. + /// Adds Dapr Jobs client support to the service collection. /// /// The . - public static IServiceCollection AddDaprJobs(this IServiceCollection serviceCollection) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); From c3b2a8c9e6c425646f8c00e1e9c1d35a67736b6d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:48:23 -0500 Subject: [PATCH 04/64] Refactored the DaprClientBuilder out to a common project. Updated dependencies in both Dapr.Client and Dapr.AspNetCore Signed-off-by: Whit Waldo --- src/Dapr.AspNetCore/Dapr.AspNetCore.csproj | 1 + .../DaprServiceCollectionExtensions.cs | 3 +- src/Dapr.Client/Dapr.Client.csproj | 3 +- src/Dapr.Client/DaprClientBuilder.cs | 190 +++--------------- src/Dapr.Client/properties/AssemblyInfo.cs | 2 +- src/Dapr.Common/Dapr.Common.csproj | 18 ++ src/Dapr.Common/DaprGenericClientBuilder.cs | 175 ++++++++++++++++ 7 files changed, 228 insertions(+), 164 deletions(-) create mode 100644 src/Dapr.Common/Dapr.Common.csproj create mode 100644 src/Dapr.Common/DaprGenericClientBuilder.cs diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index 54996e4b..6440279d 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 388015b8..9bfafb06 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection { using System; - using System.Linq; using Dapr.Client; using Extensions; diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 1a348cc8..7731b005 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -21,9 +21,8 @@ - - + diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 50a4979d..3c90f86d 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,180 +11,52 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client -{ - using System; - using System.Net.Http; - using System.Text.Json; - using Grpc.Net.Client; - using Autogenerated = Autogen.Grpc.v1; +using System; +using System.Net.Http; +using Dapr.Common; +using Grpc.Net.Client; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +namespace Dapr.Client; + +/// +/// Build for building a . +/// +public sealed class DaprClientBuilder : DaprGenericClientBuilder +{ /// - /// Builder for building + /// Builds the client instance from the properties of the builder. /// - public sealed class DaprClientBuilder + public override DaprClient Build() { - /// - /// Initializes a new instance of the class. - /// - public DaprClientBuilder() - { - this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); - - this.GrpcChannelOptions = new GrpcChannelOptions() - { - // The gRPC client doesn't throw the right exception for cancellation - // by default, this switches that behavior on. - ThrowOperationCanceledOnCancellation = true, - }; - - this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - } - - // property exposed for testing purposes - internal string GrpcEndpoint { get; private set; } - - // property exposed for testing purposes - internal string HttpEndpoint { get; private set; } - - private Func HttpClientFactory { get; set; } - - // property exposed for testing purposes - internal JsonSerializerOptions JsonSerializerOptions { get; private set; } - - // property exposed for testing purposes - internal GrpcChannelOptions GrpcChannelOptions { get; private set; } - internal string DaprApiToken { get; private set; } - internal TimeSpan Timeout { get; private set; } - - /// - /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback - /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the - /// corresponding environment variables. - /// - /// The instance. - public DaprClientBuilder UseHttpEndpoint(string httpEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); - this.HttpEndpoint = httpEndpoint; - return this; - } - - // Internal for testing of DaprClient - internal DaprClientBuilder UseHttpClientFactory(Func factory) - { - this.HttpClientFactory = factory; - return this; - } - - /// - /// Overrides the gRPC endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the - /// DAPR_GRPC_PORT environment variable. - /// - /// The instance. - public DaprClientBuilder UseGrpcEndpoint(string grpcEndpoint) + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") { - ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); - this.GrpcEndpoint = grpcEndpoint; - return this; + throw new InvalidOperationException("The gRPC endpoint must use http or https."); } - /// - /// - /// Uses the specified when serializing or deserializing using . - /// - /// - /// The default value is created using . - /// - /// - /// Json serialization options. - /// The instance. - public DaprClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) { - this.JsonSerializerOptions = options; - return this; + // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - /// - /// Uses the provided for creating the . - /// - /// The to use for creating the . - /// The instance. - public DaprClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { - this.GrpcChannelOptions = grpcChannelOptions; - return this; + throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - /// - /// Adds the provided on every request to the Dapr runtime. - /// - /// The token to be added to the request headers/>. - /// The instance. - public DaprClientBuilder UseDaprApiToken(string apiToken) - { - this.DaprApiToken = apiToken; - return this; - } + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - /// - /// Sets the timeout for the HTTP client used by the . - /// - /// - /// - public DaprClientBuilder UseTimeout(TimeSpan timeout) + var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + if (this.Timeout > TimeSpan.Zero) { - this.Timeout = timeout; - return this; + httpClient.Timeout = this.Timeout; } - /// - /// Builds a instance from the properties of the builder. - /// - /// The . - public DaprClient Build() - { - var grpcEndpoint = new Uri(this.GrpcEndpoint); - if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The gRPC endpoint must use http or https."); - } - - if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) - { - // Set correct switch to maksecure gRPC service calls. This switch must be set before creating the GrpcChannel. - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - var httpEndpoint = new Uri(this.HttpEndpoint); - if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The HTTP endpoint must use http or https."); - } - - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - var client = new Autogenerated.Dapr.DaprClient(channel); - - - var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); - var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); - - if (this.Timeout > TimeSpan.Zero) - { - httpClient.Timeout = this.Timeout; - } - - return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); - } + var client = new Autogenerated.Dapr.DaprClient(channel); + var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); + return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/src/Dapr.Client/properties/AssemblyInfo.cs b/src/Dapr.Client/properties/AssemblyInfo.cs index ee00f7bf..4decc064 100644 --- a/src/Dapr.Client/properties/AssemblyInfo.cs +++ b/src/Dapr.Client/properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj new file mode 100644 index 00000000..a715123c --- /dev/null +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs new file mode 100644 index 00000000..8a2b81be --- /dev/null +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -0,0 +1,175 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common +{ + using System; + using System.Net.Http; + using System.Text.Json; + using Grpc.Net.Client; + + /// + /// Builder for building a generic Dapr client. + /// + public abstract class DaprGenericClientBuilder where TClientBuilder : class + { + /// + /// Initializes a new instance of the class. + /// + public DaprGenericClientBuilder() + { + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + + this.GrpcChannelOptions = new GrpcChannelOptions() + { + // The gRPC client doesn't throw the right exception for cancellation + // by default, this switches that behavior on. + ThrowOperationCanceledOnCancellation = true, + }; + + this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + } + + /// + /// Property exposed for testing purposes. + /// + public string GrpcEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + public string HttpEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + public Func? HttpClientFactory { get; set; } + + /// + /// Property exposed for testing purposes. + /// + public JsonSerializerOptions JsonSerializerOptions { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + public GrpcChannelOptions GrpcChannelOptions { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public string DaprApiToken { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public TimeSpan Timeout { get; private set; } + + /// + /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// The instance. + public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); + this.HttpEndpoint = httpEndpoint; + return this; + } + + /// + /// Exposed internally for testing purposes. + /// + internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) + { + this.HttpClientFactory = factory; + return this; + } + + /// + /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// The instance. + public DaprGenericClientBuilder UseGrpcEndpoint(string grpcEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); + this.GrpcEndpoint = grpcEndpoint; + return this; + } + + /// + /// + /// Uses the specified when serializing or deserializing using . + /// + /// + /// The default value is created using . + /// + /// + /// Json serialization options. + /// The instance. + public DaprGenericClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + { + this.JsonSerializerOptions = options; + return this; + } + + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + /// The instance. + public DaprGenericClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + return this; + } + + /// + /// Adds the provided on every request to the Dapr runtime. + /// + /// The token to be added to the request headers/>. + /// The instance. + public DaprGenericClientBuilder UseDaprApiToken(string apiToken) + { + this.DaprApiToken = apiToken; + return this; + } + + /// + /// Sets the timeout for the HTTP client used by the Dapr client. + /// + /// + /// + public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + public abstract TClientBuilder Build(); + } +} From 12fb7960f4949f75ad75c72351e5299104cbba6d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:49:07 -0500 Subject: [PATCH 05/64] Renamed to reflect that it's a Dapr exception Signed-off-by: Whit Waldo --- ...alformedJobException.cs => DaprMalformedJobException.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/Dapr.Jobs/{MalformedJobException.cs => DaprMalformedJobException.cs} (84%) diff --git a/src/Dapr.Jobs/MalformedJobException.cs b/src/Dapr.Jobs/DaprMalformedJobException.cs similarity index 84% rename from src/Dapr.Jobs/MalformedJobException.cs rename to src/Dapr.Jobs/DaprMalformedJobException.cs index 1eba8922..37170d85 100644 --- a/src/Dapr.Jobs/MalformedJobException.cs +++ b/src/Dapr.Jobs/DaprMalformedJobException.cs @@ -14,16 +14,16 @@ namespace Dapr.Jobs; /// -/// The exception type thrown when an malformed request is made to the Dapr Jobs service. +/// The exception type thrown when a malformed request is made to the Dapr Scheduler service. /// [Serializable] -public class MalformedJobException : Exception +public class DaprMalformedJobException : Exception { /// /// Initializes a new for a non-successful HTTP request. /// /// - public MalformedJobException(HttpResponseMessage? response) : base(FormatExceptionForFailedRequest()) + public DaprMalformedJobException(HttpResponseMessage? response) : base(FormatExceptionForFailedRequest()) { Response = response; } From cfd74f779d68957bbb8d3a6928b1efdc74eb42e3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:49:53 -0500 Subject: [PATCH 06/64] Updated solution to propertly find Dapr.Common in /src directory Signed-off-by: Whit Waldo --- all.sln | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/all.sln b/all.sln index 8fd6b28d..43f5257c 100644 --- a/all.sln +++ b/all.sln @@ -120,7 +120,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Cl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{D34F9326-8D8C-43C4-975B-7201A9C97E6E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{EDEE625E-6815-40E1-935F-35129771A0F8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{EDEE625E-6815-40E1-935F-35129771A0F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{3E075F71-185E-4C09-9449-79D21A958487}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -302,6 +304,10 @@ Global {EDEE625E-6815-40E1-935F-35129771A0F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.Build.0 = Release|Any CPU + {3E075F71-185E-4C09-9449-79D21A958487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E075F71-185E-4C09-9449-79D21A958487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -357,6 +363,7 @@ Global {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} {D34F9326-8D8C-43C4-975B-7201A9C97E6E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {EDEE625E-6815-40E1-935F-35129771A0F8} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {3E075F71-185E-4C09-9449-79D21A958487} = {27C5D71D-0721-4221-9286-B94AB07B58CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} From 24ae98ce531903e295de0883215eaffb164e2a21 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:50:40 -0500 Subject: [PATCH 07/64] Added scheduler.proto and updated project file to autogenerate appropriate gRPC types + client Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Dapr.Jobs.csproj | 16 +- .../dapr/proto/scheduler/v1/scheduler.proto | 160 ++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index 751d03b0..b95e33ff 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -12,13 +12,23 @@ + + - - - + + + + + + + + + + + diff --git a/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto b/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto new file mode 100644 index 00000000..aa3978f1 --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto @@ -0,0 +1,160 @@ +syntax = "proto3"; + +package dapr.proto.scheduler.v1; + +import "google/protobuf/any.proto"; + +option csharp_namespace = "Dapr.Scheduler.Autogen.Grpc.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/scheduler/v1;scheduler"; + +service Scheduler { + // ScheduleJob is used by the daprd sidecar to schedule a job. + rpc ScheduleJob(ScheduleJobRequest) returns (ScheduleJobResponse) {} + // Get a job + rpc GetJob(GetJobRequest) returns (GetJobResponse) {} + // DeleteJob is used by the daprd sidecar to delete a job. + rpc DeleteJob(DeleteJobRequest) returns (DeleteJobResponse) {} + // WatchJobs is used by the daprd sidecar to connect to the Scheduler + // service to watch for jobs triggering back. + rpc WatchJobs(stream WatchJobsRequest) returns (stream WatchJobsResponse) {} +} + +message Job { + // The schedule for the job. + optional string schedule = 1; + + // Optional: jobs with fixed repeat counts (accounting for Actor Reminders). + optional uint32 repeats = 2; + + // Optional: sets time at which or time interval before the callback is invoked for the first time. + optional string due_time = 3; + + // Optional: Time To Live to allow for auto deletes (accounting for Actor Reminders). + optional string ttl = 4; + + // Job data. + google.protobuf.Any data = 5; +} + +// TargetJob is the message used by the daprd sidecar to schedule a job +// from an App. +message TargetJob {} + +// TargetActorReminder is the message used by the daprd sidecar to +// schedule a job from an Actor Reminder. +message TargetActorReminder { + // id is the actor ID. + string id = 1; + + // type is the actor type. + string type = 2; +} + +// JobTargetMetadata holds the typed metadata associated with the job for +// different origins. +message JobTargetMetadata { + oneof type { + TargetJob job = 1; + TargetActorReminder actor = 2; + } +} + +// JobMetadata is the message used by the daprd sidecar to schedule/get/delete a +// job. +message JobMetadata { + // app_id is the App ID of the requester. + string app_id = 1; + + // namespace is the namespace of the requester. + string namespace = 2; + + // target is the type of the job. + JobTargetMetadata target = 3; +} + +// WatchJobsRequest is the message used by the daprd sidecar to connect to the +// Scheduler and send Job process results. +message WatchJobsRequest { + oneof watch_job_request_type { + WatchJobsRequestInitial initial = 1; + WatchJobsRequestResult result = 2; + } +} + +// WatchJobsRequestInitial is the initial request to start watching for jobs. +message WatchJobsRequestInitial { + // app_id is the App ID of the requester. + string app_id = 1; + + // namespace is the namespace of the requester. + string namespace = 2; + + // actor_types is the optional list of actor types to watch for. + repeated string actor_types = 3; +} + +// WatchJobsRequestResult is the result of a job execution to allow the job to +// be marked as processed. +message WatchJobsRequestResult { + // id is the id of the job that has finished processing, used as an incremental counter. + uint64 id = 1; +} + +// WatchJobsResponse is the response message to convey the details of a job. +message WatchJobsResponse { + // name is the name of the job which was triggered. + string name = 1; + + // id is the id of the job trigger event which should be sent back from + // the client to be marked as processed, used as an incremental counter. + uint64 id = 2; + + // Job data. + google.protobuf.Any data = 3; + + // The metadata associated with the job. + JobMetadata metadata = 4; +} + +message ScheduleJobRequest { + // name is the name of the job to create. + string name = 1; + + // The job to be scheduled. + Job job = 2; + + // The metadata associated with the job. + JobMetadata metadata = 3; +} + +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message used by the daprd sidecar to delete or get a job. +message GetJobRequest { + // name is the name of the job. + string name = 1; + + // The metadata associated with the job. + JobMetadata metadata = 2; +} + +// GetJobResponse is the response message to convey the details of a job. +message GetJobResponse { + // The job to be scheduled. + Job job = 1; +} + +// DeleteJobRequest is the message used by the daprd sidecar to delete or get a job. +message DeleteJobRequest { + string name = 1; + + // The metadata associated with the job. + JobMetadata metadata = 2; +} + +// DeleteJobRequest is the response message used by the daprd sidecar to delete or get a job. +message DeleteJobResponse { + // Empty +} From 4e5d856606ca164e335dec309af7d2e6c868f92f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:51:13 -0500 Subject: [PATCH 08/64] Removed unused type Signed-off-by: Whit Waldo --- src/Dapr.Jobs/ScheduleJobOptions.cs | 31 ----------------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/Dapr.Jobs/ScheduleJobOptions.cs diff --git a/src/Dapr.Jobs/ScheduleJobOptions.cs b/src/Dapr.Jobs/ScheduleJobOptions.cs deleted file mode 100644 index 5c77b0a7..00000000 --- a/src/Dapr.Jobs/ScheduleJobOptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2024 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using System.Text.Json.Serialization; - -namespace Dapr.Jobs; - -/// -/// The options used to schedule a new job. -/// -/// The internal job options. -internal record ScheduleJobOptions([property: JsonPropertyName("job")] ScheduleJobInnerOptions JobData); - -/// -/// The payload used to schedule a new job. -/// -/// The JSON serializable data payload. -/// The -internal record ScheduleJobInnerOptions( - [property: JsonPropertyName("data")] object JsonSerializableData, - [property: JsonPropertyName("dueTime")] string DueTime); From d0e577ba7549387fb14cbefaaa584c044b941f8e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:56:19 -0500 Subject: [PATCH 09/64] Fixed an XML comment that didn't make sense Signed-off-by: Whit Waldo --- src/Dapr.Client/DaprClientBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 3c90f86d..b600b4dc 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -20,7 +20,7 @@ namespace Dapr.Client; /// -/// Build for building a . +/// Builds a . /// public sealed class DaprClientBuilder : DaprGenericClientBuilder { From 5faa91471ecb9b4c0d1f92a569aaf9814113066d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 06:58:53 -0500 Subject: [PATCH 10/64] Implemented service-specific equivalent of DaprClientBuilder for Dapr Jobs Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobClientBuilder.cs | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/Dapr.Jobs/DaprJobClientBuilder.cs diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobClientBuilder.cs new file mode 100644 index 00000000..05d2db71 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobClientBuilder.cs @@ -0,0 +1,62 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Grpc.Net.Client; +using Autogenerated = Dapr.Scheduler.Autogen.Grpc.v1.Scheduler; + +namespace Dapr.Jobs; + +/// +/// Builds a . +/// +public sealed class DaprJobClientBuilder : DaprGenericClientBuilder +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + public override DaprJobsClient Build() + { + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The gRPC endpoint must use http or https."); + } + + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) + { + // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The HTTP endpoint must use http or https."); + } + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + + var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + if (this.Timeout > TimeSpan.Zero) + { + httpClient.Timeout = this.Timeout; + } + + var client = new Autogenerated.SchedulerClient(channel); + var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); + + return new DaprJobsGrpcClient(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); + } +} From 4ca40eb34bb8d76665128bdd75ce408a35a95a57 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 07:10:30 -0500 Subject: [PATCH 11/64] Updated to support DaprJobClientOptions which specify the App ID and namespace the job is being requested by Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobClientBuilder.cs | 13 ++++++++++++- src/Dapr.Jobs/DaprJobClientOptions.cs | 28 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/Dapr.Jobs/DaprJobClientOptions.cs diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobClientBuilder.cs index 05d2db71..58f3a238 100644 --- a/src/Dapr.Jobs/DaprJobClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobClientBuilder.cs @@ -22,6 +22,17 @@ namespace Dapr.Jobs; /// public sealed class DaprJobClientBuilder : DaprGenericClientBuilder { + private readonly DaprJobClientOptions options; + + /// + /// Used to construct a new instance of . + /// + /// + public DaprJobClientBuilder(DaprJobClientOptions options) + { + this.options = options; + } + /// /// Builds the client instance from the properties of the builder. /// @@ -57,6 +68,6 @@ public override DaprJobsClient Build() var client = new Autogenerated.SchedulerClient(channel); var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); - return new DaprJobsGrpcClient(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); + return new DaprJobsGrpcClient(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader, this.options); } } diff --git a/src/Dapr.Jobs/DaprJobClientOptions.cs b/src/Dapr.Jobs/DaprJobClientOptions.cs new file mode 100644 index 00000000..e16e20e5 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobClientOptions.cs @@ -0,0 +1,28 @@ +namespace Dapr.Jobs; + +/// +/// Options used to configure the Dapr job client. +/// +public class DaprJobClientOptions +{ + /// + /// Initializes a new instance of . + /// + /// The ID of the app . + /// The namespace of the app. + public DaprJobClientOptions(string appId, string appNamespace) + { + AppId = appId; + Namespace = appNamespace; + } + + /// + /// The App ID of the requester. + /// + public string AppId { get; init; } + + /// + /// The namespace of the requester. + /// + public string Namespace { get; init; } +} From b0291bf970a1385ce82f676c2864b10525b53d08 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 07:10:58 -0500 Subject: [PATCH 12/64] Added DI registration extensions that support options Signed-off-by: Whit Waldo --- .../DaprJobsServiceCollectionExtensions.cs | 66 +++++++++++++++++++ .../JobsServiceCollectionExtensions.cs | 50 -------------- 2 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs delete mode 100644 src/Dapr.Jobs/JobsServiceCollectionExtensions.cs diff --git a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs new file mode 100644 index 00000000..9e17ef68 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Jobs; + +/// +/// Contains extension methods for using Dapr Jobs with dependency injection. +/// +public static class DaprJobsServiceCollectionExtensions +{ + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// The options used to configure the . + /// Optionally allows greater configuration of the . + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, DaprJobClientOptions options, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + + serviceCollection.TryAddSingleton(_ => + { + var builder = new DaprJobClientBuilder(options); + configure?.Invoke(builder); + + return builder.Build(); + }); + + return serviceCollection; + } + + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// The options used to configure the . + /// Optionally allows greater configuration of the using injected services. + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, DaprJobClientOptions options, + Action? configure = null) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + serviceCollection.TryAddSingleton(serviceProvider => + { + var builder = new DaprJobClientBuilder(options); + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + } +} diff --git a/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs deleted file mode 100644 index fad93725..00000000 --- a/src/Dapr.Jobs/JobsServiceCollectionExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2024 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Microsoft.Extensions.DependencyInjection; - -namespace Dapr.Jobs; - -/// -/// Contains extension methods for using Dapr Jobs with dependency injection. -/// -public static class JobsServiceCollectionExtensions -{ - /// - /// Adds Dapr Jobs client support to the service collection. - /// - /// The . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection) - { - ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - - serviceCollection.AddHttpClient(httpClient => - { - var jobsHttpEndpoint = new Uri($"{DaprDefaults.GetDefaultHttpEndpoint()}/v1.0-alpha1/jobs/"); - if (jobsHttpEndpoint.Scheme != "http" && jobsHttpEndpoint.Scheme != "https") - throw new InvalidOperationException("The HTTP endpoint must use http or https"); - - httpClient.BaseAddress = jobsHttpEndpoint; - httpClient.DefaultRequestHeaders.Accept.Add( - new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); - - var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - if (!string.IsNullOrEmpty(daprApiToken)) - { - httpClient.DefaultRequestHeaders.Add("dapr-api-token", daprApiToken); - } - }); - - return serviceCollection; - } -} From 797be63760ca9ba8f167256f278535b60de01685 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 07:14:43 -0500 Subject: [PATCH 13/64] Refactored out DaprException to common project - retained same namespace so nothing is changed in Dapr.Client Signed-off-by: Whit Waldo --- src/{Dapr.Client => Dapr.Common}/DaprException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename src/{Dapr.Client => Dapr.Common}/DaprException.cs (96%) diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Common/DaprException.cs similarity index 96% rename from src/Dapr.Client/DaprException.cs rename to src/Dapr.Common/DaprException.cs index e7b1efab..2b600ef3 100644 --- a/src/Dapr.Client/DaprException.cs +++ b/src/Dapr.Common/DaprException.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Runtime.Serialization; namespace Dapr From c4b028be9cb4e6f10899976fd5a84b8a66f758d6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 08:43:35 -0500 Subject: [PATCH 14/64] Fixed method not returning properly Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs index 9e17ef68..8c10ea46 100644 --- a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs @@ -62,5 +62,7 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi return builder.Build(); }); + + return serviceCollection; } } From c15e0900ef153a4de7f04dd5d9ca0b4fe0525942 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 09:00:23 -0500 Subject: [PATCH 15/64] Added extension method for parsing a TimeSpan from a Golang interval Signed-off-by: Whit Waldo --- src/Dapr.Jobs/TimeSpanExtensions.cs | 40 ++++++++++++++ test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs | 52 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/Dapr.Jobs/TimeSpanExtensions.cs b/src/Dapr.Jobs/TimeSpanExtensions.cs index 38840b41..fba1acde 100644 --- a/src/Dapr.Jobs/TimeSpanExtensions.cs +++ b/src/Dapr.Jobs/TimeSpanExtensions.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System.Text; +using System.Text.RegularExpressions; namespace Dapr.Jobs; @@ -45,4 +46,43 @@ public static string ToDurationString(this TimeSpan timespan) return sb.ToString(); } + + /// + /// Creates a given a Golang duration string. + /// + /// The duration string to parse. + /// A timespan value. + public static TimeSpan FromDurationString(this string interval) + { + interval = interval.Replace("ms", "q"); + + //Define regular expressions to capture each segment + var hourRegex = new Regex(@"(\d+)h"); + var minuteRegex = new Regex(@"(\d+)m"); + var secondRegex = new Regex(@"(\d+)s"); + var millisecondRegex = new Regex(@"(\d+)q"); + + int hours = 0; + int minutes = 0; + int seconds = 0; + int milliseconds = 0; + + var hourMatch = hourRegex.Match(interval); + if (hourMatch.Success) + hours = int.Parse(hourMatch.Groups[1].Value); + + var minuteMatch = minuteRegex.Match(interval); + if (minuteMatch.Success) + minutes = int.Parse(minuteMatch.Groups[1].Value); + + var secondMatch = secondRegex.Match(interval); + if (secondMatch.Success) + seconds = int.Parse(secondMatch.Groups[1].Value); + + var millisecondMatch = millisecondRegex.Match(interval); + if (millisecondMatch.Success) + milliseconds = int.Parse(millisecondMatch.Groups[1].Value); + + return new TimeSpan(0, hours, minutes, seconds, milliseconds); + } } diff --git a/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs b/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs index 3f75e03c..ffc22e96 100644 --- a/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/TimeSpanExtensionsTest.cs @@ -72,4 +72,56 @@ public void ToDurationString_Combined() Assert.Equal("2h4m24s28ms", result); } + + [Fact] + public void FromDurationString_AllSegments() + { + const string interval = "13h57m4s10ms"; + var result = interval.FromDurationString(); + + Assert.Equal(13, result.Hours); + Assert.Equal(57, result.Minutes); + Assert.Equal(4, result.Seconds); + Assert.Equal(10, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments1() + { + const string interval = "5h12ms"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Hours); + Assert.Equal(12, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments2() + { + const string interval = "5m"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Minutes); + } + + [Fact] + public void FromDurationString_LimitedSegments3() + { + const string interval = "16s43ms"; + var result = interval.FromDurationString(); + + Assert.Equal(16, result.Seconds); + Assert.Equal(43, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments4() + { + const string interval = "4h32m16s"; + var result = interval.FromDurationString(); + + Assert.Equal(4, result.Hours); + Assert.Equal(32, result.Minutes); + Assert.Equal(16, result.Seconds); + } } From f3a7c413a4e7c4cca99bed02d73f80c04f1239ec Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 09:01:01 -0500 Subject: [PATCH 16/64] Implemented all but the Watch method on the client Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 132 +++---- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 348 +++++++++++++++++++ src/Dapr.Jobs/Models/Responses/JobDetails.cs | 50 +++ 3 files changed, 464 insertions(+), 66 deletions(-) create mode 100644 src/Dapr.Jobs/DaprJobsGrpcClient.cs create mode 100644 src/Dapr.Jobs/Models/Responses/JobDetails.cs diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index d36d57fd..f7136baf 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -11,96 +11,96 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Net; -using System.Net.Http.Json; +using System.Text.Json; +using Dapr.Jobs.Models.Responses; +using Dapr.Scheduler.Autogen.Grpc.v1; +using Google.Protobuf; namespace Dapr.Jobs; /// /// Defines client operations for managing Dapr jobs. /// -public class DaprJobsClient +public abstract class DaprJobsClient { - private readonly HttpClient httpClient; - /// - /// Initializes a new instance of the class. + /// Gets the used for JSON serialization purposes. /// - /// The used to communicate with the Dapr sidecar. - public DaprJobsClient(HttpClient httpClient) - { - this.httpClient = httpClient; - } + public abstract JsonSerializerOptions JsonSerializerOptions { get; } /// - /// Schedules a job with a name. + /// Schedules a recurring job using a cron expression. /// - /// The name of the job being scheduled. - /// A string value providing any related content. Content is returned when the reminder expires. - /// Specifies the time after which this job is invoked. - public async Task ScheduleJobAsync(string name, object jsonSerializableData, TimeSpan dueTime) - { - ArgumentNullException.ThrowIfNull(name, nameof(name)); - ArgumentNullException.ThrowIfNull(jsonSerializableData, nameof(jsonSerializableData)); - ArgumentNullException.ThrowIfNull(dueTime, nameof(dueTime)); - - var options = - new ScheduleJobOptions(new ScheduleJobInnerOptions(jsonSerializableData, dueTime.ToDurationString())); - - var response = await httpClient.PostAsJsonAsync($"/{name}", options); + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// The main payload of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime, + uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; - switch (response.StatusCode) - { - case HttpStatusCode.BadRequest: - throw new MalformedJobException(response); - case HttpStatusCode.InternalServerError: - throw new DaprJobsServiceException(response); - default: - return; - } - } + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// The main payload of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom, + uint? repeats = null, DateTime? ttl = null, T? payload = default, + CancellationToken cancellationToken = default) where T : IMessage; /// - /// Gets a job from its name. + /// Schedules a one-time job. /// - /// The name of the scheduled job being retrieved. - /// The job data. - public async Task GetJobDataAsync(string name) - { - ArgumentNullException.ThrowIfNull(name, nameof(name)); + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// Stores the main payload of the job which is passed to the trigger function. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, T? payload = default, + CancellationToken cancellationToken = default) where T : IMessage; - var response = await httpClient.GetAsync($"/{name}"); + /// + /// Retrieves the details of a registered job. + /// + /// The jobName of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> GetJobAsync(string jobName, CancellationToken cancellationToken = default) + where T : IMessage, new(); - switch (response.StatusCode) - { - case HttpStatusCode.BadRequest: - throw new MalformedJobException(response); - case HttpStatusCode.InternalServerError: - throw new DaprJobsServiceException(response); - default: - return await response.Content.ReadAsStringAsync(); - } - } + /// + /// Deletes the specified job. + /// + /// The jobName of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default); /// - /// Deletes a named job. + /// Watches for triggered jobs. /// - /// The name of the job being deleted. + /// Cancellation token. /// - public async Task DeleteJobAsync(string name) - { - ArgumentNullException.ThrowIfNull(name, nameof(name)); - - var response = await httpClient.DeleteAsync($"/{name}"); + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> WatchJobsAsync(CancellationToken cancellationToken = default); - switch (response.StatusCode) + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) { - case HttpStatusCode.BadRequest: - throw new MalformedJobException(response); - case HttpStatusCode.InternalServerError: - throw new DaprJobsServiceException(response); - default: - return; + return null; } + + return new KeyValuePair("dapr-api-token", apiToken); } } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs new file mode 100644 index 00000000..1750b662 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -0,0 +1,348 @@ +using System.Net.Http.Headers; +using System.Reflection; +using System.Text.Json; +using System.Text.RegularExpressions; +using Dapr.Jobs.Models.Responses; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Net.Client; +using Autogenerated = Dapr.Scheduler.Autogen.Grpc.v1; + +namespace Dapr.Jobs; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal class DaprJobsGrpcClient : DaprJobsClient +{ + private readonly Uri httpEndpoint; + private readonly HttpClient httpClient; + + private readonly JsonSerializerOptions jsonSerializerOptions; + + private readonly GrpcChannel channel; + private readonly Autogenerated.Scheduler.SchedulerClient client; + private readonly KeyValuePair? apiTokenHeader; + private readonly DaprJobClientOptions options; + + // property exposed for testing purposes + internal Autogenerated.Scheduler.SchedulerClient Client => client; + + public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; + + internal DaprJobsGrpcClient( + GrpcChannel channel, + Autogenerated.Scheduler.SchedulerClient innerClient, + HttpClient httpClient, + Uri httpEndpoint, + JsonSerializerOptions jsonSerializerOptions, + KeyValuePair? apiTokenHeader, + DaprJobClientOptions options) + { + this.channel = channel; + this.client = innerClient; + this.httpClient = httpClient; + this.httpEndpoint = httpEndpoint; + this.jsonSerializerOptions = jsonSerializerOptions; + this.apiTokenHeader = apiTokenHeader; + this.options = options; + + this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); + } + + /// + /// Schedules a recurring job using a cron expression. + /// + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// The main payload of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime, uint? repeats = null, + DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + if (string.IsNullOrWhiteSpace(cronExpression)) + throw new ArgumentNullException(nameof(cronExpression)); + + var job = new Autogenerated.Job { Schedule = cronExpression }; + + if (dueTime is not null) + job.DueTime = ((DateTime)dueTime).ToString("O"); + + if (repeats is not null) + job.Repeats = (uint)repeats; + + if (payload is not null) + job.Data = Any.Pack(payload); + + if (ttl is not null) + { + if (ttl <= dueTime) + throw new ArgumentException( + $"When both {nameof(ttl)} and {nameof(dueTime)} are specified, {nameof(ttl)} must represent a later point in time"); + + job.Ttl = ((DateTime)ttl).ToString("O"); + } + + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata + { + Job = new() + } + }; + + var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.ScheduleJobAsync(envelope, callOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Schedule job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// The main payload of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom, uint? repeats = null, + DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var job = new Autogenerated.Job { Schedule = interval.ToDurationString() }; + + if (startingFrom is not null) + job.DueTime = ((DateTime)startingFrom).ToString("O"); + + if (repeats is not null) + job.Repeats = (uint)repeats; + + if (payload is not null) + job.Data = Any.Pack(payload); + + if (ttl is not null) + { + if (ttl < startingFrom) + throw new ArgumentException( + $"When both {nameof(ttl)} and {nameof(startingFrom)} are specified, the {nameof(ttl)} must represent a later point in time"); + + job.Ttl = ((DateTime)ttl).ToString("O"); + } + + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata + { + Job = new() + } + }; + + var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.ScheduleJobAsync(envelope, callOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Schedule job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Schedules a one-time job. + /// + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// Stores the main payload of the job which is passed to the trigger function. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task ScheduleJobAsync(string jobName, DateTime scheduledTime, T? payload = default, + CancellationToken cancellationToken = default) where T : default + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var job = new Autogenerated.Job { DueTime = scheduledTime.ToString("O") }; + + if (payload is not null) + job.Data = Any.Pack(payload); + + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata + { + Job = new() + } + }; + + var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.ScheduleJobAsync(envelope, callOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Schedule job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Retrieves the details of a registered job. + /// + /// The name of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> GetJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata { Job = new() } + }; + + var envelope = new Autogenerated.GetJobRequest { Name = jobName, Metadata = jobMetadata }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetJobResponse response; + + try + { + response = await client.GetJobAsync(envelope, callOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Get job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + var intervalRegex = new Regex("h|m|(ms)|s"); + + return new JobDetails + { + DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, + TTL = response.Job.Ttl is not null ? DateTime.Parse(response.Job.Ttl) : null, + Interval = response.Job.Schedule is not null && intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule.FromDurationString() : null, + CronExpression = response.Job.Schedule is null || !intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule : null, + RepeatCount = response.Job.Repeats == default ? null : response.Job.Repeats, + Payload = response.Job.Data.Unpack() + }; + } + + /// + /// Deletes the specified job. + /// + /// The name of the job. + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata + { + Job = new() + } + }; + + var envelope = new Autogenerated.DeleteJobRequest { Name = jobName, Metadata = jobMetadata}; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.DeleteJobAsync(envelope, callOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Delete job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Watches for triggered jobs. + /// + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task> WatchJobsAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) + { + var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + + callOptions.Headers!.Add("User-Agent", UserAgent().ToString()); + + if (apiTokenHeader is not null) + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + + return callOptions; + } + + /// + /// Returns the value for the User-Agent. + /// + /// A containing the value to use for the User-Agent. + protected static ProductInfoHeaderValue UserAgent() + { + var assembly = typeof(DaprJobsClient).Assembly; + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } +} diff --git a/src/Dapr.Jobs/Models/Responses/JobDetails.cs b/src/Dapr.Jobs/Models/Responses/JobDetails.cs new file mode 100644 index 00000000..195b7f0a --- /dev/null +++ b/src/Dapr.Jobs/Models/Responses/JobDetails.cs @@ -0,0 +1,50 @@ +using Google.Protobuf; + +namespace Dapr.Jobs.Models.Responses; + +/// +/// Represents the details of a retrieved job. +/// +/// The type to deserialize the payload to. +public record JobDetails where T : IMessage +{ + /// + /// A cron-like expression that defines when a job should be triggered. + /// + /// + /// Either this or the property should be specified. + /// + public string? CronExpression { get; init; } + + /// + /// The interval expression that defines when a job should be triggered. + /// + /// + /// Either this or the property should be specified. + /// + public TimeSpan? Interval { get; init; } + + /// + /// Allows for jobs with fixed repeat counts. + /// + public uint? RepeatCount { get; init; } + + /// + /// Identifies a point-in-time representing when the job schedule should start from, + /// or as a "one-shot" time if other scheduling fields are not provided. + /// + public DateTime? DueTime { get; init; } + + /// + /// A point-in-time value representing with the job should expire. + /// + /// + /// This must be greater than if both are set. + /// + public DateTime? TTL { get; init; } + + /// + /// Stores the main payload of the job which is passed to the trigger function. + /// + public T? Payload { get; init; } +} From 6157cd07ad18be7db37e28933bd405f3b1820473 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 09:02:20 -0500 Subject: [PATCH 17/64] Added missing copyright headers Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobClientOptions.cs | 15 ++++++++++++++- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 15 ++++++++++++++- src/Dapr.Jobs/Models/Responses/JobDetails.cs | 15 ++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobClientOptions.cs b/src/Dapr.Jobs/DaprJobClientOptions.cs index e16e20e5..226eaf68 100644 --- a/src/Dapr.Jobs/DaprJobClientOptions.cs +++ b/src/Dapr.Jobs/DaprJobClientOptions.cs @@ -1,4 +1,17 @@ -namespace Dapr.Jobs; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs; /// /// Options used to configure the Dapr job client. diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 1750b662..7761db48 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -1,4 +1,17 @@ -using System.Net.Http.Headers; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; diff --git a/src/Dapr.Jobs/Models/Responses/JobDetails.cs b/src/Dapr.Jobs/Models/Responses/JobDetails.cs index 195b7f0a..0bf59ba9 100644 --- a/src/Dapr.Jobs/Models/Responses/JobDetails.cs +++ b/src/Dapr.Jobs/Models/Responses/JobDetails.cs @@ -1,4 +1,17 @@ -using Google.Protobuf; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Google.Protobuf; namespace Dapr.Jobs.Models.Responses; From 0f0954d47556e4766f3c7889ce611e21e4f48ed4 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 09:16:34 -0500 Subject: [PATCH 18/64] Finished out most of the WatchJobs method. Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 2 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 43 +++++++++++++++++-- .../Models/Responses/WatchedJobDetails.cs | 10 +++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index f7136baf..1e50b672 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -92,7 +92,7 @@ public abstract Task> GetJobAsync(string jobName, CancellationT /// Cancellation token. /// [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task> WatchJobsAsync(CancellationToken cancellationToken = default); + public abstract Task>> WatchJobsAsync(CancellationToken cancellationToken = default) where T : IMessage, new() internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) { diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 7761db48..5951ca7f 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -13,9 +13,11 @@ using System.Net.Http.Headers; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.RegularExpressions; using Dapr.Jobs.Models.Responses; +using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; @@ -326,10 +328,45 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc /// /// Cancellation token. /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task> WatchJobsAsync(CancellationToken cancellationToken = default) + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> WatchJobsAsync( + CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + var jobMetadata = new Autogenerated.JobMetadata + { + Namespace = this.options.Namespace, + AppId = this.options.AppId, + Target = new Autogenerated.JobTargetMetadata { Job = new() } + }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.WatchJobs(callOptions); + + //Run both operations at the same time + var receiveResult = Task.FromResult(RetrieveWatchedJobsAsync(duplexStream, cancellationToken)); + + //TODO Flag whatever is subscribed to this that another job has been invoked + + //TODO Return a response to the server indicating that the job ID has been handled + } + + /// + /// Retrieves the watched jobs from the Dapr scheduler. + /// + /// The type to deserialize the job payload to. + /// The duplex stream to monitor. + /// Cancellation token. + /// + private async IAsyncEnumerable> RetrieveWatchedJobsAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) where T : IMessage, new() + { + await foreach (var watchedJob in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return new WatchedJobDetails(watchedJob.Id, watchedJob.Name, watchedJob.Data.Unpack()); + } } private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) diff --git a/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs b/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs new file mode 100644 index 00000000..07c00d93 --- /dev/null +++ b/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs @@ -0,0 +1,10 @@ +namespace Dapr.Jobs.Models.Responses; + +/// +/// Returns information about a watched job. +/// +/// The type to deserialize the payload to. +/// The identifier of the job itself - once the job is processed, this should be returned back to the server so it can be finalized. +/// The name of the job. +/// The payload data included with the job. +public record WatchedJobDetails(ulong Id, string Name, T Payload); From 2cdd554a7fc22a930d0eba676ce24e6108b177f3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 19:50:29 -0500 Subject: [PATCH 19/64] Adding attribute to mark endpoints for Job trigger invocations Signed-off-by: Whit Waldo --- src/Dapr.Jobs/JobAttribute.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Dapr.Jobs/JobAttribute.cs diff --git a/src/Dapr.Jobs/JobAttribute.cs b/src/Dapr.Jobs/JobAttribute.cs new file mode 100644 index 00000000..b0a7e732 --- /dev/null +++ b/src/Dapr.Jobs/JobAttribute.cs @@ -0,0 +1,22 @@ +namespace Dapr.Jobs; + +/// +/// Describes an endpoint as a subscriber for a job invocation. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class JobAttribute : Attribute +{ + /// + /// The name of the job. + /// + public string JobName { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the job that invokes this method. + public JobAttribute(string jobName) + { + JobName = jobName; + } +} From e713661445250a3663ba35323976d35075092607 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 9 Jul 2024 19:51:37 -0500 Subject: [PATCH 20/64] Partially through bidirectional watcher implementation, but stopping at this point pending further discussion as to intended goals around how this is intended to be utilized (e.g. as opposed to strictly using an HTTP callback) Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 5951ca7f..3d4dc369 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -343,14 +343,26 @@ public override Task>> WatchJobsAsync( var callOptions = CreateCallOptions(headers: null, cancellationToken); var duplexStream = client.WatchJobs(callOptions); + //Leave this stream open - when new jobs come in, retrieve the job information and pass it back to the job invocator + //When that job invocator runs successfully, send back a response that contains the job ID so it can be marked as completed + //Run both operations at the same time var receiveResult = Task.FromResult(RetrieveWatchedJobsAsync(duplexStream, cancellationToken)); + + //TODO Flag whatever is subscribed to this that another job has been invoked //TODO Return a response to the server indicating that the job ID has been handled } + private async Task HandleJobInvocationAsync(WatchedJobDetails details, AsyncDuplexStreamingCall duplexStream, CancellationToken cancellationToken) + { + //TODO Invoke it somehow + await duplexStream.RequestStream.WriteAsync(new Autogenerated.WatchJobsRequestResult { Id = details.Id }, + cancellationToken); + } + /// /// Retrieves the watched jobs from the Dapr scheduler. /// From 49c78eb83e9d15e586d4ed7b2862c1ba9985e6bf Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 10 Jul 2024 10:50:48 -0500 Subject: [PATCH 21/64] Updates to use the correct protos (dapr runtime, not the scheduler service) Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Dapr.Jobs.csproj | 8 +- src/Dapr.Jobs/DaprJobClientBuilder.cs | 6 +- src/Dapr.Jobs/DaprJobsClient.cs | 11 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 149 +- .../DaprJobsServiceCollectionExtensions.cs | 2 +- .../Protos/dapr/proto/common/v1/common.proto | 160 +++ .../dapr/proto/runtime/v1/appcallback.proto | 343 +++++ .../Protos/dapr/proto/runtime/v1/dapr.proto | 1234 +++++++++++++++++ .../dapr/proto/scheduler/v1/scheduler.proto | 160 --- 9 files changed, 1768 insertions(+), 305 deletions(-) create mode 100644 src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto create mode 100644 src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto create mode 100644 src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto delete mode 100644 src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index b95e33ff..2f75deb2 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -20,15 +20,17 @@ - + - + - + + + diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobClientBuilder.cs index 58f3a238..065e2ff2 100644 --- a/src/Dapr.Jobs/DaprJobClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobClientBuilder.cs @@ -13,7 +13,7 @@ using Dapr.Common; using Grpc.Net.Client; -using Autogenerated = Dapr.Scheduler.Autogen.Grpc.v1.Scheduler; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -65,9 +65,9 @@ public override DaprJobsClient Build() httpClient.Timeout = this.Timeout; } - var client = new Autogenerated.SchedulerClient(channel); + var client = new Autogenerated.Dapr.DaprClient(channel); var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); - + return new DaprJobsGrpcClient(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader, this.options); } } diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 1e50b672..73445ee1 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -13,7 +13,6 @@ using System.Text.Json; using Dapr.Jobs.Models.Responses; -using Dapr.Scheduler.Autogen.Grpc.v1; using Google.Protobuf; namespace Dapr.Jobs; @@ -85,15 +84,7 @@ public abstract Task> GetJobAsync(string jobName, CancellationT /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default); - - /// - /// Watches for triggered jobs. - /// - /// Cancellation token. - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task>> WatchJobsAsync(CancellationToken cancellationToken = default) where T : IMessage, new() - + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) { if (string.IsNullOrWhiteSpace(apiToken)) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 3d4dc369..9563d5e0 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -13,15 +13,13 @@ using System.Net.Http.Headers; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.RegularExpressions; using Dapr.Jobs.Models.Responses; -using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; -using Autogenerated = Dapr.Scheduler.Autogen.Grpc.v1; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -36,18 +34,18 @@ internal class DaprJobsGrpcClient : DaprJobsClient private readonly JsonSerializerOptions jsonSerializerOptions; private readonly GrpcChannel channel; - private readonly Autogenerated.Scheduler.SchedulerClient client; + private readonly Autogenerated.Dapr.DaprClient client; private readonly KeyValuePair? apiTokenHeader; private readonly DaprJobClientOptions options; // property exposed for testing purposes - internal Autogenerated.Scheduler.SchedulerClient Client => client; + internal Autogenerated.Dapr.DaprClient Client => client; public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; internal DaprJobsGrpcClient( GrpcChannel channel, - Autogenerated.Scheduler.SchedulerClient innerClient, + Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, Uri httpEndpoint, JsonSerializerOptions jsonSerializerOptions, @@ -83,8 +81,8 @@ public override async Task ScheduleJobAsync(string jobName, string cronExpres throw new ArgumentNullException(nameof(jobName)); if (string.IsNullOrWhiteSpace(cronExpression)) throw new ArgumentNullException(nameof(cronExpression)); - - var job = new Autogenerated.Job { Schedule = cronExpression }; + + var job = new Autogenerated.Job { Name = jobName, Schedule = cronExpression }; if (dueTime is not null) job.DueTime = ((DateTime)dueTime).ToString("O"); @@ -103,24 +101,14 @@ public override async Task ScheduleJobAsync(string jobName, string cronExpres job.Ttl = ((DateTime)ttl).ToString("O"); } - - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata - { - Job = new() - } - }; - - var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; var callOptions = CreateCallOptions(headers: null, cancellationToken); try { - await client.ScheduleJobAsync(envelope, callOptions); + await client.ScheduleJobAlpha1Async(envelope, callOptions); } catch (RpcException ex) { @@ -147,7 +135,7 @@ public override async Task ScheduleJobAsync(string jobName, TimeSpan interval if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); - var job = new Autogenerated.Job { Schedule = interval.ToDurationString() }; + var job = new Autogenerated.Job { Name = jobName, Schedule = interval.ToDurationString() }; if (startingFrom is not null) job.DueTime = ((DateTime)startingFrom).ToString("O"); @@ -166,24 +154,14 @@ public override async Task ScheduleJobAsync(string jobName, TimeSpan interval job.Ttl = ((DateTime)ttl).ToString("O"); } - - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata - { - Job = new() - } - }; - - var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var envelope = new Autogenerated.ScheduleJobRequest { Job = job}; var callOptions = CreateCallOptions(headers: null, cancellationToken); try { - await client.ScheduleJobAsync(envelope, callOptions); + await client.ScheduleJobAlpha1Async(envelope, callOptions); } catch (RpcException ex) { @@ -207,28 +185,18 @@ public override async Task ScheduleJobAsync(string jobName, DateTime schedule if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); - var job = new Autogenerated.Job { DueTime = scheduledTime.ToString("O") }; + var job = new Autogenerated.Job { Name = jobName, DueTime = scheduledTime.ToString("O") }; if (payload is not null) job.Data = Any.Pack(payload); - - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata - { - Job = new() - } - }; - - var envelope = new Autogenerated.ScheduleJobRequest { Name = jobName, Job = job, Metadata = jobMetadata }; + + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; var callOptions = CreateCallOptions(headers: null, cancellationToken); try { - await client.ScheduleJobAsync(envelope, callOptions); + await client.ScheduleJobAlpha1Async(envelope, callOptions); } catch (RpcException ex) { @@ -250,21 +218,14 @@ public override async Task> GetJobAsync(string jobName, Cancell if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata { Job = new() } - }; - - var envelope = new Autogenerated.GetJobRequest { Name = jobName, Metadata = jobMetadata }; + var envelope = new Autogenerated.GetJobRequest { Name = jobName }; var callOptions = CreateCallOptions(headers: null, cancellationToken); Autogenerated.GetJobResponse response; try { - response = await client.GetJobAsync(envelope, callOptions); + response = await client.GetJobAlpha1Async(envelope, callOptions); } catch (RpcException ex) { @@ -297,23 +258,13 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata - { - Job = new() - } - }; - - var envelope = new Autogenerated.DeleteJobRequest { Name = jobName, Metadata = jobMetadata}; + var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; var callOptions = CreateCallOptions(headers: null, cancellationToken); try { - await client.DeleteJobAsync(envelope, callOptions); + await client.DeleteJobAlpha1Async(envelope, callOptions); } catch (RpcException ex) { @@ -323,64 +274,6 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc } } - /// - /// Watches for triggered jobs. - /// - /// Cancellation token. - /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task>> WatchJobsAsync( - CancellationToken cancellationToken = default) - { - var jobMetadata = new Autogenerated.JobMetadata - { - Namespace = this.options.Namespace, - AppId = this.options.AppId, - Target = new Autogenerated.JobTargetMetadata { Job = new() } - }; - - var callOptions = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.WatchJobs(callOptions); - - //Leave this stream open - when new jobs come in, retrieve the job information and pass it back to the job invocator - //When that job invocator runs successfully, send back a response that contains the job ID so it can be marked as completed - - //Run both operations at the same time - var receiveResult = Task.FromResult(RetrieveWatchedJobsAsync(duplexStream, cancellationToken)); - - - - //TODO Flag whatever is subscribed to this that another job has been invoked - - //TODO Return a response to the server indicating that the job ID has been handled - } - - private async Task HandleJobInvocationAsync(WatchedJobDetails details, AsyncDuplexStreamingCall duplexStream, CancellationToken cancellationToken) - { - //TODO Invoke it somehow - await duplexStream.RequestStream.WriteAsync(new Autogenerated.WatchJobsRequestResult { Id = details.Id }, - cancellationToken); - } - - /// - /// Retrieves the watched jobs from the Dapr scheduler. - /// - /// The type to deserialize the job payload to. - /// The duplex stream to monitor. - /// Cancellation token. - /// - private async IAsyncEnumerable> RetrieveWatchedJobsAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) where T : IMessage, new() - { - await foreach (var watchedJob in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return new WatchedJobDetails(watchedJob.Id, watchedJob.Name, watchedJob.Data.Unpack()); - } - } - private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) { var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); diff --git a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs index 8c10ea46..7ddb62b2 100644 --- a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs @@ -39,7 +39,7 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi return builder.Build(); }); - + return serviceCollection; } diff --git a/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto new file mode 100644 index 00000000..1e63b885 --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto @@ -0,0 +1,160 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.common.v1; + +import "google/protobuf/any.proto"; + +option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; +option java_outer_classname = "CommonProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; + +// HTTPExtension includes HTTP verb and querystring +// when Dapr runtime delivers HTTP content. +// +// For example, when callers calls http invoke api +// POST http://localhost:3500/v1.0/invoke//method/?query1=value1&query2=value2 +// +// Dapr runtime will parse POST as a verb and extract querystring to quersytring map. +message HTTPExtension { + // Type of HTTP 1.1 Methods + // RFC 7231: https://tools.ietf.org/html/rfc7231#page-24 + // RFC 5789: https://datatracker.ietf.org/doc/html/rfc5789 + enum Verb { + NONE = 0; + GET = 1; + HEAD = 2; + POST = 3; + PUT = 4; + DELETE = 5; + CONNECT = 6; + OPTIONS = 7; + TRACE = 8; + PATCH = 9; + } + + // Required. HTTP verb. + Verb verb = 1; + + // Optional. querystring represents an encoded HTTP url query string in the following format: name=value&name2=value2 + string querystring = 2; +} + +// InvokeRequest is the message to invoke a method with the data. +// This message is used in InvokeService of Dapr gRPC Service and OnInvoke +// of AppCallback gRPC service. +message InvokeRequest { + // Required. method is a method name which will be invoked by caller. + string method = 1; + + // Required in unary RPCs. Bytes value or Protobuf message which caller sent. + // Dapr treats Any.value as bytes type if Any.type_url is unset. + google.protobuf.Any data = 2; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 3; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + HTTPExtension http_extension = 4; +} + +// InvokeResponse is the response message including data and its content type +// from app callback. +// This message is used in InvokeService of Dapr gRPC Service and OnInvoke +// of AppCallback gRPC service. +message InvokeResponse { + // Required in unary RPCs. The content body of InvokeService response. + google.protobuf.Any data = 1; + + // Required. The type of data content. + string content_type = 2; +} + +// Chunk of data sent in a streaming request or response. +// This is used in requests including InternalInvokeRequestStream. +message StreamPayload { + // Data sent in the chunk. + // The amount of data included in each chunk is up to the discretion of the sender, and can be empty. + // Additionally, the amount of data doesn't need to be fixed and subsequent messages can send more, or less, data. + // Receivers must not make assumptions about the number of bytes they'll receive in each chunk. + bytes data = 1; + + // Sequence number. This is a counter that starts from 0 and increments by 1 on each chunk sent. + uint64 seq = 2; +} + +// StateItem represents state key, value, and additional options to save state. +message StateItem { + // Required. The state key + string key = 1; + + // Required. The state data for key + bytes value = 2; + + // The entity tag which represents the specific version of data. + // The exact ETag format is defined by the corresponding data store. + Etag etag = 3; + + // The metadata which will be passed to state store component. + map metadata = 4; + + // Options for concurrency and consistency to save the state. + StateOptions options = 5; +} + +// Etag represents a state item version +message Etag { + // value sets the etag value + string value = 1; +} + +// StateOptions configures concurrency and consistency for state operations +message StateOptions { + // Enum describing the supported concurrency for state. + enum StateConcurrency { + CONCURRENCY_UNSPECIFIED = 0; + CONCURRENCY_FIRST_WRITE = 1; + CONCURRENCY_LAST_WRITE = 2; + } + + // Enum describing the supported consistency for state. + enum StateConsistency { + CONSISTENCY_UNSPECIFIED = 0; + CONSISTENCY_EVENTUAL = 1; + CONSISTENCY_STRONG = 2; + } + + StateConcurrency concurrency = 1; + StateConsistency consistency = 2; +} + +// ConfigurationItem represents all the configuration with its name(key). +message ConfigurationItem { + // Required. The value of configuration item. + string value = 1; + + // Version is response only and cannot be fetched. Store is not expected to keep all versions available + string version = 2; + + // the metadata which will be passed to/from configuration store component. + map metadata = 3; +} diff --git a/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto new file mode 100644 index 00000000..3e98b536 --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -0,0 +1,343 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.runtime.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "dapr/proto/common/v1/common.proto"; +import "google/protobuf/struct.proto"; + +option csharp_namespace = "Dapr.AppCallback.Autogen.Grpc.v1"; +option java_outer_classname = "DaprAppCallbackProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/runtime/v1;runtime"; + +// AppCallback V1 allows user application to interact with Dapr runtime. +// User application needs to implement AppCallback service if it needs to +// receive message from dapr runtime. +service AppCallback { + // Invokes service method with InvokeRequest. + rpc OnInvoke (common.v1.InvokeRequest) returns (common.v1.InvokeResponse) {} + + // Lists all topics subscribed by this app. + rpc ListTopicSubscriptions(google.protobuf.Empty) returns (ListTopicSubscriptionsResponse) {} + + // Subscribes events from Pubsub + rpc OnTopicEvent(TopicEventRequest) returns (TopicEventResponse) {} + + // Lists all input bindings subscribed by this app. + rpc ListInputBindings(google.protobuf.Empty) returns (ListInputBindingsResponse) {} + + // Listens events from the input bindings + // + // User application can save the states or send the events to the output + // bindings optionally by returning BindingEventResponse. + rpc OnBindingEvent(BindingEventRequest) returns (BindingEventResponse) {} +} + +// AppCallbackHealthCheck V1 is an optional extension to AppCallback V1 to implement +// the HealthCheck method. +service AppCallbackHealthCheck { + // Health check. + rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse) {} +} + +// AppCallbackAlpha V1 is an optional extension to AppCallback V1 to opt +// for Alpha RPCs. +service AppCallbackAlpha { + // Subscribes bulk events from Pubsub + rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} + + // Sends job back to the app's endpoint at trigger time. + rpc OnJobEventAlpha1 (JobEventRequest) returns (JobEventResponse); +} + +message JobEventRequest { + // Job name. + string name = 1; + + // Job data to be sent back to app. + google.protobuf.Any data = 2; + + // Required. method is a method name which will be invoked by caller. + string method = 3; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 4; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + common.v1.HTTPExtension http_extension = 5; +} + +// JobEventResponse is the response from the app when a job is triggered. +message JobEventResponse {} + +// TopicEventRequest message is compatible with CloudEvent spec v1.0 +// https://github.com/cloudevents/spec/blob/v1.0/spec.md +message TopicEventRequest { + // id identifies the event. Producers MUST ensure that source + id + // is unique for each distinct event. If a duplicate event is re-sent + // (e.g. due to a network error) it MAY have the same id. + string id = 1; + + // source identifies the context in which an event happened. + // Often this will include information such as the type of the + // event source, the organization publishing the event or the process + // that produced the event. The exact syntax and semantics behind + // the data encoded in the URI is defined by the event producer. + string source = 2; + + // The type of event related to the originating occurrence. + string type = 3; + + // The version of the CloudEvents specification. + string spec_version = 4; + + // The content type of data value. + string data_content_type = 5; + + // The content of the event. + bytes data = 7; + + // The pubsub topic which publisher sent to. + string topic = 6; + + // The name of the pubsub the publisher sent to. + string pubsub_name = 8; + + // The matching path from TopicSubscription/routes (if specified) for this event. + // This value is used by OnTopicEvent to "switch" inside the handler. + string path = 9; + + // The map of additional custom properties to be sent to the app. These are considered to be cloud event extensions. + google.protobuf.Struct extensions = 10; +} + +// TopicEventResponse is response from app on published message +message TopicEventResponse { + // TopicEventResponseStatus allows apps to have finer control over handling of the message. + enum TopicEventResponseStatus { + // SUCCESS is the default behavior: message is acknowledged and not retried or logged. + SUCCESS = 0; + // RETRY status signals Dapr to retry the message as part of an expected scenario (no warning is logged). + RETRY = 1; + // DROP status signals Dapr to drop the message as part of an unexpected scenario (warning is logged). + DROP = 2; + } + + // The list of output bindings. + TopicEventResponseStatus status = 1; +} + +// TopicEventCERequest message is compatible with CloudEvent spec v1.0 +message TopicEventCERequest { + // The unique identifier of this cloud event. + string id = 1; + + // source identifies the context in which an event happened. + string source = 2; + + // The type of event related to the originating occurrence. + string type = 3; + + // The version of the CloudEvents specification. + string spec_version = 4; + + // The content type of data value. + string data_content_type = 5; + + // The content of the event. + bytes data = 6; + + // Custom attributes which includes cloud event extensions. + google.protobuf.Struct extensions = 7; +} + +// TopicEventBulkRequestEntry represents a single message inside a bulk request +message TopicEventBulkRequestEntry { + // Unique identifier for the message. + string entry_id = 1; + + // The content of the event. + oneof event { + bytes bytes = 2; + TopicEventCERequest cloud_event = 3; + } + + // content type of the event contained. + string content_type = 4; + + // The metadata associated with the event. + map metadata = 5; +} + +// TopicEventBulkRequest represents request for bulk message +message TopicEventBulkRequest { + // Unique identifier for the bulk request. + string id = 1; + + // The list of items inside this bulk request. + repeated TopicEventBulkRequestEntry entries = 2; + + // The metadata associated with the this bulk request. + map metadata = 3; + + // The pubsub topic which publisher sent to. + string topic = 4; + + // The name of the pubsub the publisher sent to. + string pubsub_name = 5; + + // The type of event related to the originating occurrence. + string type = 6; + + // The matching path from TopicSubscription/routes (if specified) for this event. + // This value is used by OnTopicEvent to "switch" inside the handler. + string path = 7; +} + +// TopicEventBulkResponseEntry Represents single response, as part of TopicEventBulkResponse, to be +// sent by subscibed App for the corresponding single message during bulk subscribe +message TopicEventBulkResponseEntry { + // Unique identifier associated the message. + string entry_id = 1; + + // The status of the response. + TopicEventResponse.TopicEventResponseStatus status = 2; +} + +// AppBulkResponse is response from app on published message +message TopicEventBulkResponse { + + // The list of all responses for the bulk request. + repeated TopicEventBulkResponseEntry statuses = 1; +} + +// BindingEventRequest represents input bindings event. +message BindingEventRequest { + // Required. The name of the input binding component. + string name = 1; + + // Required. The payload that the input bindings sent + bytes data = 2; + + // The metadata set by the input binging components. + map metadata = 3; +} + +// BindingEventResponse includes operations to save state or +// send data to output bindings optionally. +message BindingEventResponse { + // The name of state store where states are saved. + string store_name = 1; + + // The state key values which will be stored in store_name. + repeated common.v1.StateItem states = 2; + + // BindingEventConcurrency is the kind of concurrency + enum BindingEventConcurrency { + // SEQUENTIAL sends data to output bindings specified in "to" sequentially. + SEQUENTIAL = 0; + // PARALLEL sends data to output bindings specified in "to" in parallel. + PARALLEL = 1; + } + + // The list of output bindings. + repeated string to = 3; + + // The content which will be sent to "to" output bindings. + bytes data = 4; + + // The concurrency of output bindings to send data to + // "to" output bindings list. The default is SEQUENTIAL. + BindingEventConcurrency concurrency = 5; +} + +// ListTopicSubscriptionsResponse is the message including the list of the subscribing topics. +message ListTopicSubscriptionsResponse { + // The list of topics. + repeated TopicSubscription subscriptions = 1; +} + +// TopicSubscription represents topic and metadata. +message TopicSubscription { + // Required. The name of the pubsub containing the topic below to subscribe to. + string pubsub_name = 1; + + // Required. The name of topic which will be subscribed + string topic = 2; + + // The optional properties used for this topic's subscription e.g. session id + map metadata = 3; + + // The optional routing rules to match against. In the gRPC interface, OnTopicEvent + // is still invoked but the matching path is sent in the TopicEventRequest. + TopicRoutes routes = 5; + + // The optional dead letter queue for this topic to send events to. + string dead_letter_topic = 6; + + // The optional bulk subscribe settings for this topic. + BulkSubscribeConfig bulk_subscribe = 7; +} + +message TopicRoutes { + // The list of rules for this topic. + repeated TopicRule rules = 1; + + // The default path for this topic. + string default = 2; +} + +message TopicRule { + // The optional CEL expression used to match the event. + // If the match is not specified, then the route is considered + // the default. + string match = 1; + + // The path used to identify matches for this subscription. + // This value is passed in TopicEventRequest and used by OnTopicEvent to "switch" + // inside the handler. + string path = 2; +} + +// BulkSubscribeConfig is the message to pass settings for bulk subscribe +message BulkSubscribeConfig { + // Required. Flag to enable/disable bulk subscribe + bool enabled = 1; + + // Optional. Max number of messages to be sent in a single bulk request + int32 max_messages_count = 2; + + // Optional. Max duration to wait for messages to be sent in a single bulk request + int32 max_await_duration_ms = 3; +} + +// ListInputBindingsResponse is the message including the list of input bindings. +message ListInputBindingsResponse { + // The list of input bindings. + repeated string bindings = 1; +} + +// HealthCheckResponse is the message with the response to the health check. +// This message is currently empty as used as placeholder. +message HealthCheckResponse {} diff --git a/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto new file mode 100644 index 00000000..ed4ae6de --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto @@ -0,0 +1,1234 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.runtime.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "dapr/proto/common/v1/common.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; + +option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; +option java_outer_classname = "DaprProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/runtime/v1;runtime"; + +// Dapr service provides APIs to user application to access Dapr building blocks. +service Dapr { + // Invokes a method on a remote Dapr app. + // Deprecated: Use proxy mode service invocation instead. + rpc InvokeService(InvokeServiceRequest) returns (common.v1.InvokeResponse) {} + + // Gets the state for a specific key. + rpc GetState(GetStateRequest) returns (GetStateResponse) {} + + // Gets a bulk of state items for a list of keys + rpc GetBulkState(GetBulkStateRequest) returns (GetBulkStateResponse) {} + + // Saves the state for a specific key. + rpc SaveState(SaveStateRequest) returns (google.protobuf.Empty) {} + + // Queries the state. + rpc QueryStateAlpha1(QueryStateRequest) returns (QueryStateResponse) {} + + // Deletes the state for a specific key. + rpc DeleteState(DeleteStateRequest) returns (google.protobuf.Empty) {} + + // Deletes a bulk of state items for a list of keys + rpc DeleteBulkState(DeleteBulkStateRequest) returns (google.protobuf.Empty) {} + + // Executes transactions for a specified store + rpc ExecuteStateTransaction(ExecuteStateTransactionRequest) returns (google.protobuf.Empty) {} + + // Publishes events to the specific topic. + rpc PublishEvent(PublishEventRequest) returns (google.protobuf.Empty) {} + + // Bulk Publishes multiple events to the specified topic. + rpc BulkPublishEventAlpha1(BulkPublishRequest) returns (BulkPublishResponse) {} + + // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic + // events from it. + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream TopicEventRequest) {} + + // Invokes binding data to specific output bindings + rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} + + // Gets secrets from secret stores. + rpc GetSecret(GetSecretRequest) returns (GetSecretResponse) {} + + // Gets a bulk of secrets + rpc GetBulkSecret(GetBulkSecretRequest) returns (GetBulkSecretResponse) {} + + // Register an actor timer. + rpc RegisterActorTimer(RegisterActorTimerRequest) returns (google.protobuf.Empty) {} + + // Unregister an actor timer. + rpc UnregisterActorTimer(UnregisterActorTimerRequest) returns (google.protobuf.Empty) {} + + // Register an actor reminder. + rpc RegisterActorReminder(RegisterActorReminderRequest) returns (google.protobuf.Empty) {} + + // Unregister an actor reminder. + rpc UnregisterActorReminder(UnregisterActorReminderRequest) returns (google.protobuf.Empty) {} + + // Gets the state for a specific actor. + rpc GetActorState(GetActorStateRequest) returns (GetActorStateResponse) {} + + // Executes state transactions for a specified actor + rpc ExecuteActorStateTransaction(ExecuteActorStateTransactionRequest) returns (google.protobuf.Empty) {} + + // InvokeActor calls a method on an actor. + rpc InvokeActor (InvokeActorRequest) returns (InvokeActorResponse) {} + + // GetConfiguration gets configuration from configuration store. + rpc GetConfigurationAlpha1(GetConfigurationRequest) returns (GetConfigurationResponse) {} + + // GetConfiguration gets configuration from configuration store. + rpc GetConfiguration(GetConfigurationRequest) returns (GetConfigurationResponse) {} + + // SubscribeConfiguration gets configuration from configuration store and subscribe the updates event by grpc stream + rpc SubscribeConfigurationAlpha1(SubscribeConfigurationRequest) returns (stream SubscribeConfigurationResponse) {} + + // SubscribeConfiguration gets configuration from configuration store and subscribe the updates event by grpc stream + rpc SubscribeConfiguration(SubscribeConfigurationRequest) returns (stream SubscribeConfigurationResponse) {} + + // UnSubscribeConfiguration unsubscribe the subscription of configuration + rpc UnsubscribeConfigurationAlpha1(UnsubscribeConfigurationRequest) returns (UnsubscribeConfigurationResponse) {} + + // UnSubscribeConfiguration unsubscribe the subscription of configuration + rpc UnsubscribeConfiguration(UnsubscribeConfigurationRequest) returns (UnsubscribeConfigurationResponse) {} + + // TryLockAlpha1 tries to get a lock with an expiry. + rpc TryLockAlpha1(TryLockRequest)returns (TryLockResponse) {} + + // UnlockAlpha1 unlocks a lock. + rpc UnlockAlpha1(UnlockRequest)returns (UnlockResponse) {} + + // EncryptAlpha1 encrypts a message using the Dapr encryption scheme and a key stored in the vault. + rpc EncryptAlpha1(stream EncryptRequest) returns (stream EncryptResponse); + + // DecryptAlpha1 decrypts a message using the Dapr encryption scheme and a key stored in the vault. + rpc DecryptAlpha1(stream DecryptRequest) returns (stream DecryptResponse); + + // Gets metadata of the sidecar + rpc GetMetadata (GetMetadataRequest) returns (GetMetadataResponse) {} + + // Sets value in extended metadata of the sidecar + rpc SetMetadata (SetMetadataRequest) returns (google.protobuf.Empty) {} + + // SubtleGetKeyAlpha1 returns the public part of an asymmetric key stored in the vault. + rpc SubtleGetKeyAlpha1(SubtleGetKeyRequest) returns (SubtleGetKeyResponse); + + // SubtleEncryptAlpha1 encrypts a small message using a key stored in the vault. + rpc SubtleEncryptAlpha1(SubtleEncryptRequest) returns (SubtleEncryptResponse); + + // SubtleDecryptAlpha1 decrypts a small message using a key stored in the vault. + rpc SubtleDecryptAlpha1(SubtleDecryptRequest) returns (SubtleDecryptResponse); + + // SubtleWrapKeyAlpha1 wraps a key using a key stored in the vault. + rpc SubtleWrapKeyAlpha1(SubtleWrapKeyRequest) returns (SubtleWrapKeyResponse); + + // SubtleUnwrapKeyAlpha1 unwraps a key using a key stored in the vault. + rpc SubtleUnwrapKeyAlpha1(SubtleUnwrapKeyRequest) returns (SubtleUnwrapKeyResponse); + + // SubtleSignAlpha1 signs a message using a key stored in the vault. + rpc SubtleSignAlpha1(SubtleSignRequest) returns (SubtleSignResponse); + + // SubtleVerifyAlpha1 verifies the signature of a message using a key stored in the vault. + rpc SubtleVerifyAlpha1(SubtleVerifyRequest) returns (SubtleVerifyResponse); + + // Starts a new instance of a workflow + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + + // Starts a new instance of a workflow + rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowBeta1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowBeta1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowBeta1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowBeta1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowBeta1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Shutdown the sidecar + rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} + + // Create and schedule a job + rpc ScheduleJobAlpha1(ScheduleJobRequest) returns (ScheduleJobResponse) {} + + // Gets a scheduled job + rpc GetJobAlpha1(GetJobRequest) returns (GetJobResponse) {} + + // Delete a job + rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} +} + +// InvokeServiceRequest represents the request message for Service invocation. +message InvokeServiceRequest { + // Required. Callee's app id. + string id = 1; + + // Required. message which will be delivered to callee. + common.v1.InvokeRequest message = 3; +} + +// GetStateRequest is the message to get key-value states from specific state store. +message GetStateRequest { + // The name of state store. + string store_name = 1; + + // The key of the desired state + string key = 2; + + // The read consistency of the state store. + common.v1.StateOptions.StateConsistency consistency = 3; + + // The metadata which will be sent to state store components. + map metadata = 4; +} + +// GetBulkStateRequest is the message to get a list of key-value states from specific state store. +message GetBulkStateRequest { + // The name of state store. + string store_name = 1; + + // The keys to get. + repeated string keys = 2; + + // The number of parallel operations executed on the state store for a get operation. + int32 parallelism = 3; + + // The metadata which will be sent to state store components. + map metadata = 4; +} + +// GetBulkStateResponse is the response conveying the list of state values. +message GetBulkStateResponse { + // The list of items containing the keys to get values for. + repeated BulkStateItem items = 1; +} + +// BulkStateItem is the response item for a bulk get operation. +// Return values include the item key, data and etag. +message BulkStateItem { + // state item key + string key = 1; + + // The byte array data + bytes data = 2; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 3; + + // The error that was returned from the state store in case of a failed get operation. + string error = 4; + + // The metadata which will be sent to app. + map metadata = 5; +} + +// GetStateResponse is the response conveying the state value and etag. +message GetStateResponse { + // The byte array data + bytes data = 1; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 2; + + // The metadata which will be sent to app. + map metadata = 3; +} + +// DeleteStateRequest is the message to delete key-value states in the specific state store. +message DeleteStateRequest { + // The name of state store. + string store_name = 1; + + // The key of the desired state + string key = 2; + + // The entity tag which represents the specific version of data. + // The exact ETag format is defined by the corresponding data store. + common.v1.Etag etag = 3; + + // State operation options which includes concurrency/ + // consistency/retry_policy. + common.v1.StateOptions options = 4; + + // The metadata which will be sent to state store components. + map metadata = 5; +} + +// DeleteBulkStateRequest is the message to delete a list of key-value states from specific state store. +message DeleteBulkStateRequest { + // The name of state store. + string store_name = 1; + + // The array of the state key values. + repeated common.v1.StateItem states = 2; +} + +// SaveStateRequest is the message to save multiple states into state store. +message SaveStateRequest { + // The name of state store. + string store_name = 1; + + // The array of the state key values. + repeated common.v1.StateItem states = 2; +} + +// QueryStateRequest is the message to query state store. +message QueryStateRequest { + // The name of state store. + string store_name = 1 [json_name = "storeName"]; + + // The query in JSON format. + string query = 2; + + // The metadata which will be sent to state store components. + map metadata = 3; +} + +message QueryStateItem { + // The object key. + string key = 1; + + // The object value. + bytes data = 2; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 3; + + // The error message indicating an error in processing of the query result. + string error = 4; +} + +// QueryStateResponse is the response conveying the query results. +message QueryStateResponse { + // An array of query results. + repeated QueryStateItem results = 1; + + // Pagination token. + string token = 2; + + // The metadata which will be sent to app. + map metadata = 3; +} + +// PublishEventRequest is the message to publish event data to pubsub topic +message PublishEventRequest { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The data which will be published to topic. + bytes data = 3; + + // The content type for the data (optional). + string data_content_type = 4; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 5; +} + +// BulkPublishRequest is the message to bulk publish events to pubsub topic +message BulkPublishRequest { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The entries which contain the individual events and associated details to be published + repeated BulkPublishRequestEntry entries = 3; + + // The request level metadata passing to to the pubsub components + map metadata = 4; +} + +// BulkPublishRequestEntry is the message containing the event to be bulk published +message BulkPublishRequestEntry { + // The request scoped unique ID referring to this message. Used to map status in response + string entry_id = 1; + + // The event which will be pulished to the topic + bytes event = 2; + + // The content type for the event + string content_type = 3; + + // The event level metadata passing to the pubsub component + map metadata = 4; +} + +// BulkPublishResponse is the message returned from a BulkPublishEvent call +message BulkPublishResponse { + // The entries for different events that failed publish in the BulkPublishEvent call + repeated BulkPublishResponseFailedEntry failedEntries = 1; +} + +// BulkPublishResponseFailedEntry is the message containing the entryID and error of a failed event in BulkPublishEvent call +message BulkPublishResponseFailedEntry { + // The response scoped unique ID referring to this message + string entry_id = 1; + + // The error message if any on failure + string error = 2; +} + +// SubscribeTopicEventsRequestAlpha1 is a message containing the details for +// subscribing to a topic via streaming. +// The first message must always be the initial request. All subsequent +// messages must be event responses. +message SubscribeTopicEventsRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeTopicEventsInitialRequestAlpha1 initial_request = 1; + SubscribeTopicEventsResponseAlpha1 event_response = 2; + } +} + +// SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the +// details for subscribing to a topic via streaming. +message SubscribeTopicEventsInitialRequestAlpha1 { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 3; + + // dead_letter_topic is the topic to which messages that fail to be processed + // are sent. + optional string dead_letter_topic = 4; +} + +// SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +// subscription to a topic. +message SubscribeTopicEventsResponseAlpha1 { + // id is the unique identifier for the subscription request. + string id = 1; + + // status is the result of the subscription request. + TopicEventResponse status = 2; +} + +// InvokeBindingRequest is the message to send data to output bindings +message InvokeBindingRequest { + // The name of the output binding to invoke. + string name = 1; + + // The data which will be sent to output binding. + bytes data = 2; + + // The metadata passing to output binding components + // + // Common metadata property: + // - ttlInSeconds : the time to live in seconds for the message. + // If set in the binding definition will cause all messages to + // have a default time to live. The message ttl overrides any value + // in the binding definition. + map metadata = 3; + + // The name of the operation type for the binding to invoke + string operation = 4; +} + +// InvokeBindingResponse is the message returned from an output binding invocation +message InvokeBindingResponse { + // The data which will be sent to output binding. + bytes data = 1; + + // The metadata returned from an external system + map metadata = 2; +} + +// GetSecretRequest is the message to get secret from secret store. +message GetSecretRequest { + // The name of secret store. + string store_name = 1 [json_name = "storeName"]; + + // The name of secret key. + string key = 2; + + // The metadata which will be sent to secret store components. + map metadata = 3; +} + +// GetSecretResponse is the response message to convey the requested secret. +message GetSecretResponse { + // data is the secret value. Some secret store, such as kubernetes secret + // store, can save multiple secrets for single secret key. + map data = 1; +} + +// GetBulkSecretRequest is the message to get the secrets from secret store. +message GetBulkSecretRequest { + // The name of secret store. + string store_name = 1 [json_name = "storeName"]; + + // The metadata which will be sent to secret store components. + map metadata = 2; +} + +// SecretResponse is a map of decrypted string/string values +message SecretResponse { + map secrets = 1; +} + +// GetBulkSecretResponse is the response message to convey the requested secrets. +message GetBulkSecretResponse { + // data hold the secret values. Some secret store, such as kubernetes secret + // store, can save multiple secrets for single secret key. + map data = 1; +} + +// TransactionalStateOperation is the message to execute a specified operation with a key-value pair. +message TransactionalStateOperation { + // The type of operation to be executed + string operationType = 1; + + // State values to be operated on + common.v1.StateItem request = 2; +} + +// ExecuteStateTransactionRequest is the message to execute multiple operations on a specified store. +message ExecuteStateTransactionRequest { + // Required. name of state store. + string storeName = 1; + + // Required. transactional operation list. + repeated TransactionalStateOperation operations = 2; + + // The metadata used for transactional operations. + map metadata = 3; +} + +// RegisterActorTimerRequest is the message to register a timer for an actor of a given type and id. +message RegisterActorTimerRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; + string due_time = 4 [json_name = "dueTime"]; + string period = 5; + string callback = 6; + bytes data = 7; + string ttl = 8; +} + +// UnregisterActorTimerRequest is the message to unregister an actor timer +message UnregisterActorTimerRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; +} + +// RegisterActorReminderRequest is the message to register a reminder for an actor of a given type and id. +message RegisterActorReminderRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; + string due_time = 4 [json_name = "dueTime"]; + string period = 5; + bytes data = 6; + string ttl = 7; +} + +// UnregisterActorReminderRequest is the message to unregister an actor reminder. +message UnregisterActorReminderRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; +} + +// GetActorStateRequest is the message to get key-value states from specific actor. +message GetActorStateRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string key = 3; +} + +// GetActorStateResponse is the response conveying the actor's state value. +message GetActorStateResponse { + bytes data = 1; + + // The metadata which will be sent to app. + map metadata = 2; +} + +// ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. +message ExecuteActorStateTransactionRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + repeated TransactionalActorStateOperation operations = 3; +} + +// TransactionalActorStateOperation is the message to execute a specified operation with a key-value pair. +message TransactionalActorStateOperation { + string operationType = 1; + string key = 2; + google.protobuf.Any value = 3; + // The metadata used for transactional operations. + // + // Common metadata property: + // - ttlInSeconds : the time to live in seconds for the stored value. + map metadata = 4; +} + +// InvokeActorRequest is the message to call an actor. +message InvokeActorRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string method = 3; + bytes data = 4; + map metadata = 5; +} + +// InvokeActorResponse is the method that returns an actor invocation response. +message InvokeActorResponse { + bytes data = 1; +} + +// GetMetadataRequest is the message for the GetMetadata request. +message GetMetadataRequest { + // Empty +} + +// GetMetadataResponse is a message that is returned on GetMetadata rpc call. +message GetMetadataResponse { + string id = 1; + // Deprecated alias for actor_runtime.active_actors. + repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors", deprecated = true]; + repeated RegisteredComponents registered_components = 3 [json_name = "components"]; + map extended_metadata = 4 [json_name = "extended"]; + repeated PubsubSubscription subscriptions = 5 [json_name = "subscriptions"]; + repeated MetadataHTTPEndpoint http_endpoints = 6 [json_name = "httpEndpoints"]; + AppConnectionProperties app_connection_properties = 7 [json_name = "appConnectionProperties"]; + string runtime_version = 8 [json_name = "runtimeVersion"]; + repeated string enabled_features = 9 [json_name = "enabledFeatures"]; + ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; + //TODO: Cassie: probably add scheduler runtime status +} + +message ActorRuntime { + enum ActorRuntimeStatus { + // Indicates that the actor runtime is still being initialized. + INITIALIZING = 0; + // Indicates that the actor runtime is disabled. + // This normally happens when Dapr is started without "placement-host-address" + DISABLED = 1; + // Indicates the actor runtime is running, either as an actor host or client. + RUNNING = 2; + } + + // Contains an enum indicating whether the actor runtime has been initialized. + ActorRuntimeStatus runtime_status = 1 [json_name = "runtimeStatus"]; + // Count of active actors per type. + repeated ActiveActorsCount active_actors = 2 [json_name = "activeActors"]; + // Indicates whether the actor runtime is ready to host actors. + bool host_ready = 3 [json_name = "hostReady"]; + // Custom message from the placement provider. + string placement = 4 [json_name = "placement"]; +} + +message ActiveActorsCount { + string type = 1; + int32 count = 2; +} + +message RegisteredComponents { + string name = 1; + string type = 2; + string version = 3; + repeated string capabilities = 4; +} + +message MetadataHTTPEndpoint { + string name = 1 [json_name = "name"]; +} + +message AppConnectionProperties { + int32 port = 1; + string protocol = 2; + string channel_address = 3 [json_name = "channelAddress"]; + int32 max_concurrency = 4 [json_name = "maxConcurrency"]; + AppConnectionHealthProperties health = 5; +} + +message AppConnectionHealthProperties { + string health_check_path = 1 [json_name = "healthCheckPath"]; + string health_probe_interval = 2 [json_name = "healthProbeInterval"]; + string health_probe_timeout = 3 [json_name = "healthProbeTimeout"]; + int32 health_threshold = 4 [json_name = "healthThreshold"]; +} + +message PubsubSubscription { + string pubsub_name = 1 [json_name = "pubsubname"]; + string topic = 2 [json_name = "topic"]; + map metadata = 3 [json_name = "metadata"]; + PubsubSubscriptionRules rules = 4 [json_name = "rules"]; + string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; + PubsubSubscriptionType type = 6 [json_name = "type"]; +} + +// PubsubSubscriptionType indicates the type of subscription +enum PubsubSubscriptionType { + // UNKNOWN is the default value for the subscription type. + UNKNOWN = 0; + // Declarative subscription (k8s CRD) + DECLARATIVE = 1; + // Programmatically created subscription + PROGRAMMATIC = 2; + // Bidirectional Streaming subscription + STREAMING = 3; +} + +message PubsubSubscriptionRules { + repeated PubsubSubscriptionRule rules = 1; +} + +message PubsubSubscriptionRule { + string match = 1; + string path = 2; +} + +message SetMetadataRequest { + string key = 1; + string value = 2; +} + +// GetConfigurationRequest is the message to get a list of key-value configuration from specified configuration store. +message GetConfigurationRequest { + // Required. The name of configuration store. + string store_name = 1; + + // Optional. The key of the configuration item to fetch. + // If set, only query for the specified configuration items. + // Empty list means fetch all. + repeated string keys = 2; + + // Optional. The metadata which will be sent to configuration store components. + map metadata = 3; +} + +// GetConfigurationResponse is the response conveying the list of configuration values. +// It should be the FULL configuration of specified application which contains all of its configuration items. +message GetConfigurationResponse { + map items = 1; +} + +// SubscribeConfigurationRequest is the message to get a list of key-value configuration from specified configuration store. +message SubscribeConfigurationRequest { + // The name of configuration store. + string store_name = 1; + + // Optional. The key of the configuration item to fetch. + // If set, only query for the specified configuration items. + // Empty list means fetch all. + repeated string keys = 2; + + // The metadata which will be sent to configuration store components. + map metadata = 3; +} + +// UnSubscribeConfigurationRequest is the message to stop watching the key-value configuration. +message UnsubscribeConfigurationRequest { + // The name of configuration store. + string store_name = 1; + + // The id to unsubscribe. + string id = 2; +} + +message SubscribeConfigurationResponse { + // Subscribe id, used to stop subscription. + string id = 1; + + // The list of items containing configuration values + map items = 2; +} + +message UnsubscribeConfigurationResponse { + bool ok = 1; + string message = 2; +} + +message TryLockRequest { + // Required. The lock store name,e.g. `redis`. + string store_name = 1 [json_name = "storeName"]; + + // Required. resource_id is the lock key. e.g. `order_id_111` + // It stands for "which resource I want to protect" + string resource_id = 2 [json_name = "resourceId"]; + + // Required. lock_owner indicate the identifier of lock owner. + // You can generate a uuid as lock_owner.For example,in golang: + // + // req.LockOwner = uuid.New().String() + // + // This field is per request,not per process,so it is different for each request, + // which aims to prevent multi-thread in the same process trying the same lock concurrently. + // + // The reason why we don't make it automatically generated is: + // 1. If it is automatically generated,there must be a 'my_lock_owner_id' field in the response. + // This name is so weird that we think it is inappropriate to put it into the api spec + // 2. If we change the field 'my_lock_owner_id' in the response to 'lock_owner',which means the current lock owner of this lock, + // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. + // 3. When reentrant lock is needed,the existing lock_owner is required to identify client and check "whether this client can reenter this lock". + // So this field in the request shouldn't be removed. + string lock_owner = 3 [json_name = "lockOwner"]; + + // Required. The time before expiry.The time unit is second. + int32 expiry_in_seconds = 4 [json_name = "expiryInSeconds"]; +} + +message TryLockResponse { + bool success = 1; +} + +message UnlockRequest { + string store_name = 1 [json_name = "storeName"]; + // resource_id is the lock key. + string resource_id = 2 [json_name = "resourceId"]; + string lock_owner = 3 [json_name = "lockOwner"]; +} + +message UnlockResponse { + enum Status { + SUCCESS = 0; + LOCK_DOES_NOT_EXIST = 1; + LOCK_BELONGS_TO_OTHERS = 2; + INTERNAL_ERROR = 3; + } + + Status status = 1; +} + +// SubtleGetKeyRequest is the request object for SubtleGetKeyAlpha1. +message SubtleGetKeyRequest { + enum KeyFormat { + // PEM (PKIX) (default) + PEM = 0; + // JSON (JSON Web Key) as string + JSON = 1; + } + + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key to use in the key vault + string name = 2; + // Response format + KeyFormat format = 3; +} + +// SubtleGetKeyResponse is the response for SubtleGetKeyAlpha1. +message SubtleGetKeyResponse { + // Name (or name/version) of the key. + // This is returned as response too in case there is a version. + string name = 1; + // Public key, encoded in the requested format + string public_key = 2 [json_name="publicKey"]; +} + +// SubtleEncryptRequest is the request for SubtleEncryptAlpha1. +message SubtleEncryptRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Message to encrypt. + bytes plaintext = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 6 [json_name="associatedData"]; +} + +// SubtleEncryptResponse is the response for SubtleEncryptAlpha1. +message SubtleEncryptResponse { + // Encrypted ciphertext. + bytes ciphertext = 1; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 2; +} + +// SubtleDecryptRequest is the request for SubtleDecryptAlpha1. +message SubtleDecryptRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Message to decrypt. + bytes ciphertext = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 6; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 7 [json_name="associatedData"]; +} + +// SubtleDecryptResponse is the response for SubtleDecryptAlpha1. +message SubtleDecryptResponse { + // Decrypted plaintext. + bytes plaintext = 1; +} + +// SubtleWrapKeyRequest is the request for SubtleWrapKeyAlpha1. +message SubtleWrapKeyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Key to wrap + bytes plaintext_key = 2 [json_name="plaintextKey"]; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 6 [json_name="associatedData"]; +} + +// SubtleWrapKeyResponse is the response for SubtleWrapKeyAlpha1. +message SubtleWrapKeyResponse { + // Wrapped key. + bytes wrapped_key = 1 [json_name="wrappedKey"]; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 2; +} + +// SubtleUnwrapKeyRequest is the request for SubtleUnwrapKeyAlpha1. +message SubtleUnwrapKeyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Wrapped key. + bytes wrapped_key = 2 [json_name="wrappedKey"]; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 6; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 7 [json_name="associatedData"]; +} + +// SubtleUnwrapKeyResponse is the response for SubtleUnwrapKeyAlpha1. +message SubtleUnwrapKeyResponse { + // Key in plaintext + bytes plaintext_key = 1 [json_name="plaintextKey"]; +} + +// SubtleSignRequest is the request for SubtleSignAlpha1. +message SubtleSignRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Digest to sign. + bytes digest = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; +} + +// SubtleSignResponse is the response for SubtleSignAlpha1. +message SubtleSignResponse { + // The signature that was computed + bytes signature = 1; +} + +// SubtleVerifyRequest is the request for SubtleVerifyAlpha1. +message SubtleVerifyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Digest of the message. + bytes digest = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Signature to verify. + bytes signature = 5; +} + +// SubtleVerifyResponse is the response for SubtleVerifyAlpha1. +message SubtleVerifyResponse { + // True if the signature is valid. + bool valid = 1; +} + +// EncryptRequest is the request for EncryptAlpha1. +message EncryptRequest { + // Request details. Must be present in the first message only. + EncryptRequestOptions options = 1; + // Chunk of data of arbitrary size. + common.v1.StreamPayload payload = 2; +} + +// EncryptRequestOptions contains options for the first message in the EncryptAlpha1 request. +message EncryptRequestOptions { + // Name of the component. Required. + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key. Required. + string key_name = 2 [json_name="keyName"]; + // Key wrapping algorithm to use. Required. + // Supported options include: A256KW (alias: AES), A128CBC, A192CBC, A256CBC, RSA-OAEP-256 (alias: RSA). + string key_wrap_algorithm = 3; + // Cipher used to encrypt data (optional): "aes-gcm" (default) or "chacha20-poly1305" + string data_encryption_cipher = 10; + // If true, the encrypted document does not contain a key reference. + // In that case, calls to the Decrypt method must provide a key reference (name or name/version). + // Defaults to false. + bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; + // Key reference to embed in the encrypted document (name or name/version). + // This is helpful if the reference of the key used to decrypt the document is different from the one used to encrypt it. + // If unset, uses the reference of the key used to encrypt the document (this is the default behavior). + // This option is ignored if omit_decryption_key_name is true. + string decryption_key_name = 12 [json_name="decryptionKeyName"]; +} + +// EncryptResponse is the response for EncryptAlpha1. +message EncryptResponse { + // Chunk of data. + common.v1.StreamPayload payload = 1; +} + +// DecryptRequest is the request for DecryptAlpha1. +message DecryptRequest { + // Request details. Must be present in the first message only. + DecryptRequestOptions options = 1; + // Chunk of data of arbitrary size. + common.v1.StreamPayload payload = 2; +} + +// DecryptRequestOptions contains options for the first message in the DecryptAlpha1 request. +message DecryptRequestOptions { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key to decrypt the message. + // Overrides any key reference included in the message if present. + // This is required if the message doesn't include a key reference (i.e. was created with omit_decryption_key_name set to true). + string key_name = 12 [json_name="keyName"]; +} + +// DecryptResponse is the response for DecryptAlpha1. +message DecryptResponse { + // Chunk of data. + common.v1.StreamPayload payload = 1; +} + +// GetWorkflowRequest is the request for GetWorkflowBeta1. +message GetWorkflowRequest { + // ID of the workflow instance to query. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// GetWorkflowResponse is the response for GetWorkflowBeta1. +message GetWorkflowResponse { + // ID of the workflow instance. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow. + string workflow_name = 2 [json_name = "workflowName"]; + // The time at which the workflow instance was created. + google.protobuf.Timestamp created_at = 3 [json_name = "createdAt"]; + // The last time at which the workflow instance had its state changed. + google.protobuf.Timestamp last_updated_at = 4 [json_name = "lastUpdatedAt"]; + // The current status of the workflow instance, for example, "PENDING", "RUNNING", "SUSPENDED", "COMPLETED", "FAILED", and "TERMINATED". + string runtime_status = 5 [json_name = "runtimeStatus"]; + // Additional component-specific properties of the workflow instance. + map properties = 6; +} + +// StartWorkflowRequest is the request for StartWorkflowBeta1. +message StartWorkflowRequest { + // The ID to assign to the started workflow instance. If empty, a random ID is generated. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; + // Name of the workflow. + string workflow_name = 3 [json_name = "workflowName"]; + // Additional component-specific options for starting the workflow instance. + map options = 4; + // Input data for the workflow instance. + bytes input = 5; +} + +// StartWorkflowResponse is the response for StartWorkflowBeta1. +message StartWorkflowResponse { + // ID of the started workflow instance. + string instance_id = 1 [json_name = "instanceID"]; +} + +// TerminateWorkflowRequest is the request for TerminateWorkflowBeta1. +message TerminateWorkflowRequest { + // ID of the workflow instance to terminate. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// PauseWorkflowRequest is the request for PauseWorkflowBeta1. +message PauseWorkflowRequest { + // ID of the workflow instance to pause. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// ResumeWorkflowRequest is the request for ResumeWorkflowBeta1. +message ResumeWorkflowRequest { + // ID of the workflow instance to resume. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowBeta1. +message RaiseEventWorkflowRequest { + // ID of the workflow instance to raise an event for. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; + // Name of the event. + string event_name = 3 [json_name = "eventName"]; + // Data associated with the event. + bytes event_data = 4; +} + +// PurgeWorkflowRequest is the request for PurgeWorkflowBeta1. +message PurgeWorkflowRequest { + // ID of the workflow instance to purge. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// ShutdownRequest is the request for Shutdown. +message ShutdownRequest { + // Empty +} + +// Job is the definition of a job. +message Job { + // The unique name for the job. + string name = 1; + + // The schedule for the job. + optional string schedule = 2; + + // Optional: jobs with fixed repeat counts (accounting for Actor Reminders). + optional uint32 repeats = 3; + + // Optional: sets time at which or time interval before the callback is invoked for the first time. + optional string due_time = 4; + + // Optional: Time To Live to allow for auto deletes (accounting for Actor Reminders). + optional string ttl = 5; + + // Job data. + google.protobuf.Any data = 6; +} + +// ScheduleJobRequest is the message to create/schedule the job. +message ScheduleJobRequest { + // The job details. + Job job = 1; +} + +// ScheduleJobResponse is the message response to create/schedule the job. +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message to retrieve a job. +message GetJobRequest { + // The name of the job. + string name = 1; +} + +// GetJobResponse is the message's response for a job retrieved. +message GetJobResponse { + // The job details. + Job job = 1; +} + +// DeleteJobRequest is the message to delete the job by name. +message DeleteJobRequest { + // The name of the job. + string name = 1; +} + +// DeleteJobResponse is the message response to delete the job by name. +message DeleteJobResponse { + // Empty +} \ No newline at end of file diff --git a/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto b/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto deleted file mode 100644 index aa3978f1..00000000 --- a/src/Dapr.Jobs/Protos/dapr/proto/scheduler/v1/scheduler.proto +++ /dev/null @@ -1,160 +0,0 @@ -syntax = "proto3"; - -package dapr.proto.scheduler.v1; - -import "google/protobuf/any.proto"; - -option csharp_namespace = "Dapr.Scheduler.Autogen.Grpc.v1"; -option go_package = "github.com/dapr/dapr/pkg/proto/scheduler/v1;scheduler"; - -service Scheduler { - // ScheduleJob is used by the daprd sidecar to schedule a job. - rpc ScheduleJob(ScheduleJobRequest) returns (ScheduleJobResponse) {} - // Get a job - rpc GetJob(GetJobRequest) returns (GetJobResponse) {} - // DeleteJob is used by the daprd sidecar to delete a job. - rpc DeleteJob(DeleteJobRequest) returns (DeleteJobResponse) {} - // WatchJobs is used by the daprd sidecar to connect to the Scheduler - // service to watch for jobs triggering back. - rpc WatchJobs(stream WatchJobsRequest) returns (stream WatchJobsResponse) {} -} - -message Job { - // The schedule for the job. - optional string schedule = 1; - - // Optional: jobs with fixed repeat counts (accounting for Actor Reminders). - optional uint32 repeats = 2; - - // Optional: sets time at which or time interval before the callback is invoked for the first time. - optional string due_time = 3; - - // Optional: Time To Live to allow for auto deletes (accounting for Actor Reminders). - optional string ttl = 4; - - // Job data. - google.protobuf.Any data = 5; -} - -// TargetJob is the message used by the daprd sidecar to schedule a job -// from an App. -message TargetJob {} - -// TargetActorReminder is the message used by the daprd sidecar to -// schedule a job from an Actor Reminder. -message TargetActorReminder { - // id is the actor ID. - string id = 1; - - // type is the actor type. - string type = 2; -} - -// JobTargetMetadata holds the typed metadata associated with the job for -// different origins. -message JobTargetMetadata { - oneof type { - TargetJob job = 1; - TargetActorReminder actor = 2; - } -} - -// JobMetadata is the message used by the daprd sidecar to schedule/get/delete a -// job. -message JobMetadata { - // app_id is the App ID of the requester. - string app_id = 1; - - // namespace is the namespace of the requester. - string namespace = 2; - - // target is the type of the job. - JobTargetMetadata target = 3; -} - -// WatchJobsRequest is the message used by the daprd sidecar to connect to the -// Scheduler and send Job process results. -message WatchJobsRequest { - oneof watch_job_request_type { - WatchJobsRequestInitial initial = 1; - WatchJobsRequestResult result = 2; - } -} - -// WatchJobsRequestInitial is the initial request to start watching for jobs. -message WatchJobsRequestInitial { - // app_id is the App ID of the requester. - string app_id = 1; - - // namespace is the namespace of the requester. - string namespace = 2; - - // actor_types is the optional list of actor types to watch for. - repeated string actor_types = 3; -} - -// WatchJobsRequestResult is the result of a job execution to allow the job to -// be marked as processed. -message WatchJobsRequestResult { - // id is the id of the job that has finished processing, used as an incremental counter. - uint64 id = 1; -} - -// WatchJobsResponse is the response message to convey the details of a job. -message WatchJobsResponse { - // name is the name of the job which was triggered. - string name = 1; - - // id is the id of the job trigger event which should be sent back from - // the client to be marked as processed, used as an incremental counter. - uint64 id = 2; - - // Job data. - google.protobuf.Any data = 3; - - // The metadata associated with the job. - JobMetadata metadata = 4; -} - -message ScheduleJobRequest { - // name is the name of the job to create. - string name = 1; - - // The job to be scheduled. - Job job = 2; - - // The metadata associated with the job. - JobMetadata metadata = 3; -} - -message ScheduleJobResponse { - // Empty -} - -// GetJobRequest is the message used by the daprd sidecar to delete or get a job. -message GetJobRequest { - // name is the name of the job. - string name = 1; - - // The metadata associated with the job. - JobMetadata metadata = 2; -} - -// GetJobResponse is the response message to convey the details of a job. -message GetJobResponse { - // The job to be scheduled. - Job job = 1; -} - -// DeleteJobRequest is the message used by the daprd sidecar to delete or get a job. -message DeleteJobRequest { - string name = 1; - - // The metadata associated with the job. - JobMetadata metadata = 2; -} - -// DeleteJobRequest is the response message used by the daprd sidecar to delete or get a job. -message DeleteJobResponse { - // Empty -} From e76b32302d9ece92a9620199e2dd2afb36f9dd7f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 10 Jul 2024 11:33:06 -0500 Subject: [PATCH 22/64] Added default null values Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 4 ++-- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 73445ee1..9e9cb344 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -38,7 +38,7 @@ public abstract class DaprJobsClient /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime, + public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; /// @@ -52,7 +52,7 @@ public abstract Task ScheduleJobAsync(string jobName, string cronExpression, /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom, + public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 9563d5e0..dee43a17 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -74,7 +74,7 @@ internal DaprJobsGrpcClient( /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime, uint? repeats = null, + public override async Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default { if (string.IsNullOrWhiteSpace(jobName)) @@ -129,7 +129,7 @@ public override async Task ScheduleJobAsync(string jobName, string cronExpres /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom, uint? repeats = null, + public override async Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default { if (string.IsNullOrWhiteSpace(jobName)) From 79e56d6235348a09b3429e500ad9faf58217df85 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 10 Jul 2024 11:40:39 -0500 Subject: [PATCH 23/64] Added schedule job overloads for when the developer doesn't want to submit job data Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 40 ++++++++++++++++++++++++- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 45 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 9e9cb344..44ef4465 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -41,6 +41,20 @@ public abstract class DaprJobsClient public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; + /// + /// Schedules a recurring job using a cron expression. + /// + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); + /// /// Schedules a recurring job with an optional future starting date. /// @@ -56,6 +70,20 @@ public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, Date uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, + uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); + /// /// Schedules a one-time job. /// @@ -67,6 +95,16 @@ public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, Date public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; + /// + /// Schedules a one-time job. + /// + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, + CancellationToken cancellationToken = default); + /// /// Retrieves the details of a registered job. /// @@ -76,7 +114,7 @@ public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task> GetJobAsync(string jobName, CancellationToken cancellationToken = default) where T : IMessage, new(); - + /// /// Deletes the specified job. /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index dee43a17..69ef94d8 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -16,6 +16,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using Dapr.Jobs.Models.Responses; +using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; @@ -118,6 +119,22 @@ public override async Task ScheduleJobAsync(string jobName, string cronExpres } } + /// + /// Schedules a recurring job using a cron expression. + /// + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, + DateTime? ttl = null, CancellationToken cancellationToken = default) + { + return ScheduleJobAsync(jobName, cronExpression, dueTime, repeats, ttl, null, cancellationToken); + } + /// /// Schedules a recurring job with an optional future starting date. /// @@ -171,6 +188,22 @@ public override async Task ScheduleJobAsync(string jobName, TimeSpan interval } } + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, + DateTime? ttl = null, CancellationToken cancellationToken = default) + { + return ScheduleJobAsync(jobName, interval, startingFrom, repeats, ttl, null, cancellationToken); + } + /// /// Schedules a one-time job. /// @@ -206,6 +239,18 @@ public override async Task ScheduleJobAsync(string jobName, DateTime schedule } } + /// + /// Schedules a one-time job. + /// + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task ScheduleJobAsync(string jobName, DateTime scheduledTime, CancellationToken cancellationToken = default) + { + return ScheduleJobAsync(jobName, scheduledTime, null, cancellationToken); + } + /// /// Retrieves the details of a registered job. /// From 9723ff55430bd1a84561b25f6cea3feab15adda4 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 10 Jul 2024 13:35:38 -0500 Subject: [PATCH 24/64] Rather than 6 overloads of the same method, changed the names of each pair to match their purpose Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 17 +++++++++-------- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 18 +++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 44ef4465..3631abf9 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -38,9 +38,10 @@ public abstract class DaprJobsClient /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, - uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; - + public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) + where T : IMessage; + /// /// Schedules a recurring job using a cron expression. /// @@ -52,7 +53,7 @@ public abstract Task ScheduleJobAsync(string jobName, string cronExpression, /// Cancellation token. [Obsolete( "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); /// @@ -66,7 +67,7 @@ public abstract Task ScheduleJobAsync(string jobName, string cronExpression, Dat /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, + public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; @@ -81,7 +82,7 @@ public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, Date /// Cancellation token. [Obsolete( "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, + public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); /// @@ -92,7 +93,7 @@ public abstract Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTim /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, T? payload = default, + public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, T? payload = default, CancellationToken cancellationToken = default) where T : IMessage; /// @@ -102,7 +103,7 @@ public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, /// The point in time when the job should be run. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleJobAsync(string jobName, DateTime scheduledTime, + public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, CancellationToken cancellationToken = default); /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 69ef94d8..e4078f66 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -75,7 +75,7 @@ internal DaprJobsGrpcClient( /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, + public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default { if (string.IsNullOrWhiteSpace(jobName)) @@ -129,10 +129,10 @@ public override async Task ScheduleJobAsync(string jobName, string cronExpres /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, + public override Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default) { - return ScheduleJobAsync(jobName, cronExpression, dueTime, repeats, ttl, null, cancellationToken); + return ScheduleCronJobAsync(jobName, cronExpression, dueTime, repeats, ttl, null, cancellationToken); } /// @@ -146,7 +146,7 @@ public override Task ScheduleJobAsync(string jobName, string cronExpression, Dat /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, + public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default { if (string.IsNullOrWhiteSpace(jobName)) @@ -198,10 +198,10 @@ public override async Task ScheduleJobAsync(string jobName, TimeSpan interval /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, + public override Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default) { - return ScheduleJobAsync(jobName, interval, startingFrom, repeats, ttl, null, cancellationToken); + return ScheduleIntervalJobAsync(jobName, interval, startingFrom, repeats, ttl, null, cancellationToken); } /// @@ -212,7 +212,7 @@ public override Task ScheduleJobAsync(string jobName, TimeSpan interval, DateTim /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleJobAsync(string jobName, DateTime scheduledTime, T? payload = default, + public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, T? payload = default, CancellationToken cancellationToken = default) where T : default { if (string.IsNullOrWhiteSpace(jobName)) @@ -246,9 +246,9 @@ public override async Task ScheduleJobAsync(string jobName, DateTime schedule /// The point in time when the job should be run. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleJobAsync(string jobName, DateTime scheduledTime, CancellationToken cancellationToken = default) + public override Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, CancellationToken cancellationToken = default) { - return ScheduleJobAsync(jobName, scheduledTime, null, cancellationToken); + return ScheduleOneTimeJobAsync(jobName, scheduledTime, null, cancellationToken); } /// From 6bd99c52aa97fe802b3fb1a96dcfad4318cfe9bb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 10 Jul 2024 13:40:26 -0500 Subject: [PATCH 25/64] Solution update Signed-off-by: Whit Waldo --- all.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/all.sln b/all.sln index 43f5257c..d3c368c9 100644 --- a/all.sln +++ b/all.sln @@ -122,7 +122,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs", "src\Dapr.Jobs\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{EDEE625E-6815-40E1-935F-35129771A0F8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{3E075F71-185E-4C09-9449-79D21A958487}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{3E075F71-185E-4C09-9449-79D21A958487}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 63df29d43e19bfb90a9d65bc0079489aee780061 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 11 Jul 2024 04:41:46 -0500 Subject: [PATCH 26/64] Removed serialization from SDK - swapping to only accepting and returning a byte[] as the job payload and trying it out for size Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 52 ++------------ src/Dapr.Jobs/DaprJobsGrpcClient.cs | 76 +++++--------------- src/Dapr.Jobs/Models/Responses/JobDetails.cs | 17 ++--- 3 files changed, 29 insertions(+), 116 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 3631abf9..e39a1370 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -13,7 +13,6 @@ using System.Text.Json; using Dapr.Jobs.Models.Responses; -using Google.Protobuf; namespace Dapr.Jobs; @@ -38,23 +37,9 @@ public abstract class DaprJobsClient /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, - uint? repeats = null, DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) - where T : IMessage; - - /// - /// Schedules a recurring job using a cron expression. - /// - /// The name of the job being scheduled. - /// The systemd Cron-like expression indicating when the job should be triggered. - /// The optional point-in-time from which the job schedule should start. - /// The optional number of times the job should be triggered. - /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. - /// Cancellation token. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, - uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); + uint? repeats = null, DateTime? ttl = null, byte[]? payload = null, + CancellationToken cancellationToken = default); /// /// Schedules a recurring job with an optional future starting date. @@ -67,23 +52,9 @@ public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, - uint? repeats = null, DateTime? ttl = null, T? payload = default, - CancellationToken cancellationToken = default) where T : IMessage; - - /// - /// Schedules a recurring job with an optional future starting date. - /// - /// The name of the job being scheduled. - /// The interval at which the job should be triggered. - /// The optional point-in-time from which the job schedule should start. - /// The optional maximum number of times the job should be triggered. - /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. - /// Cancellation token. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, - uint? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default); + uint? repeats = null, DateTime? ttl = null, byte[]? payload = null, + CancellationToken cancellationToken = default); /// /// Schedules a one-time job. @@ -93,17 +64,7 @@ public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, T? payload = default, - CancellationToken cancellationToken = default) where T : IMessage; - - /// - /// Schedules a one-time job. - /// - /// The name of the job being scheduled. - /// The point in time when the job should be run. - /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, + public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, byte[]? payload = null, CancellationToken cancellationToken = default); /// @@ -113,8 +74,7 @@ public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledT /// Cancellation token. /// The details comprising the job. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task> GetJobAsync(string jobName, CancellationToken cancellationToken = default) - where T : IMessage, new(); + public abstract Task GetJobAsync(string jobName, CancellationToken cancellationToken = default); /// /// Deletes the specified job. diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index e4078f66..e96ef5a0 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -27,7 +27,7 @@ namespace Dapr.Jobs; /// /// A client for interacting with the Dapr endpoints. /// -internal class DaprJobsGrpcClient : DaprJobsClient +internal sealed class DaprJobsGrpcClient : DaprJobsClient { private readonly Uri httpEndpoint; private readonly HttpClient httpClient; @@ -75,8 +75,8 @@ internal DaprJobsGrpcClient( /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, - DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default + public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, + DateTime? ttl = null, byte[]? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); @@ -92,7 +92,7 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronEx job.Repeats = (uint)repeats; if (payload is not null) - job.Data = Any.Pack(payload); + job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; if (ttl is not null) { @@ -119,22 +119,6 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronEx } } - /// - /// Schedules a recurring job using a cron expression. - /// - /// The name of the job being scheduled. - /// The systemd Cron-like expression indicating when the job should be triggered. - /// The optional point-in-time from which the job schedule should start. - /// The optional number of times the job should be triggered. - /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. - /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, - DateTime? ttl = null, CancellationToken cancellationToken = default) - { - return ScheduleCronJobAsync(jobName, cronExpression, dueTime, repeats, ttl, null, cancellationToken); - } - /// /// Schedules a recurring job with an optional future starting date. /// @@ -146,8 +130,8 @@ public override Task ScheduleCronJobAsync(string jobName, string cronExpression, /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, - DateTime? ttl = null, T? payload = default, CancellationToken cancellationToken = default) where T : default + public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, + DateTime? ttl = null, byte[]? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); @@ -161,7 +145,7 @@ public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan job.Repeats = (uint)repeats; if (payload is not null) - job.Data = Any.Pack(payload); + job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; if (ttl is not null) { @@ -188,22 +172,6 @@ public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan } } - /// - /// Schedules a recurring job with an optional future starting date. - /// - /// The name of the job being scheduled. - /// The interval at which the job should be triggered. - /// The optional point-in-time from which the job schedule should start. - /// The optional maximum number of times the job should be triggered. - /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. - /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, - DateTime? ttl = null, CancellationToken cancellationToken = default) - { - return ScheduleIntervalJobAsync(jobName, interval, startingFrom, repeats, ttl, null, cancellationToken); - } - /// /// Schedules a one-time job. /// @@ -212,17 +180,17 @@ public override Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, T? payload = default, - CancellationToken cancellationToken = default) where T : default + public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, byte[]? payload = null, + CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); var job = new Autogenerated.Job { Name = jobName, DueTime = scheduledTime.ToString("O") }; - - if (payload is not null) - job.Data = Any.Pack(payload); + if (payload is not null) + job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; var callOptions = CreateCallOptions(headers: null, cancellationToken); @@ -239,18 +207,6 @@ public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime s } } - /// - /// Schedules a one-time job. - /// - /// The name of the job being scheduled. - /// The point in time when the job should be run. - /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, CancellationToken cancellationToken = default) - { - return ScheduleOneTimeJobAsync(jobName, scheduledTime, null, cancellationToken); - } - /// /// Retrieves the details of a registered job. /// @@ -258,7 +214,7 @@ public override Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledT /// Cancellation token. /// The details comprising the job. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> GetJobAsync(string jobName, CancellationToken cancellationToken = default) + public override async Task GetJobAsync(string jobName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); @@ -280,14 +236,14 @@ public override async Task> GetJobAsync(string jobName, Cancell var intervalRegex = new Regex("h|m|(ms)|s"); - return new JobDetails + return new JobDetails { DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, TTL = response.Job.Ttl is not null ? DateTime.Parse(response.Job.Ttl) : null, Interval = response.Job.Schedule is not null && intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule.FromDurationString() : null, CronExpression = response.Job.Schedule is null || !intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule : null, RepeatCount = response.Job.Repeats == default ? null : response.Job.Repeats, - Payload = response.Job.Data.Unpack() + Payload = response.Job.Data.ToByteArray() }; } @@ -335,7 +291,7 @@ private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cance /// Returns the value for the User-Agent. /// /// A containing the value to use for the User-Agent. - protected static ProductInfoHeaderValue UserAgent() + private static ProductInfoHeaderValue UserAgent() { var assembly = typeof(DaprJobsClient).Assembly; var assemblyVersion = assembly diff --git a/src/Dapr.Jobs/Models/Responses/JobDetails.cs b/src/Dapr.Jobs/Models/Responses/JobDetails.cs index 0bf59ba9..a8978007 100644 --- a/src/Dapr.Jobs/Models/Responses/JobDetails.cs +++ b/src/Dapr.Jobs/Models/Responses/JobDetails.cs @@ -11,15 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Google.Protobuf; - namespace Dapr.Jobs.Models.Responses; /// /// Represents the details of a retrieved job. /// -/// The type to deserialize the payload to. -public record JobDetails where T : IMessage +public record JobDetails { /// /// A cron-like expression that defines when a job should be triggered. @@ -27,7 +24,7 @@ public record JobDetails where T : IMessage /// /// Either this or the property should be specified. /// - public string? CronExpression { get; init; } + public string? CronExpression { get; init; } = null; /// /// The interval expression that defines when a job should be triggered. @@ -35,18 +32,18 @@ public record JobDetails where T : IMessage /// /// Either this or the property should be specified. /// - public TimeSpan? Interval { get; init; } + public TimeSpan? Interval { get; init; } = null; /// /// Allows for jobs with fixed repeat counts. /// - public uint? RepeatCount { get; init; } + public uint? RepeatCount { get; init; } = null; /// /// Identifies a point-in-time representing when the job schedule should start from, /// or as a "one-shot" time if other scheduling fields are not provided. /// - public DateTime? DueTime { get; init; } + public DateTime? DueTime { get; init; } = null; /// /// A point-in-time value representing with the job should expire. @@ -54,10 +51,10 @@ public record JobDetails where T : IMessage /// /// This must be greater than if both are set. /// - public DateTime? TTL { get; init; } + public DateTime? TTL { get; init; } = null; /// /// Stores the main payload of the job which is passed to the trigger function. /// - public T? Payload { get; init; } + public byte[]? Payload { get; init; } = null; } From d6cbf513993ae0d3a18686014b3d1feed8ff01e8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 12 Jul 2024 07:54:08 -0500 Subject: [PATCH 27/64] Update to use ReadOnlyMemory instead of byte[] Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 6 +++--- src/Dapr.Jobs/Models/Responses/JobDetails.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index e39a1370..1747d3a4 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -38,7 +38,7 @@ public abstract class DaprJobsClient /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, - uint? repeats = null, DateTime? ttl = null, byte[]? payload = null, + uint? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); /// @@ -53,7 +53,7 @@ public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, - uint? repeats = null, DateTime? ttl = null, byte[]? payload = null, + uint? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); /// @@ -64,7 +64,7 @@ public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, byte[]? payload = null, + public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); /// diff --git a/src/Dapr.Jobs/Models/Responses/JobDetails.cs b/src/Dapr.Jobs/Models/Responses/JobDetails.cs index a8978007..1af70023 100644 --- a/src/Dapr.Jobs/Models/Responses/JobDetails.cs +++ b/src/Dapr.Jobs/Models/Responses/JobDetails.cs @@ -56,5 +56,5 @@ public record JobDetails /// /// Stores the main payload of the job which is passed to the trigger function. /// - public byte[]? Payload { get; init; } = null; + public ReadOnlyMemory? Payload { get; init; } = null; } From bcdf08bb5776a79c520ed727cf61f1e8784309ca Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 12 Jul 2024 07:55:15 -0500 Subject: [PATCH 28/64] Refactored the generic client builder so that less identical code needs to be included in each of the override package-specific client builders Signed-off-by: Whit Waldo --- src/Dapr.Client/DaprClientBuilder.cs | 34 +-- src/Dapr.Common/DaprGenericClientBuilder.cs | 315 +++++++++++--------- src/Dapr.Jobs/DaprJobClientBuilder.cs | 32 +- 3 files changed, 185 insertions(+), 196 deletions(-) diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index b600b4dc..0dce2f44 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -11,10 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Net.Http; using Dapr.Common; -using Grpc.Net.Client; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Client; @@ -29,34 +26,11 @@ public sealed class DaprClientBuilder : DaprGenericClientBuilder /// public override DaprClient Build() { - var grpcEndpoint = new Uri(this.GrpcEndpoint); - if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The gRPC endpoint must use http or https."); - } + var daprClientDependencies = this.BuildDaprClientDependencies(); - if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) - { - // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - var httpEndpoint = new Uri(this.HttpEndpoint); - if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The HTTP endpoint must use http or https."); - } - - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - - var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); - if (this.Timeout > TimeSpan.Zero) - { - httpClient.Timeout = this.Timeout; - } - - var client = new Autogenerated.Dapr.DaprClient(channel); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); - return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); + return new DaprClientGrpc(daprClientDependencies.channel, client, daprClientDependencies.httpClient, + daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 8a2b81be..2abbc06b 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -11,165 +11,204 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Common -{ - using System; - using System.Net.Http; - using System.Text.Json; - using Grpc.Net.Client; +namespace Dapr.Common; + +using System; +using System.Net.Http; +using System.Text.Json; +using Grpc.Net.Client; +/// +/// Builder for building a generic Dapr client. +/// +public abstract class DaprGenericClientBuilder where TClientBuilder : class +{ /// - /// Builder for building a generic Dapr client. + /// Initializes a new instance of the class. /// - public abstract class DaprGenericClientBuilder where TClientBuilder : class + public DaprGenericClientBuilder() { - /// - /// Initializes a new instance of the class. - /// - public DaprGenericClientBuilder() - { - this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); - - this.GrpcChannelOptions = new GrpcChannelOptions() - { - // The gRPC client doesn't throw the right exception for cancellation - // by default, this switches that behavior on. - ThrowOperationCanceledOnCancellation = true, - }; - - this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - } + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + + this.GrpcChannelOptions = new GrpcChannelOptions() + { + // The gRPC client doesn't throw the right exception for cancellation + // by default, this switches that behavior on. + ThrowOperationCanceledOnCancellation = true, + }; + + this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + } - /// - /// Property exposed for testing purposes. - /// - public string GrpcEndpoint { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public string GrpcEndpoint { get; private set; } - /// - /// Property exposed for testing purposes. - /// - public string HttpEndpoint { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public string HttpEndpoint { get; private set; } - /// - /// Property exposed for testing purposes. - /// - public Func? HttpClientFactory { get; set; } - - /// - /// Property exposed for testing purposes. - /// - public JsonSerializerOptions JsonSerializerOptions { get; private set; } - - /// - /// Property exposed for testing purposes. - /// - public GrpcChannelOptions GrpcChannelOptions { get; private set; } - /// - /// Property exposed for testing purposes. - /// - public string DaprApiToken { get; private set; } - /// - /// Property exposed for testing purposes. - /// - public TimeSpan Timeout { get; private set; } - - /// - /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback - /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the - /// corresponding environment variables. - /// - /// The instance. - public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); - this.HttpEndpoint = httpEndpoint; - return this; - } + /// + /// Property exposed for testing purposes. + /// + public Func? HttpClientFactory { get; set; } - /// - /// Exposed internally for testing purposes. - /// - internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) - { - this.HttpClientFactory = factory; - return this; - } + /// + /// Property exposed for testing purposes. + /// + public JsonSerializerOptions JsonSerializerOptions { get; private set; } - /// - /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the - /// DAPR_GRPC_PORT environment variable. - /// - /// The instance. - public DaprGenericClientBuilder UseGrpcEndpoint(string grpcEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); - this.GrpcEndpoint = grpcEndpoint; - return this; - } + /// + /// Property exposed for testing purposes. + /// + public GrpcChannelOptions GrpcChannelOptions { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public string DaprApiToken { get; private set; } + /// + /// Property exposed for testing purposes. + /// + public TimeSpan Timeout { get; private set; } + + /// + /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// The instance. + public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); + this.HttpEndpoint = httpEndpoint; + return this; + } + + /// + /// Exposed internally for testing purposes. + /// + internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) + { + this.HttpClientFactory = factory; + return this; + } + + /// + /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// The instance. + public DaprGenericClientBuilder UseGrpcEndpoint(string grpcEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); + this.GrpcEndpoint = grpcEndpoint; + return this; + } + + /// + /// + /// Uses the specified when serializing or deserializing using . + /// + /// + /// The default value is created using . + /// + /// + /// Json serialization options. + /// The instance. + public DaprGenericClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + { + this.JsonSerializerOptions = options; + return this; + } - /// - /// - /// Uses the specified when serializing or deserializing using . - /// - /// - /// The default value is created using . - /// - /// - /// Json serialization options. - /// The instance. - public DaprGenericClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + /// The instance. + public DaprGenericClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + return this; + } + + /// + /// Adds the provided on every request to the Dapr runtime. + /// + /// The token to be added to the request headers/>. + /// The instance. + public DaprGenericClientBuilder UseDaprApiToken(string apiToken) + { + this.DaprApiToken = apiToken; + return this; + } + + /// + /// Sets the timeout for the HTTP client used by the Dapr client. + /// + /// + /// + public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + + /// + /// Builds out the inner DaprClient that provides the core shape of the + /// runtime gRPC client used by the consuming package. + /// + /// A DaprClient instance. + /// + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + { + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") { - this.JsonSerializerOptions = options; - return this; + throw new InvalidOperationException("The gRPC endpoint must use http or https."); } - /// - /// Uses the provided for creating the . - /// - /// The to use for creating the . - /// The instance. - public DaprGenericClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) { - this.GrpcChannelOptions = grpcChannelOptions; - return this; + // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - /// - /// Adds the provided on every request to the Dapr runtime. - /// - /// The token to be added to the request headers/>. - /// The instance. - public DaprGenericClientBuilder UseDaprApiToken(string apiToken) + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { - this.DaprApiToken = apiToken; - return this; + throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - /// - /// Sets the timeout for the HTTP client used by the Dapr client. - /// - /// - /// - public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + + var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + if (this.Timeout > TimeSpan.Zero) { - this.Timeout = timeout; - return this; + httpClient.Timeout = this.Timeout; } - /// - /// Builds the client instance from the properties of the builder. - /// - /// The Dapr client instance. - public abstract TClientBuilder Build(); + return (channel, httpClient, httpEndpoint); } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public abstract TClientBuilder Build(); } diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobClientBuilder.cs index 065e2ff2..45795cdc 100644 --- a/src/Dapr.Jobs/DaprJobClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobClientBuilder.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using Dapr.Common; -using Grpc.Net.Client; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -39,35 +38,12 @@ public DaprJobClientBuilder(DaprJobClientOptions options) /// The Dapr client instance. public override DaprJobsClient Build() { - var grpcEndpoint = new Uri(this.GrpcEndpoint); - if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The gRPC endpoint must use http or https."); - } + var daprClientDependencies = this.BuildDaprClientDependencies(); - if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) - { - // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - var httpEndpoint = new Uri(this.HttpEndpoint); - if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The HTTP endpoint must use http or https."); - } - - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - - var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); - if (this.Timeout > TimeSpan.Zero) - { - httpClient.Timeout = this.Timeout; - } - - var client = new Autogenerated.Dapr.DaprClient(channel); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); - return new DaprJobsGrpcClient(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader, this.options); + return new DaprJobsGrpcClient(daprClientDependencies.channel, client, daprClientDependencies.httpClient, + daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader, this.options); } } From 597ac0da0c943ec1efbb7f3dbda1a32dd70fc1bf Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 12 Jul 2024 07:56:02 -0500 Subject: [PATCH 29/64] Updating constructor to use protected instead of public Signed-off-by: Whit Waldo --- src/Dapr.Common/DaprGenericClientBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 2abbc06b..59ec4977 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -26,7 +26,7 @@ public abstract class DaprGenericClientBuilder where TClientBuil /// /// Initializes a new instance of the class. /// - public DaprGenericClientBuilder() + protected DaprGenericClientBuilder() { this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); From c08376a44710c32e308f09eaa5d23e4a7cd145a7 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 12 Jul 2024 07:59:05 -0500 Subject: [PATCH 30/64] Neglected to update DaprJobsGrpcClient to reflect signature changes. Opted for implementation that eliminates one allocation (using .Span on ReadOnlyMemory<>) Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index e96ef5a0..fb05b34b 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -76,7 +76,7 @@ internal DaprJobsGrpcClient( /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, - DateTime? ttl = null, byte[]? payload = null, CancellationToken cancellationToken = default) + DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); @@ -92,7 +92,7 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre job.Repeats = (uint)repeats; if (payload is not null) - job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; + job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; if (ttl is not null) { @@ -131,7 +131,7 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, - DateTime? ttl = null, byte[]? payload = null, CancellationToken cancellationToken = default) + DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) throw new ArgumentNullException(nameof(jobName)); @@ -145,7 +145,7 @@ public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan int job.Repeats = (uint)repeats; if (payload is not null) - job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; + job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; if (ttl is not null) { @@ -180,7 +180,7 @@ public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan int /// Stores the main payload of the job which is passed to the trigger function. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, byte[]? payload = null, + public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledTime, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) @@ -189,7 +189,7 @@ public override async Task ScheduleOneTimeJobAsync(string jobName, DateTime sche var job = new Autogenerated.Job { Name = jobName, DueTime = scheduledTime.ToString("O") }; if (payload is not null) - job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload), TypeUrl = "dapr.io/schedule/jobpayload" }; + job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; From ff3cf9c4ed711ce7a861e5d0940d19ac091361b6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 05:43:50 -0500 Subject: [PATCH 31/64] Removed DaprJobClientOptions as no longer needed since we're using the appropriate gRPC API now Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobClientBuilder.cs | 13 +-------- src/Dapr.Jobs/DaprJobClientOptions.cs | 41 --------------------------- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 5 +--- 3 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 src/Dapr.Jobs/DaprJobClientOptions.cs diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobClientBuilder.cs index 45795cdc..a50bf6b1 100644 --- a/src/Dapr.Jobs/DaprJobClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobClientBuilder.cs @@ -21,17 +21,6 @@ namespace Dapr.Jobs; /// public sealed class DaprJobClientBuilder : DaprGenericClientBuilder { - private readonly DaprJobClientOptions options; - - /// - /// Used to construct a new instance of . - /// - /// - public DaprJobClientBuilder(DaprJobClientOptions options) - { - this.options = options; - } - /// /// Builds the client instance from the properties of the builder. /// @@ -44,6 +33,6 @@ public override DaprJobsClient Build() var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); return new DaprJobsGrpcClient(daprClientDependencies.channel, client, daprClientDependencies.httpClient, - daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader, this.options); + daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/src/Dapr.Jobs/DaprJobClientOptions.cs b/src/Dapr.Jobs/DaprJobClientOptions.cs deleted file mode 100644 index 226eaf68..00000000 --- a/src/Dapr.Jobs/DaprJobClientOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2024 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -namespace Dapr.Jobs; - -/// -/// Options used to configure the Dapr job client. -/// -public class DaprJobClientOptions -{ - /// - /// Initializes a new instance of . - /// - /// The ID of the app . - /// The namespace of the app. - public DaprJobClientOptions(string appId, string appNamespace) - { - AppId = appId; - Namespace = appNamespace; - } - - /// - /// The App ID of the requester. - /// - public string AppId { get; init; } - - /// - /// The namespace of the requester. - /// - public string Namespace { get; init; } -} diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index fb05b34b..0bd37fa3 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -37,7 +37,6 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient private readonly GrpcChannel channel; private readonly Autogenerated.Dapr.DaprClient client; private readonly KeyValuePair? apiTokenHeader; - private readonly DaprJobClientOptions options; // property exposed for testing purposes internal Autogenerated.Dapr.DaprClient Client => client; @@ -50,8 +49,7 @@ internal DaprJobsGrpcClient( HttpClient httpClient, Uri httpEndpoint, JsonSerializerOptions jsonSerializerOptions, - KeyValuePair? apiTokenHeader, - DaprJobClientOptions options) + KeyValuePair? apiTokenHeader) { this.channel = channel; this.client = innerClient; @@ -59,7 +57,6 @@ internal DaprJobsGrpcClient( this.httpEndpoint = httpEndpoint; this.jsonSerializerOptions = jsonSerializerOptions; this.apiTokenHeader = apiTokenHeader; - this.options = options; this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); } From 3e1b0f3f41dd9c6a80fed3f669a81b83d20551d9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 05:44:14 -0500 Subject: [PATCH 32/64] Consolidated all the extensions Signed-off-by: Whit Waldo --- .../DaprJobsServiceCollectionExtensions.cs | 17 +++++-------- .../EndpointRouteBuilderExtensions.cs | 24 +++++++++++++++++++ .../{ => Extensions}/TimeSpanExtensions.cs | 0 3 files changed, 30 insertions(+), 11 deletions(-) rename src/Dapr.Jobs/{ => Extensions}/DaprJobsServiceCollectionExtensions.cs (73%) create mode 100644 src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs rename src/Dapr.Jobs/{ => Extensions}/TimeSpanExtensions.cs (100%) diff --git a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs similarity index 73% rename from src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs rename to src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 7ddb62b2..fceacfb8 100644 --- a/src/Dapr.Jobs/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Dapr.Jobs; +namespace Dapr.Jobs.Extensions; /// /// Contains extension methods for using Dapr Jobs with dependency injection. @@ -24,17 +24,14 @@ public static class DaprJobsServiceCollectionExtensions /// /// Adds Dapr Jobs client support to the service collection. /// - /// The . - /// The options used to configure the . - /// Optionally allows greater configuration of the . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, DaprJobClientOptions options, Action? configure = null) + /// The . Optionally allows greater configuration of the . + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); serviceCollection.TryAddSingleton(_ => { - var builder = new DaprJobClientBuilder(options); + var builder = new DaprJobClientBuilder(); configure?.Invoke(builder); return builder.Build(); @@ -47,17 +44,15 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi /// Adds Dapr Jobs client support to the service collection. /// /// The . - /// The options used to configure the . /// Optionally allows greater configuration of the using injected services. /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, DaprJobClientOptions options, - Action? configure = null) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); serviceCollection.TryAddSingleton(serviceProvider => { - var builder = new DaprJobClientBuilder(options); + var builder = new DaprJobClientBuilder(); configure?.Invoke(serviceProvider, builder); return builder.Build(); diff --git a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..7cefdd4f --- /dev/null +++ b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Dapr.Jobs.Extensions; + +/// +/// Provides extension methods to register endpoints for Dapr Job Scheduler invocations. +/// +public static class EndpointRouteBuilderExtensions +{ + /// + /// Adds a to the that registers a + /// scheduled job trigger invocation. + /// + /// The to add the route to. + /// The name of the job that should trigger this method when invoked. + /// The delegate executed when the endpoint is matched. + /// + public static IEndpointConventionBuilder MapScheduledJob(this IEndpointRouteBuilder endpoints, string jobName, + Delegate handler) + { + return endpoints.MapPost($"/job/{jobName}", handler); + } +} diff --git a/src/Dapr.Jobs/TimeSpanExtensions.cs b/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs similarity index 100% rename from src/Dapr.Jobs/TimeSpanExtensions.cs rename to src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs From e44dca0ae939b30da99d6038a84498128c05e192 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 05:44:43 -0500 Subject: [PATCH 33/64] Renamed to ScheduledJobAttribute to match the API name Signed-off-by: Whit Waldo --- src/Dapr.Jobs/{JobAttribute.cs => ScheduledJobAttribute.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/Dapr.Jobs/{JobAttribute.cs => ScheduledJobAttribute.cs} (70%) diff --git a/src/Dapr.Jobs/JobAttribute.cs b/src/Dapr.Jobs/ScheduledJobAttribute.cs similarity index 70% rename from src/Dapr.Jobs/JobAttribute.cs rename to src/Dapr.Jobs/ScheduledJobAttribute.cs index b0a7e732..f6fef027 100644 --- a/src/Dapr.Jobs/JobAttribute.cs +++ b/src/Dapr.Jobs/ScheduledJobAttribute.cs @@ -4,7 +4,7 @@ /// Describes an endpoint as a subscriber for a job invocation. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public sealed class JobAttribute : Attribute +public sealed class ScheduledJobAttribute : Attribute { /// /// The name of the job. @@ -12,10 +12,10 @@ public sealed class JobAttribute : Attribute public string JobName { get; init; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the job that invokes this method. - public JobAttribute(string jobName) + public ScheduledJobAttribute(string jobName) { JobName = jobName; } From 5b7600dbc32d124a49358e80b95f4b498f8952b0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 05:45:31 -0500 Subject: [PATCH 34/64] Adding ASP.NET Core to package to add route builder extension support Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Dapr.Jobs.csproj | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index 2f75deb2..dd9145c6 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -1,15 +1,19 @@  - - net6;net8 - enable - enable - Dapr.Jobs - Dapr Jobs Authoring SDK - Dapr Jobs SDK for scheduling jobs and tasks with Dapr - 0.1.0 - alpha - + + net6;net8 + enable + enable + Dapr.Jobs + Dapr Jobs Authoring SDK + Dapr Jobs SDK for scheduling jobs and tasks with Dapr + 0.1.0 + alpha + + + + + @@ -20,11 +24,11 @@ - + - + @@ -33,4 +37,4 @@ - + \ No newline at end of file From c15fa6602b6146a3ba85508fa29851eb54a6eb75 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 05:59:43 -0500 Subject: [PATCH 35/64] Added another registration overload so the developer isn't forced to specify either of the action methods just to discard them. Signed-off-by: Whit Waldo --- .../DaprJobsServiceCollectionExtensions.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index fceacfb8..727320b9 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -24,7 +24,26 @@ public static class DaprJobsServiceCollectionExtensions /// /// Adds Dapr Jobs client support to the service collection. /// - /// The . Optionally allows greater configuration of the . + /// The . + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + serviceCollection.TryAddSingleton(_ => + { + var builder = new DaprJobClientBuilder(); + return builder.Build(); + }); + + return serviceCollection; + } + + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the . public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); From a29a4ddc16cc41377ece4f6d4a6a6272fba69140 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 06:57:15 -0500 Subject: [PATCH 36/64] Shifted schedule deserialization into the JobDetails record itself and out of the grpc client. Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 6 +--- src/Dapr.Jobs/Models/Responses/JobDetails.cs | 35 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 0bd37fa3..309478fd 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -230,15 +230,11 @@ public override async Task GetJobAsync(string jobName, CancellationT throw new DaprException( "Get job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - - var intervalRegex = new Regex("h|m|(ms)|s"); - + return new JobDetails { DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, TTL = response.Job.Ttl is not null ? DateTime.Parse(response.Job.Ttl) : null, - Interval = response.Job.Schedule is not null && intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule.FromDurationString() : null, - CronExpression = response.Job.Schedule is null || !intervalRegex.IsMatch(response.Job.Schedule) ? response.Job.Schedule : null, RepeatCount = response.Job.Repeats == default ? null : response.Job.Repeats, Payload = response.Job.Data.ToByteArray() }; diff --git a/src/Dapr.Jobs/Models/Responses/JobDetails.cs b/src/Dapr.Jobs/Models/Responses/JobDetails.cs index 1af70023..7190d0fa 100644 --- a/src/Dapr.Jobs/Models/Responses/JobDetails.cs +++ b/src/Dapr.Jobs/Models/Responses/JobDetails.cs @@ -11,20 +11,28 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + namespace Dapr.Jobs.Models.Responses; /// /// Represents the details of a retrieved job. /// -public record JobDetails +public sealed record JobDetails { + /// + /// Regular expression used to determine if a given schedule is a Cron expression or an interval. + /// + private readonly Regex isIntervalRegex = new("h|m|(ms)|s"); + /// /// A cron-like expression that defines when a job should be triggered. /// /// /// Either this or the property should be specified. /// - public string? CronExpression { get; init; } = null; + public string? CronExpression => Schedule is null || !isIntervalRegex.IsMatch(Schedule) ? Schedule : null; /// /// The interval expression that defines when a job should be triggered. @@ -32,17 +40,36 @@ public record JobDetails /// /// Either this or the property should be specified. /// - public TimeSpan? Interval { get; init; } = null; + public TimeSpan? Interval => + Schedule is not null && isIntervalRegex.IsMatch(Schedule) ? Schedule.FromDurationString() : null; + + /// + /// Represents whether the job is scheduled using a Cron expression. + /// + public bool IsCronExpression => CronExpression is not null; + + /// + /// Represents whether the job is scheduled using an interval expression. + /// + public bool IsIntervalExpression => !IsCronExpression; + + /// + /// The string-based schedule value returned by the job details payload. + /// + [JsonPropertyName("schedule")] + public string? Schedule { get; init; } = null; /// /// Allows for jobs with fixed repeat counts. /// + [JsonPropertyName("repeats")] public uint? RepeatCount { get; init; } = null; /// /// Identifies a point-in-time representing when the job schedule should start from, /// or as a "one-shot" time if other scheduling fields are not provided. /// + [JsonPropertyName("dueTime")] public DateTime? DueTime { get; init; } = null; /// @@ -51,10 +78,12 @@ public record JobDetails /// /// This must be greater than if both are set. /// + [JsonPropertyName("ttl")] public DateTime? TTL { get; init; } = null; /// /// Stores the main payload of the job which is passed to the trigger function. /// + [JsonPropertyName("data")] public ReadOnlyMemory? Payload { get; init; } = null; } From 1252ef6dc7819d259df35db89f6b255947d89353 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 07:54:27 -0500 Subject: [PATCH 37/64] Adding JobsSample project to prove out API Signed-off-by: Whit Waldo --- all.sln | 14 ++++++ examples/Jobs/JobsSample/JobsSample.csproj | 13 ++++++ examples/Jobs/JobsSample/Program.cs | 43 +++++++++++++++++++ .../JobsSample/Properties/launchSettings.json | 30 +++++++++++++ .../JobsSample/appsettings.Development.json | 8 ++++ examples/Jobs/JobsSample/appsettings.json | 9 ++++ 6 files changed, 117 insertions(+) create mode 100644 examples/Jobs/JobsSample/JobsSample.csproj create mode 100644 examples/Jobs/JobsSample/Program.cs create mode 100644 examples/Jobs/JobsSample/Properties/launchSettings.json create mode 100644 examples/Jobs/JobsSample/appsettings.Development.json create mode 100644 examples/Jobs/JobsSample/appsettings.json diff --git a/all.sln b/all.sln index d3c368c9..7a31243e 100644 --- a/all.sln +++ b/all.sln @@ -124,6 +124,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs.Test", "test\Dapr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{3E075F71-185E-4C09-9449-79D21A958487}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{4DA40205-C38D-4E19-BD9A-0F18EE06CBAB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{11E59564-D677-4137-81BD-CF0B142530DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -308,6 +312,14 @@ Global {3E075F71-185E-4C09-9449-79D21A958487}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.Build.0 = Release|Any CPU + {11E59564-D677-4137-81BD-CF0B142530DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11E59564-D677-4137-81BD-CF0B142530DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.Build.0 = Release|Any CPU + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -364,6 +376,8 @@ Global {D34F9326-8D8C-43C4-975B-7201A9C97E6E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {EDEE625E-6815-40E1-935F-35129771A0F8} = {DD020B34-460F-455F-8D17-CF4A949F100B} {3E075F71-185E-4C09-9449-79D21A958487} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {4DA40205-C38D-4E19-BD9A-0F18EE06CBAB} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {11E59564-D677-4137-81BD-CF0B142530DB} = {4DA40205-C38D-4E19-BD9A-0F18EE06CBAB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Jobs/JobsSample/JobsSample.csproj b/examples/Jobs/JobsSample/JobsSample.csproj new file mode 100644 index 00000000..89ee1baf --- /dev/null +++ b/examples/Jobs/JobsSample/JobsSample.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs new file mode 100644 index 00000000..c81a481f --- /dev/null +++ b/examples/Jobs/JobsSample/Program.cs @@ -0,0 +1,43 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using System.Text; +using Dapr.Jobs; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.Models.Responses; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.MapScheduledJob("myJob", (ILogger logger, JobDetails details) => +{ + logger.LogInformation("Received trigger invocation for 'myJob'"); + + if (details.Payload is not null) + { + var deserializedPayload = Encoding.UTF8.GetString(details.Payload.Value.ToArray()); + + logger.LogInformation($"Received invocation for the 'myJob' job with payload '{deserializedPayload}'"); + } + else + { + logger.LogInformation("Failed to deserialize payload for trigger invocation"); + } +}); + +app.Run(); + +await using var scope = app.Services.CreateAsyncScope(); +var logger = scope.ServiceProvider.GetRequiredService(); +var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + +logger.LogInformation("Scheduling one-time job 'myJob' to execute 10 seconds from now"); +await daprJobsClient.ScheduleOneTimeJobAsync("myJob", DateTime.UtcNow.AddSeconds(10), + Encoding.UTF8.GetBytes("This is a test")); +logger.LogInformation("Scheduled one-time job 'myJob'"); + + +#pragma warning restore CS0618 // Type or member is obsolete diff --git a/examples/Jobs/JobsSample/Properties/launchSettings.json b/examples/Jobs/JobsSample/Properties/launchSettings.json new file mode 100644 index 00000000..edc5d029 --- /dev/null +++ b/examples/Jobs/JobsSample/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40682", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Jobs/JobsSample/appsettings.Development.json b/examples/Jobs/JobsSample/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/examples/Jobs/JobsSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Jobs/JobsSample/appsettings.json b/examples/Jobs/JobsSample/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/Jobs/JobsSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 4057ca522b673e9be8a924cb03492653a7055c02 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 08:15:53 -0500 Subject: [PATCH 38/64] Removed tentatively unnecessary attribute since we're relying on ASP.NET Core source generation instead of a custom one Signed-off-by: Whit Waldo --- src/Dapr.Jobs/ScheduledJobAttribute.cs | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/Dapr.Jobs/ScheduledJobAttribute.cs diff --git a/src/Dapr.Jobs/ScheduledJobAttribute.cs b/src/Dapr.Jobs/ScheduledJobAttribute.cs deleted file mode 100644 index f6fef027..00000000 --- a/src/Dapr.Jobs/ScheduledJobAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Dapr.Jobs; - -/// -/// Describes an endpoint as a subscriber for a job invocation. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public sealed class ScheduledJobAttribute : Attribute -{ - /// - /// The name of the job. - /// - public string JobName { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the job that invokes this method. - public ScheduledJobAttribute(string jobName) - { - JobName = jobName; - } -} From 449609e818f2573e4393f8a4dd33429f1dcb1621 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 08:19:54 -0500 Subject: [PATCH 39/64] Simplified example Signed-off-by: Whit Waldo --- examples/Jobs/JobsSample/Program.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs index c81a481f..d73fc004 100644 --- a/examples/Jobs/JobsSample/Program.cs +++ b/examples/Jobs/JobsSample/Program.cs @@ -21,11 +21,9 @@ var deserializedPayload = Encoding.UTF8.GetString(details.Payload.Value.ToArray()); logger.LogInformation($"Received invocation for the 'myJob' job with payload '{deserializedPayload}'"); + return; } - else - { - logger.LogInformation("Failed to deserialize payload for trigger invocation"); - } + logger.LogInformation("Failed to deserialize payload for trigger invocation"); }); app.Run(); From 01013277cb13bb6ae804240d75a9b0f068895c8b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 08:45:03 -0500 Subject: [PATCH 40/64] Fixing unit tests - resolving errors due to lack of InternalsVisibleTo attributes following Dapr.Common refactoring. Signed-off-by: Whit Waldo --- all.sln | 11 ++++++---- properties/IsExternalInit.cs | 2 +- src/Dapr.Common/AssemblyInfo.cs | 19 +++++++++++++++++ .../ArgumentVerifierTest.cs | 7 ++++--- test/Dapr.Common.Test/Dapr.Common.Test.csproj | 21 +++++++++++++++++++ 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/Dapr.Common/AssemblyInfo.cs rename test/{Dapr.Client.Test => Dapr.Common.Test}/ArgumentVerifierTest.cs (93%) create mode 100644 test/Dapr.Common.Test/Dapr.Common.Test.csproj diff --git a/all.sln b/all.sln index 7a31243e..1e1903b9 100644 --- a/all.sln +++ b/all.sln @@ -128,6 +128,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{4DA40205-C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{11E59564-D677-4137-81BD-CF0B142530DB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -316,10 +318,10 @@ Global {11E59564-D677-4137-81BD-CF0B142530DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.Build.0 = Release|Any CPU - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release + {229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -378,6 +380,7 @@ Global {3E075F71-185E-4C09-9449-79D21A958487} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {4DA40205-C38D-4E19-BD9A-0F18EE06CBAB} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {11E59564-D677-4137-81BD-CF0B142530DB} = {4DA40205-C38D-4E19-BD9A-0F18EE06CBAB} + {229BB84C-69A4-4A40-AD49-1FD6C237E0C5} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/properties/IsExternalInit.cs b/properties/IsExternalInit.cs index 34357c39..28e38a0c 100644 --- a/properties/IsExternalInit.cs +++ b/properties/IsExternalInit.cs @@ -13,5 +13,5 @@ namespace System.Runtime.CompilerServices internal static class IsExternalInit { } - + } diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs new file mode 100644 index 00000000..bb7dbef3 --- /dev/null +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/test/Dapr.Client.Test/ArgumentVerifierTest.cs b/test/Dapr.Common.Test/ArgumentVerifierTest.cs similarity index 93% rename from test/Dapr.Client.Test/ArgumentVerifierTest.cs rename to test/Dapr.Common.Test/ArgumentVerifierTest.cs index c839ac3e..27515018 100644 --- a/test/Dapr.Client.Test/ArgumentVerifierTest.cs +++ b/test/Dapr.Common.Test/ArgumentVerifierTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,11 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Common.Test { using System; using Xunit; - + + public class ArgumentVerifierTest { [Fact] diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj new file mode 100644 index 00000000..4aceb46f --- /dev/null +++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj @@ -0,0 +1,21 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file From c8602600d94ff07ddc6ed03305216303688e6d87 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 08:47:28 -0500 Subject: [PATCH 41/64] Fixed last locally-broken test client due to lack of InternalsVisibleTo attribute on Dapr.Common Signed-off-by: Whit Waldo --- src/Dapr.Common/AssemblyInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index bb7dbef3..5ee6e1a5 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -17,3 +17,4 @@ [assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] From fb983a9f9d54636e9584c16d42127222ef632e5b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 16:03:18 -0500 Subject: [PATCH 42/64] Updated name of invocation endpoint registration method to include "Dapr" Signed-off-by: Whit Waldo --- examples/Jobs/JobsSample/Program.cs | 2 +- src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs index d73fc004..2e9484a3 100644 --- a/examples/Jobs/JobsSample/Program.cs +++ b/examples/Jobs/JobsSample/Program.cs @@ -12,7 +12,7 @@ // Configure the HTTP request pipeline. -app.MapScheduledJob("myJob", (ILogger logger, JobDetails details) => +app.MapDaprScheduledJob("myJob", (ILogger logger, JobDetails details) => { logger.LogInformation("Received trigger invocation for 'myJob'"); diff --git a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs index 7cefdd4f..0c3f2a3c 100644 --- a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs @@ -10,13 +10,13 @@ public static class EndpointRouteBuilderExtensions { /// /// Adds a to the that registers a - /// scheduled job trigger invocation. + /// Dapr scheduled job trigger invocation. /// /// The to add the route to. /// The name of the job that should trigger this method when invoked. /// The delegate executed when the endpoint is matched. /// - public static IEndpointConventionBuilder MapScheduledJob(this IEndpointRouteBuilder endpoints, string jobName, + public static IEndpointConventionBuilder MapDaprScheduledJob(this IEndpointRouteBuilder endpoints, string jobName, Delegate handler) { return endpoints.MapPost($"/job/{jobName}", handler); From b3db8439622a7049afc7a465e87fd06bb3f0502a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:01:02 -0500 Subject: [PATCH 43/64] Building out unit tests Signed-off-by: Whit Waldo --- test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj | 35 ++++++------ ...DaprJobsServiceCollectionExtensionsTest.cs | 56 +++++++++++++++++++ .../EndpointRouteBuilderExtensionsTest.cs | 28 ++++++++++ 3 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs create mode 100644 test/Dapr.Jobs.Test/EndpointRouteBuilderExtensionsTest.cs diff --git a/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj index 1dd8858e..f8b3b50f 100644 --- a/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj +++ b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj @@ -1,21 +1,22 @@  - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + - + \ No newline at end of file diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..cb95400e --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.Json; +using Dapr.Jobs.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class DaprJobsServiceCollectionExtensionsTest +{ + [Fact] + public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() + { + var services = new ServiceCollection(); + + var clientBuilder = new Action(builder => + builder.UseJsonSerializationOptions(new JsonSerializerOptions { PropertyNameCaseInsensitive = false })); + + //Registers with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) + services.AddDaprJobsClient(); + //Register with PropertyNameCaseInsensitive = false + services.AddDaprJobsClient(clientBuilder); + + var serviceProvider = services.BuildServiceProvider(); + DaprJobsGrpcClient daprClient = serviceProvider.GetService() as DaprJobsGrpcClient; + Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprJobsClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); + + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = caseSensitivity + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + DaprJobsGrpcClient client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; + + //Registers with case-insensitive as true by default, but we set as false above + Assert.False(client.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; + } +} diff --git a/test/Dapr.Jobs.Test/EndpointRouteBuilderExtensionsTest.cs b/test/Dapr.Jobs.Test/EndpointRouteBuilderExtensionsTest.cs new file mode 100644 index 00000000..d1a3bb0e --- /dev/null +++ b/test/Dapr.Jobs.Test/EndpointRouteBuilderExtensionsTest.cs @@ -0,0 +1,28 @@ +namespace Dapr.Jobs.Test; + +public class EndpointRouteBuilderExtensionsTest +{ + //The following won't work because Moq can't test static methods - leaving here should the project ever adopt some sort of commercial tool that enables such testing + //[Fact] + //public void MapDaprScheduledJobs_ValidateRegisteredRoute() + //{ + // var endpoints = new Mock(); + // endpoints.Setup(a => a.DataSources).Returns(new List()); //While .NET 9 changed this behavior, .NET 8 and lower will throw if this isn't setup on the mock + + // const string jobName = "test-job"; + // var handler = (JobDetails details) => + // { + // //Doesn't matter what it does here + // }; + + // var result = endpoints.Object.MapDaprScheduledJob(jobName, handler); + + // var registeredRoutes = endpoints.Invocations + // .Where(invocation => invocation.Method.Name == "MapPost") + // .ToList(); + + // Assert.Single(registeredRoutes); //One MapPost call + // Assert.Equal($"job/{jobName}", registeredRoutes[0].Arguments[0]); //Validate the route + // Assert.Equal(handler, registeredRoutes[0].Arguments[1]); //Validate the handler delegate + //} +} From f6333e91bc0a254db44a3e233bad2dc4f89cd6b3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:01:16 -0500 Subject: [PATCH 44/64] Removed unused using Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 309478fd..766b405c 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -14,7 +14,6 @@ using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; -using System.Text.RegularExpressions; using Dapr.Jobs.Models.Responses; using Google.Protobuf; using Google.Protobuf.WellKnownTypes; From 2bf4cfd18167266c6a858ed90e1ad8e68ef6fe5c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:01:27 -0500 Subject: [PATCH 45/64] Added some null validation Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs index 0c3f2a3c..caf286d1 100644 --- a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs @@ -19,6 +19,10 @@ public static class EndpointRouteBuilderExtensions public static IEndpointConventionBuilder MapDaprScheduledJob(this IEndpointRouteBuilder endpoints, string jobName, Delegate handler) { + ArgumentNullException.ThrowIfNull(endpoints, nameof(endpoints)); + ArgumentNullException.ThrowIfNull(jobName, nameof(jobName)); + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + return endpoints.MapPost($"/job/{jobName}", handler); } } From e5d564618adeac9d415197380a56aefaa10f2b77 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:03:56 -0500 Subject: [PATCH 46/64] Refactored to use the same naming convention as the other types in the project Signed-off-by: Whit Waldo --- .../{DaprJobClientBuilder.cs => DaprJobsClientBuilder.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Dapr.Jobs/{DaprJobClientBuilder.cs => DaprJobsClientBuilder.cs} (94%) diff --git a/src/Dapr.Jobs/DaprJobClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs similarity index 94% rename from src/Dapr.Jobs/DaprJobClientBuilder.cs rename to src/Dapr.Jobs/DaprJobsClientBuilder.cs index a50bf6b1..3dda35f3 100644 --- a/src/Dapr.Jobs/DaprJobClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -19,7 +19,7 @@ namespace Dapr.Jobs; /// /// Builds a . /// -public sealed class DaprJobClientBuilder : DaprGenericClientBuilder +public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder { /// /// Builds the client instance from the properties of the builder. From 61b19784b77a9ca338cc6c5a93b6ed2ed0f1b56e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:14:07 -0500 Subject: [PATCH 47/64] Building on last commit, updated extension names Signed-off-by: Whit Waldo --- .../Extensions/DaprJobsServiceCollectionExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 727320b9..3a0b9ddc 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi serviceCollection.TryAddSingleton(_ => { - var builder = new DaprJobClientBuilder(); + var builder = new DaprJobsClientBuilder(); return builder.Build(); }); @@ -44,13 +44,13 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi /// /// The . /// Optionally allows greater configuration of the . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); serviceCollection.TryAddSingleton(_ => { - var builder = new DaprJobClientBuilder(); + var builder = new DaprJobsClientBuilder(); configure?.Invoke(builder); return builder.Build(); @@ -65,13 +65,13 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi /// The . /// Optionally allows greater configuration of the using injected services. /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); serviceCollection.TryAddSingleton(serviceProvider => { - var builder = new DaprJobClientBuilder(); + var builder = new DaprJobsClientBuilder(); configure?.Invoke(serviceProvider, builder); return builder.Build(); From 90a5fe14ef65c8aa431c8333078ecd6a4624210f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:14:14 -0500 Subject: [PATCH 48/64] Adding more unit tests Signed-off-by: Whit Waldo --- .../DaprJobsClientBuilderTest.cs | 109 ++++++++++++++++++ ...DaprJobsServiceCollectionExtensionsTest.cs | 2 +- test/Dapr.Jobs.Test/JobDetailsTests.cs | 37 ++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs create mode 100644 test/Dapr.Jobs.Test/JobDetailsTests.cs diff --git a/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs new file mode 100644 index 00000000..310da7a7 --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.Json; +using Grpc.Net.Client; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class DaprJobsClientBuilderTest +{ + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() + { + DaprJobsClientBuilder builder = new DaprJobsClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprJobsClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesHttpEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_SetsApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_SetsNullApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken(null); + builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenSet_SetsApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.Equal(default, entry); + } + + [Fact] + public void DaprJobsClientBuilder_SetsTimeout() + { + var builder = new DaprJobsClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index cb95400e..7c2300e3 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -13,7 +13,7 @@ public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var clientBuilder = new Action(builder => + var clientBuilder = new Action(builder => builder.UseJsonSerializationOptions(new JsonSerializerOptions { PropertyNameCaseInsensitive = false })); //Registers with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) diff --git a/test/Dapr.Jobs.Test/JobDetailsTests.cs b/test/Dapr.Jobs.Test/JobDetailsTests.cs new file mode 100644 index 00000000..9aa81db8 --- /dev/null +++ b/test/Dapr.Jobs.Test/JobDetailsTests.cs @@ -0,0 +1,37 @@ +using System; +using Dapr.Jobs.Models.Responses; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class JobDetailsTests +{ + [Fact] + public void JobDetails_CronExpressionShouldPopulateFromSchedule() + { + const string cronSchedule = "5 4 * * *"; + + var jobDetails = new JobDetails { Schedule = cronSchedule }; + + Assert.False(jobDetails.IsIntervalExpression); + Assert.True(jobDetails.IsCronExpression); + Assert.Null(jobDetails.Interval); + Assert.Equal(cronSchedule, jobDetails.Schedule); + Assert.Equal(cronSchedule, jobDetails.CronExpression); + } + + [Fact] + public void JobDetails_IntervalShouldPopulateFromSchedule() + { + var interval = new TimeSpan(4, 2, 1); + var intervalString = interval.ToDurationString(); + + var jobDetails = new JobDetails { Schedule = intervalString }; + + Assert.True(jobDetails.IsIntervalExpression); + Assert.False(jobDetails.IsCronExpression); + Assert.Null(jobDetails.CronExpression); + Assert.Equal(intervalString, jobDetails.Schedule); + Assert.Equal(interval, jobDetails.Interval); + } +} From e674fa8f02651182f85fb12c77d28bb5f80059c8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 19:14:37 -0500 Subject: [PATCH 49/64] Removed unused type Signed-off-by: Whit Waldo --- src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs diff --git a/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs b/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs deleted file mode 100644 index 07c00d93..00000000 --- a/src/Dapr.Jobs/Models/Responses/WatchedJobDetails.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Dapr.Jobs.Models.Responses; - -/// -/// Returns information about a watched job. -/// -/// The type to deserialize the payload to. -/// The identifier of the job itself - once the job is processed, this should be returned back to the server so it can be finalized. -/// The name of the job. -/// The payload data included with the job. -public record WatchedJobDetails(ulong Id, string Name, T Payload); From 4215d877180823ea2801a16a2ae372b1141ed8aa Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 15 Jul 2024 23:33:01 -0500 Subject: [PATCH 50/64] Updated to use int? instead of uint? for repeats parameter. Added check in GRPC client implementation to check for negative values. Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 4 ++-- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 1747d3a4..23426d66 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -38,7 +38,7 @@ public abstract class DaprJobsClient /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, - uint? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, + int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); /// @@ -53,7 +53,7 @@ public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, - uint? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, + int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 766b405c..b2494ac1 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -71,7 +71,8 @@ internal DaprJobsGrpcClient( /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, uint? repeats = null, + public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) @@ -85,7 +86,12 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre job.DueTime = ((DateTime)dueTime).ToString("O"); if (repeats is not null) + { + if (repeats < 0) + throw new ArgumentOutOfRangeException(nameof(repeats)); + job.Repeats = (uint)repeats; + } if (payload is not null) job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; @@ -126,7 +132,8 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, DateTime? startingFrom = null, uint? repeats = null, + public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan interval, + DateTime? startingFrom = null, int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) @@ -138,7 +145,12 @@ public override async Task ScheduleIntervalJobAsync(string jobName, TimeSpan int job.DueTime = ((DateTime)startingFrom).ToString("O"); if (repeats is not null) + { + if (repeats < 0) + throw new ArgumentOutOfRangeException(nameof(repeats)); + job.Repeats = (uint)repeats; + } if (payload is not null) job.Data = job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; From 8e72f5a118618badc116fece4d56715078c9cb7e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 00:07:49 -0500 Subject: [PATCH 51/64] Added support for IDisposable Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 35 ++++++++++++++++++++++++++++- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 11 ++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 23426d66..c0d554b5 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -17,10 +17,25 @@ namespace Dapr.Jobs; /// +/// /// Defines client operations for managing Dapr jobs. +/// Use to create a or register +/// for use with dependency injection via +/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient +/// . +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance +/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid +/// creating a disposing a client instance for each operation that the application performs - this can lead to socket +/// exhaustion and other problems. +/// /// -public abstract class DaprJobsClient +public abstract class DaprJobsClient : IDisposable { + private bool disposed; + /// /// Gets the used for JSON serialization purposes. /// @@ -93,4 +108,22 @@ public abstract Task ScheduleOneTimeJobAsync(string jobName, DateTime scheduledT return new KeyValuePair("dapr-api-token", apiToken); } + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index b2494ac1..72f10fbf 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -79,7 +79,7 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre throw new ArgumentNullException(nameof(jobName)); if (string.IsNullOrWhiteSpace(cronExpression)) throw new ArgumentNullException(nameof(cronExpression)); - + var job = new Autogenerated.Job { Name = jobName, Schedule = cronExpression }; if (dueTime is not null) @@ -279,6 +279,15 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc } } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.channel.Dispose(); + this.httpClient.Dispose(); + } + } + private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) { var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); From 20be1d50039364d4d7f92d7040a70828692ec6f1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 00:19:57 -0500 Subject: [PATCH 52/64] Updated to support provisioning an HttpClient from IHttpClientFactory instead of creating a new instance from the DaprClientGenericBuilder. Signed-off-by: Whit Waldo --- src/Dapr.Common/Dapr.Common.csproj | 1 + src/Dapr.Common/DaprGenericClientBuilder.cs | 12 ++++++++ .../DaprJobsServiceCollectionExtensions.cs | 29 ++++++++++++++++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index a715123c..ef8c037e 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 59ec4977..1f4891c9 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -101,6 +101,18 @@ internal DaprGenericClientBuilder UseHttpClientFactory(Func + /// Overrides the legacy mechanism for building an HttpClient and uses the new + /// introduced in .NET Core 2.1. + /// + /// The factory used to create instances. + /// + public DaprGenericClientBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory) + { + this.HttpClientFactory = httpClientFactory.CreateClient; + return this; + } + /// /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. /// diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 3a0b9ddc..92cf0a42 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -30,9 +30,16 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - serviceCollection.TryAddSingleton(_ => + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => { + var httpClientFactory = serviceProvider.GetRequiredService(); + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + return builder.Build(); }); @@ -44,13 +51,20 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi /// /// The . /// Optionally allows greater configuration of the . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - serviceCollection.TryAddSingleton(_ => + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => { + var httpClientFactory = serviceProvider.GetRequiredService(); + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + configure?.Invoke(builder); return builder.Build(); @@ -65,13 +79,20 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi /// The . /// Optionally allows greater configuration of the using injected services. /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + serviceCollection.TryAddSingleton(serviceProvider => { + var httpClientFactory = serviceProvider.GetRequiredService(); + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + configure?.Invoke(serviceProvider, builder); return builder.Build(); From 1780337d094a3147d8134166c912a06dcc96aac5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 01:59:22 -0500 Subject: [PATCH 53/64] Fixed an issue with the API token not being properly passed into client - caught while augmenting unit tests Signed-off-by: Whit Waldo --- src/Dapr.Client/DaprClientBuilder.cs | 2 +- src/Dapr.Common/DaprGenericClientBuilder.cs | 3 +- src/Dapr.Jobs/DaprJobsClientBuilder.cs | 2 +- ...DaprJobsServiceCollectionExtensionsTest.cs | 49 ++++++++++++------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 0dce2f44..ce643c86 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -29,7 +29,7 @@ public override DaprClient Build() var daprClientDependencies = this.BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); + var apiTokenHeader = this.DaprApiToken is not null ? DaprClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; return new DaprClientGrpc(daprClientDependencies.channel, client, daprClientDependencies.httpClient, daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 1f4891c9..16834455 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -69,7 +69,7 @@ protected DaprGenericClientBuilder() /// /// Property exposed for testing purposes. /// - public string DaprApiToken { get; private set; } + public string? DaprApiToken { get; private set; } /// /// Property exposed for testing purposes. /// @@ -182,7 +182,6 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) /// Builds out the inner DaprClient that provides the core shape of the /// runtime gRPC client used by the consuming package. /// - /// A DaprClient instance. /// protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() { diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 3dda35f3..955edea2 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -30,7 +30,7 @@ public override DaprJobsClient Build() var daprClientDependencies = this.BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - var apiTokenHeader = DaprJobsClient.GetDaprApiTokenHeader("dapr-api-token"); + var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; return new DaprJobsGrpcClient(daprClientDependencies.channel, client, daprClientDependencies.httpClient, daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index 7c2300e3..247a8d26 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Net.Http; using System.Text.Json; using Dapr.Jobs.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -14,43 +16,54 @@ public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() var services = new ServiceCollection(); var clientBuilder = new Action(builder => - builder.UseJsonSerializationOptions(new JsonSerializerOptions { PropertyNameCaseInsensitive = false })); + builder.UseDaprApiToken("abc")); + + services.AddDaprJobsClient(); //Doesn't set an API token value + services.AddDaprJobsClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + DaprJobsGrpcClient daprClient = serviceProvider.GetService() as DaprJobsGrpcClient; + Assert.False(daprClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprJobsClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); - //Registers with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) services.AddDaprJobsClient(); - //Register with PropertyNameCaseInsensitive = false - services.AddDaprJobsClient(clientBuilder); var serviceProvider = services.BuildServiceProvider(); - DaprJobsGrpcClient daprClient = serviceProvider.GetService() as DaprJobsGrpcClient; - Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprJobsClient = serviceProvider.GetService(); + Assert.NotNull(daprJobsClient); } [Fact] public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() { var services = new ServiceCollection(); - services.AddSingleton(); + services.AddSingleton(); services.AddDaprJobsClient((provider, builder) => { - var configProvider = provider.GetRequiredService(); - var caseSensitivity = configProvider.GetCaseSensitivity(); + var configProvider = provider.GetRequiredService(); + var daprApiToken = configProvider.GetApiTokenValue(); - builder.UseJsonSerializationOptions(new JsonSerializerOptions - { - PropertyNameCaseInsensitive = caseSensitivity - }); + builder.UseDaprApiToken(daprApiToken); }); var serviceProvider = services.BuildServiceProvider(); - DaprJobsGrpcClient client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; + var client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; + var apiTokenValue = client.httpClient.DefaultRequestHeaders.GetValues("dapr-api-token").First(); - //Registers with case-insensitive as true by default, but we set as false above - Assert.False(client.JsonSerializerOptions.PropertyNameCaseInsensitive); + Assert.Equal("abcdef", apiTokenValue); } - private class TestConfigurationProvider + private class TestSecretRetriever { - public bool GetCaseSensitivity() => false; + public string GetApiTokenValue() => "abcdef"; } } From 22d3a1745c24e28b3be301d398eee61fd20525e5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:14:44 -0500 Subject: [PATCH 54/64] Another tweak to finalize fix for Dapr API token - updated tests as well Signed-off-by: Whit Waldo --- src/Dapr.Common/DaprGenericClientBuilder.cs | 6 +++++- .../DaprJobsServiceCollectionExtensionsTest.cs | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 16834455..a23f181e 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -66,10 +66,11 @@ protected DaprGenericClientBuilder() /// Property exposed for testing purposes. /// public GrpcChannelOptions GrpcChannelOptions { get; private set; } + /// /// Property exposed for testing purposes. /// - public string? DaprApiToken { get; private set; } + public string DaprApiToken { get; private set; } /// /// Property exposed for testing purposes. /// @@ -211,6 +212,9 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) httpClient.Timeout = this.Timeout; } + //Set the API token in the HttpClient default headers even if it's an empty string + httpClient.DefaultRequestHeaders.Add("dapr-api-token", DaprApiToken); + return (channel, httpClient, httpEndpoint); } diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index 247a8d26..45b57436 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -18,12 +18,15 @@ public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() var clientBuilder = new Action(builder => builder.UseDaprApiToken("abc")); - services.AddDaprJobsClient(); //Doesn't set an API token value + services.AddDaprJobsClient(); //Sets a default API token value of an empty string services.AddDaprJobsClient(clientBuilder); //Sets the API token value var serviceProvider = services.BuildServiceProvider(); - DaprJobsGrpcClient daprClient = serviceProvider.GetService() as DaprJobsGrpcClient; - Assert.False(daprClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + DaprJobsGrpcClient daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; + + Assert.Null(daprJobClient!.apiTokenHeader); + Assert.True(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var apiTokenValue)); + Assert.Equal("", apiTokenValue.First()); } [Fact] @@ -57,9 +60,15 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() var serviceProvider = services.BuildServiceProvider(); var client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; - var apiTokenValue = client.httpClient.DefaultRequestHeaders.GetValues("dapr-api-token").First(); + //Validate it's set on the HttpClient + var apiTokenValue = client.httpClient.DefaultRequestHeaders.GetValues("dapr-api-token").First(); Assert.Equal("abcdef", apiTokenValue); + + //Validate it's set in the apiTokenHeader property + Assert.NotNull(client.apiTokenHeader); + Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); + Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); } private class TestSecretRetriever From 60b1c888f9b85ecba95272725f39e2085c489bf1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:16:18 -0500 Subject: [PATCH 55/64] Adding helper deserialization extension methods + tests Signed-off-by: Whit Waldo --- .../ByteArrayDeserializationExtensions.cs | 30 +++++++++++ .../ByteArrayDeserializationExtensionsTest.cs | 54 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/Dapr.Jobs/Extensions/Helpers/Deserialization/ByteArrayDeserializationExtensions.cs create mode 100644 test/Dapr.Jobs.Test/ByteArrayDeserializationExtensionsTest.cs diff --git a/src/Dapr.Jobs/Extensions/Helpers/Deserialization/ByteArrayDeserializationExtensions.cs b/src/Dapr.Jobs/Extensions/Helpers/Deserialization/ByteArrayDeserializationExtensions.cs new file mode 100644 index 00000000..99e8563d --- /dev/null +++ b/src/Dapr.Jobs/Extensions/Helpers/Deserialization/ByteArrayDeserializationExtensions.cs @@ -0,0 +1,30 @@ +using System.Text; +using System.Text.Json; + +namespace Dapr.Jobs.Extensions.Helpers.Deserialization; + +/// +/// Provides utility extensions for deserializing an array of UTF-8 encoded bytes. +/// +public static class ByteArrayDeserializationExtensions +{ + /// + /// Deserializes an array of UTF-8 encoded bytes to a string. + /// + /// The array of UTF-8 encoded bytes. + /// A decoded string. + public static string DeserializeToString(this byte[] byteArray) => Encoding.UTF8.GetString(byteArray); + + /// + /// Attempts to deserialize an array of UTF-8 encoded bytes to the indicated type. + /// + /// The JSON-compatible type to deserialize to. + /// The array of UTF-8 encoded bytes to deserialize. + /// Optional options to use for the . + /// The deserialized data. + public static TJsonObject? DeserializeFromJsonBytes(this byte[] byteArray, JsonSerializerOptions? jsonSerializerOptions = null) + { + var serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); + return JsonSerializer.Deserialize(byteArray, serializerOptions); + } +} diff --git a/test/Dapr.Jobs.Test/ByteArrayDeserializationExtensionsTest.cs b/test/Dapr.Jobs.Test/ByteArrayDeserializationExtensionsTest.cs new file mode 100644 index 00000000..52ab6463 --- /dev/null +++ b/test/Dapr.Jobs.Test/ByteArrayDeserializationExtensionsTest.cs @@ -0,0 +1,54 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Jobs.Extensions.Helpers.Deserialization; +using Xunit; + +#nullable enable + +namespace Dapr.Jobs.Test; + +public class ByteArrayDeserializationExtensionsTest +{ + [Fact] + public void DeserializeToString_Deserialize() + { + const string originalStringValue = "This is a simple test!"; + var serializedString = Encoding.UTF8.GetBytes(originalStringValue); + + var deserializedString = serializedString.DeserializeToString(); + Assert.Equal(originalStringValue, deserializedString); + } + + [Fact] + public void DeserializeToJsonObject_Deserialize() + { + var originalType = new TestType { Name = "Test", Value = 5 }; + var serialized = JsonSerializer.SerializeToUtf8Bytes(originalType); + + var deserializedType = serialized.DeserializeFromJsonBytes(); + Assert.Equal(originalType, deserializedType); + } + + [Fact] + public void DeserializeToJsonObject_DeserializeWithOptions() + { + const string json = "{\"value\": \"15\"}"; + var jsonOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString}; + var serializedBytes = Encoding.UTF8.GetBytes(json); + + var deserializedType = serializedBytes.DeserializeFromJsonBytes(jsonOptions); + Assert.NotNull(deserializedType); + Assert.Null(deserializedType.Name); + Assert.Equal(15, deserializedType.Value); + } + + private sealed record TestType + { + [JsonPropertyName("name")] + public string? Name { get; init; } = null; + + [JsonPropertyName("value")] + public int Value { get; init; } + } +} From db4f93fbfab9dba4b0f7987b64be0550f00cd410 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:29:10 -0500 Subject: [PATCH 56/64] Added more helper extension methods for serializing strings and JSON-compatible objects. Signed-off-by: Whit Waldo --- .../DaprCronJobsSerializationExtensions.cs | 64 +++++++++++++++++++ ...DaprIntervalJobsSerializationExtensions.cs | 61 ++++++++++++++++++ .../DaprOneTimeJobsSerializationExtensions.cs | 52 +++++++++++++++ ...DaprJobsServiceCollectionExtensionsTest.cs | 1 - 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprCronJobsSerializationExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprIntervalJobsSerializationExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprOneTimeJobsSerializationExtensions.cs diff --git a/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprCronJobsSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprCronJobsSerializationExtensions.cs new file mode 100644 index 00000000..f8cad7cc --- /dev/null +++ b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprCronJobsSerializationExtensions.cs @@ -0,0 +1,64 @@ +using System.Text; +using System.Text.Json; +using ArgumentNullException = System.ArgumentNullException; + +namespace Dapr.Jobs.Extensions.Helpers.Serialization; + +/// +/// Provides helper extensions for performing serialization operations when scheduling Cron jobs for the developer. +/// +public static class DaprCronJobsSerializationExtensions +{ + /// + /// Schedules a recurring job using a cron expression. + /// + /// The instance. + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The main payload of the job expressed as a JSON-serializable object. + /// Optional options to use for the . + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleCronJobWithPayloadAsync(this DaprJobsClient client, string jobName, + string cronExpression, object payload, JsonSerializerOptions? jsonSerializerOptions = null, DateTime? dueTime = null, int? repeats = null, DateTime? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); + var payloadBytes = + JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); + + await client.ScheduleCronJobAsync(jobName, cronExpression, dueTime, repeats, ttl, payloadBytes, + cancellationToken); + } + + /// + /// Schedules a recurring job using a cron expression. + /// + /// The instance. + /// The name of the job being scheduled. + /// The systemd Cron-like expression indicating when the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// The main payload of the job expressed as a string. + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleCronJobWithPayloadAsync(this DaprJobsClient client, string jobName, + string cronExpression, string payload, DateTime? dueTime = null, int? repeats = null, DateTime? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var payloadBytes = Encoding.UTF8.GetBytes(payload); + + await client.ScheduleCronJobAsync(jobName, cronExpression, dueTime, repeats, ttl, payloadBytes, + cancellationToken); + } +} diff --git a/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprIntervalJobsSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprIntervalJobsSerializationExtensions.cs new file mode 100644 index 00000000..b65a8f5f --- /dev/null +++ b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprIntervalJobsSerializationExtensions.cs @@ -0,0 +1,61 @@ +using System.Text; +using System.Text.Json; + +namespace Dapr.Jobs.Extensions.Helpers.Serialization; + +/// +/// Provides helper extensions for performing serialization operations when scheduling interface jobs for the developer. +/// +public static class DaprIntervalJobsSerializationExtensions +{ + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The instance. + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// The main payload of the job expressed as a JSON-serializable object. + /// Optional options to use for the . + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleIntervalJobWithPayloadAsync(this DaprJobsClient client, string jobName, + TimeSpan interval, object payload, JsonSerializerOptions? jsonSerializerOptions = null, + DateTime? startingFrom = null, + int? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); + var payloadBytes = + JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); + + await client.ScheduleIntervalJobAsync(jobName, interval, startingFrom, repeats, ttl, payloadBytes, + cancellationToken); + } + + /// + /// Schedules a recurring job with an optional future starting date. + /// + /// The instance. + /// The name of the job being scheduled. + /// The interval at which the job should be triggered. + /// The optional point-in-time from which the job schedule should start. + /// The optional maximum number of times the job should be triggered. + /// Represents when the job should expire. If both this and StartingFrom are set, TTL needs to represent a later point in time. + /// The main payload of the job expressed as a JSON-serializable object. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleIntervalJobWithPayloadAsync(this DaprJobsClient client, string jobName, + TimeSpan interval, string payload, DateTime? startingFrom = null, + int? repeats = null, DateTime? ttl = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + var payloadBytes = Encoding.UTF8.GetBytes(payload); + JsonSerializer.SerializeToUtf8Bytes(payload); + await client.ScheduleIntervalJobAsync(jobName, interval, startingFrom, repeats, ttl, payloadBytes, + cancellationToken); + } +} diff --git a/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprOneTimeJobsSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprOneTimeJobsSerializationExtensions.cs new file mode 100644 index 00000000..cc4d2450 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/Helpers/Serialization/DaprOneTimeJobsSerializationExtensions.cs @@ -0,0 +1,52 @@ +using System.Text; +using System.Text.Json; + +namespace Dapr.Jobs.Extensions.Helpers.Serialization; + +/// +/// Provides helper extensions for performing serialization operations when scheduling one-time Cron jobs for the developer. +/// +public static class DaprOneTimeJobsSerializationExtensions +{ + /// + /// Schedules a one-time job. + /// + /// The instance. + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// The main payload of the job expressed as a JSON-serializable object. + /// Optional options to use for the . + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleOneTimeJobWithPayloadAsync(this DaprJobsClient client, string jobName, DateTime scheduledTime, + object payload, JsonSerializerOptions? jsonSerializerOptions = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var serializerOptions = jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); + var payloadBytes = + JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); + + await client.ScheduleOneTimeJobAsync(jobName, scheduledTime, payloadBytes, cancellationToken); + } + + /// + /// Schedules a one-time job. + /// + /// The instance. + /// The name of the job being scheduled. + /// The point in time when the job should be run. + /// The main payload of the job expressed as a string. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleOneTimeJobWithPayloadAsync(this DaprJobsClient client, string jobName, DateTime scheduledTime, + string payload, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var payloadBytes = Encoding.UTF8.GetBytes(payload); + + await client.ScheduleOneTimeJobAsync(jobName, scheduledTime, payloadBytes, cancellationToken); + } +} diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index 45b57436..a0e668dc 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Net.Http; -using System.Text.Json; using Dapr.Jobs.Extensions; using Microsoft.Extensions.DependencyInjection; using Xunit; From 62052e6914a6444c03214272f1573f7d15eb6292 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:29:45 -0500 Subject: [PATCH 57/64] Naming correction to ensure parameter names are similar from one schedule method to the next Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsClient.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index c0d554b5..ab2f1766 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Text.Json; using Dapr.Jobs.Models.Responses; namespace Dapr.Jobs; @@ -35,24 +34,19 @@ namespace Dapr.Jobs; public abstract class DaprJobsClient : IDisposable { private bool disposed; - - /// - /// Gets the used for JSON serialization purposes. - /// - public abstract JsonSerializerOptions JsonSerializerOptions { get; } - + /// /// Schedules a recurring job using a cron expression. /// /// The name of the job being scheduled. /// The systemd Cron-like expression indicating when the job should be triggered. - /// The optional point-in-time from which the job schedule should start. + /// The optional point-in-time from which the job schedule should start. /// The optional number of times the job should be triggered. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + public abstract Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? startingFrom = null, int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default); From 9fef17710c8be3950dfec608962a51576610b438 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:30:45 -0500 Subject: [PATCH 58/64] Removed JsonSerializerOptions support from DaprJobsClient as it's not used anywhere. Rather, previous commits provided helper extensions that will allow configurable serialization, but the methods exposed on the DaprJobsClient assume the developer will bring their own serialization approach to bear. Signed-off-by: Whit Waldo --- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 72f10fbf..148e79c2 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -29,32 +29,27 @@ namespace Dapr.Jobs; internal sealed class DaprJobsGrpcClient : DaprJobsClient { private readonly Uri httpEndpoint; - private readonly HttpClient httpClient; - - private readonly JsonSerializerOptions jsonSerializerOptions; - + internal readonly HttpClient httpClient; + private readonly GrpcChannel channel; private readonly Autogenerated.Dapr.DaprClient client; - private readonly KeyValuePair? apiTokenHeader; + internal readonly KeyValuePair? apiTokenHeader; // property exposed for testing purposes internal Autogenerated.Dapr.DaprClient Client => client; - - public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; - + internal DaprJobsGrpcClient( GrpcChannel channel, Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, Uri httpEndpoint, - JsonSerializerOptions jsonSerializerOptions, + JsonSerializerOptions _, KeyValuePair? apiTokenHeader) { this.channel = channel; this.client = innerClient; this.httpClient = httpClient; this.httpEndpoint = httpEndpoint; - this.jsonSerializerOptions = jsonSerializerOptions; this.apiTokenHeader = apiTokenHeader; this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); @@ -65,13 +60,13 @@ internal DaprJobsGrpcClient( /// /// The name of the job being scheduled. /// The systemd Cron-like expression indicating when the job should be triggered. - /// The optional point-in-time from which the job schedule should start. + /// The optional point-in-time from which the job schedule should start. /// The optional number of times the job should be triggered. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. /// The main payload of the job. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? dueTime = null, + public override async Task ScheduleCronJobAsync(string jobName, string cronExpression, DateTime? startingFrom = null, int? repeats = null, DateTime? ttl = null, ReadOnlyMemory? payload = null, CancellationToken cancellationToken = default) { @@ -82,8 +77,8 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre var job = new Autogenerated.Job { Name = jobName, Schedule = cronExpression }; - if (dueTime is not null) - job.DueTime = ((DateTime)dueTime).ToString("O"); + if (startingFrom is not null) + job.DueTime = ((DateTime)startingFrom).ToString("O"); if (repeats is not null) { @@ -98,9 +93,9 @@ public override async Task ScheduleCronJobAsync(string jobName, string cronExpre if (ttl is not null) { - if (ttl <= dueTime) + if (ttl <= startingFrom) throw new ArgumentException( - $"When both {nameof(ttl)} and {nameof(dueTime)} are specified, {nameof(ttl)} must represent a later point in time"); + $"When both {nameof(ttl)} and {nameof(startingFrom)} are specified, {nameof(ttl)} must represent a later point in time"); job.Ttl = ((DateTime)ttl).ToString("O"); } From 39dbcf818aba54e3debeafde86ab8159a53bf0d2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 02:50:40 -0500 Subject: [PATCH 59/64] First pass at updating documentation Signed-off-by: Whit Waldo --- daprdocs/content/en/dotnet-sdk-docs/_index.md | 7 + .../en/dotnet-sdk-docs/dotnet-jobs/_index.md | 8 + .../dotnet-jobs/dotnet-jobs-howto.md | 269 ++++++++++++++++++ .../dotnet-jobs/dotnet-jobsclient-usage.md | 171 +++++++++++ 4 files changed, 455 insertions(+) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index e823ca29..682a11f2 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -69,6 +69,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria +
+
+
Jobs
+

Create and manage the scheduling and orchestration of jobs in .NET.

+ +
+
## More information diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md new file mode 100644 index 00000000..de000571 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md @@ -0,0 +1,8 @@ +--- +type: docs +title: "Dapr Jobs .NET SDK" +linkTitle: "Jobs" +weight: 50000 +description: Get up and running with Dapr Jobs and the Dapr .NET SDK +--- + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md new file mode 100644 index 00000000..51414b93 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -0,0 +1,269 @@ +--- +type: docs +title: "How to: Author and manage Dapr Jobs in the .NET SDK" +linkTitle: "How to: Author & manage jobs" +weight: 10000 +description: Learn how to author and manage Dapr Jobs using the .NET SDK +--- + +Let's create an endpoint that will be invoked by Dapr Jobs when it triggers, then schedule the job in the same app. We'll use the [simple example provided here](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs), for the following demonstration and walk through it as an explainer of how you can schedule one-time or recurring jobs using either an interval or Cron expression yourself. In this guide, +you will: + +- Deploy a .NET Web API application ([JobsSample](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample)) +- Utilize the .NET Jobs SDK to schedule a job invocation and set up the endpoint to be triggered + +In the .NET example project: +- The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample/Program.cs) file comprises the entirety of this demonstration. + +## Prerequisites +- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost) +- [Dapr Jobs .NET SDK](https://github.com/dapr/dotnet-sdk) + +## Set up the environment +Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). + +```sh +git clone https://github.com/dapr/dotnet-sdk.git +``` + +From the .NET SDK root directory, navigate to the Dapr Jobs example. + +```sh +cd examples/Jobs +``` + +## Run the application locally + +To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the `JobsSample` directory. + +```sh +cd JobsSample +``` + +We'll run a command that starts both the Dapr sidecar and the .NET program at the same time. + +```sh +dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run +``` +> Dapr listens for HTTP requests at `http://localhost:3500` and internal Jobs gRPC requests at `http://localhost:4001`. + +## Register the Dapr Jobs client with dependency injection +The Dapr Jobs SDK provides an extension method to simplify the registration of the Dapr Jobs client. Before completing the dependency injection registration in `Program.cs`, add the following line: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Add anywhere between these two +builder.Services.AddDaprJobsClient(); //That's it + +var app = builder.Build(); +``` + +> Note that in today's implementation of the Jobs API, the app that schedules the job will also be the app that receives the trigger notification. In other words, you cannot schedule a trigger to run in another application. As a result, while you don't explicitly need the Dapr Jobs client to be registered in your application to schedule a trigger invocation endpoint, your endpoint will never be invoked without the same app also scheduling the job somehow (whether via this Dapr Jobs .NET SDK or an HTTP call to the sidecar). + +It's possible that you may want to provide some configuration options to the Dapr Jobs client that +should be present with each call to the sidecar such as a Dapr API token or you want to use a non-standard +HTTP or gRPC endpoint. This is possible through an overload of the register method that allows configuration of a `DaprJobsClientBuilder` instance: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(daprJobsClientBuilder => +{ + daprJobsClientBuilder.UseDaprApiToken("abc123"); + daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint +}); + +var app = builder.Build(); +``` + +Still, it's possible that whatever values you wish to inject need to be retrieved from some other source, itself registered as a dependency. There's one more overload you can use to inject an `IServiceProvider` into the configuration action method. In the following example, we register a fictional singleton that can retrieve secrets from somewhere and pass it into the configuration method for `AddDaprJobClient` so +we can retrieve our Dapr API token from somewhere else for registration here: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => +{ + var secretRetriever = serviceProvider.GetRequiredService(); + var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value; + daprJobsClientBuilder.UseDaprApiToken(daprApiToken); + + daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); +}); + +var app = builder.Build(); +``` + +## Use the Dapr Jobs client without relying on dependency injection +While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to +deal with complicated configurations, you're not required to register the `DaprJobsClient` in this way. Rather, you can also elect to create an instance of it from a `DaprJobsClientBuilder` instance as demonstrated below: + +```cs + +public class MySampleClass +{ + public void DoSomething() + { + var daprJobsClientBuilder = new DaprJobsClientBuilder(); + var daprJobsClient = daprJobsClientBuilder.Build(); + + //Do something with the `daprJobsClient` + } +} + +``` + +## Set up a endpoint to be invoked when the job is triggered + +It's easy to set up a jobs endpoint if you're at all familiar with [minimal APIs in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) as the syntax is the same between the two. + +Once dependency injection registration has been completed, configure the application the same way you would to handle mapping an HTTP request via the minimal API functionality in ASP.NET Core. Implemented as an extension method, pass the name of the job it should be responsive to and a delegate. Services can be injected into the delegate's arguments as you wish and you can optionally pass a `JobDetails` to get information about the job that has been triggered (e.g. access its scheduling setup or payload): + +```cs +//We have this from the example above +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +//Add our endpoint registration +app.MapDaprScheduledJob("myJob", (JobDetails jobDetails, ILogger logger) => { + logger.LogInformation("Received trigger invocation for '{jobName}'", "myJob"); + + //Do something... +}); + +app.Run(); +``` + +## Register the job + +Finally, we have to register the job we want scheduled. Note that from here, all SDK methods have cancellation token support and use a default token if not otherwise set. + +There are three different ways to set up a job that vary based on how you want to configure the schedule: + +### One-time job +A one-time job is exactly that; it will run at a single point in time and will not repeat. This approach requires that you select a job name and specify a time it should be triggered. + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| scheduledTime | DateTime | The point in time when the job should be run. | Yes | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +One-time jobs can be scheduled from the Dapr Jobs client as in the following example: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken) + { + var today = DateTime.UtcNow; + var threeDaysFromNow = today.AddDays(3); + + await daprJobsClient.ScheduleOneTimeJobAsync("myJobName", threeDaysFromNow, cancellationToken: cancellationToken); + } +} +``` + +### Interval-based job +An interval-based job is one that runs on a recurring loop configured as a fixed amount of time, not unlike how [reminders](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-timers-reminders/#actor-reminders) work in the Actors building block today. These jobs can be scheduled with a number of optional arguments as well: + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| interval | TimeSpan | The interval at which the job should be triggered. | Yes | +| startingFrom | DateTime | The point in time from which the job schedule should start. | No | +| repeats | int | The maximum number of times the job should be triggered. | No | +| ttl | When the job should expires and no longer trigger. | No | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +Interval-based jobs can be scheduled from the Dapr Jobs client as in the following example: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + + public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken) + { + var hourlyInterval = TimeSpan.FromHours(1); + + //Trigger the job hourly, but a maximum of 5 times + await daprJobsClient.ScheduleIntervalJobAsync("myJobName", hourlyInterval, repeats: 5), cancellationToken: cancellationToken; + } +} +``` + +### Cron-based job +A Cron-based job is scheduled using a Cron expression. This gives more calendar-based control over when the job is triggered as it can used calendar-based values in the expression. Like the other options, these jobs can be scheduled with a number of optional arguments as well: + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| cronExpression | string | The systemd Cron-like expression indicating when the job should be triggered. | Yes | +| startingFrom | DateTime | The point in time from which the job schedule should start. | No | +| repeats | int | The maximum number of times the job should be triggered. | No | +| ttl | When the job should expires and no longer trigger. | No | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +A Cron-based job can be scheduled from the Dapr Jobs client as follows: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task ScheduleCronJobAsync(CancellationToken cancellationToken) + { + //At the top of every other hour on the fifth day of the month + const string cronSchedule = "0 */2 5 * *"; + + //Don't start this until next month + var now = DateTime.UtcNow; + var oneMonthFromNow = now.AddMonths(1); + var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0); + + //Trigger the job hourly, but a maximum of 5 times + await daprJobsClient.ScheduleCronJobAsync("myJobName", cronSchedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken); + } +} +``` + +## Get details of already-scheduled job +If you know the name of an already-scheduled job, you can retrieve its metadata without waiting for it to +be triggered. The returned `JobDetails` exposes a few helpful properties for consuming the information from the Dapr Jobs API: + +- If the `Schedule` property contains a Cron expression, the `IsCronExpression` property will be true and the expression will also be available in the `CronExpression` property. +- If the `Schedule` property contains a duration value, the `IsIntervalExpression` property will instead be true and the value will be converted to a `TimeSpan` value accessible from the `Interval` property. + +This can be done by using the following: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task GetJobDetailsAsync(string jobName, CancellationToken cancellationToken) + { + var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken); + return jobDetails; + } +} +``` + +## Delete a scheduled job +To delete a scheduled job, you'll need to know its name. From there, it's as simple as calling the `DeleteJobAsync` method on the Dapr Jobs client: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken) + { + await daprJobsClient.DeleteJobAsync(jobName, cancellationToken); + } +} +``` \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md new file mode 100644 index 00000000..ac4b6b82 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md @@ -0,0 +1,171 @@ +--- +type: docs +title: "DaprJobsClient usage" +linkTitle: "DaprJobsClient usage" +weight: 5000 +description: Essential tips and advice for using DaprJobsClient +--- + +## Lifetime management + +A `DaprJobsClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Jobs API. It can be registered alongside a `DaprClient` without issue. + +It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and implements `IDisposable` to support the eager cleanup of resources. + +For best performance, create a single long-lived instance of `DaprJobsClient` and provide access to that shared instance throughout your application. `DaprJobsClient` instances are thread-safe and intended to be shared. + +Avoid creating a `DaprJobsClient` for each operation and disposing it when the operation is complete. + +## Configuring DaprJobsClient via the DaprJobsClientBuilder + +A `DaprJobsClient` can be configured by invoking methods on the `DaprJobsClientBuilder` class before calling `.Build()` to create the client itself. The settings for each `DaprJobsClient` are separate +and cannot be changed after calling `.Build()`. + +```cs +var daprJobsClient = new DaprJobsClientBuilder() + .UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars + .Build(); +``` + +The `DaprJobsClientBuilder` contains settings for: + +- The HTTP endpoint of the Dapr sidecar +- The gRPC endpoint of the Dapr sidecar +- The `JsonSerializerOptions` object used to configure JSON serialization +- The `GrpcChannelOptions` object used to configure gRPC +- The API token used to authenticate requests to the sidecar +- The factory method used to create the `HttpClient` instance used by the SDK +- The timeout used for the `HttpClient` instance when making requests to the sidecar + +The SDK will read the following environment variables to configure the default values: + +- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com` +- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com` +- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar +- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar +- `DAPR_API_TOKEN`: used to set the API token + +### Configuring gRPC channel options + +Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation). + +```cs +var daprJobsClient = new DaprJobsClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true }) + .Build(); +``` + +## Using cancellation with DaprJobsClient + +The APIs on DaprJobsClient perform asynchronous operations and accept an optional `CancellationToken` parameter. This follows a standard .NET idiom for cancellable operations. Note that when cancellation occurs, there is no guarantee that the remote endpoint stops processing the request, only that the client has stopped waiting for completion. + +When an operation is cancelled, it will throw an `OperationCancelledException`. + +## Configuring DaprJobsClient via dependency injection + +Using the built-in extension methods for registering the `DaprJobsClient` in a dependency injection container can provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances). + +There are three overloads available to give the developer the greatest flexibility in configuring the client for their scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure the `DaprJobsClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same instance as much as possible and avoid socket exhaution and other issues. + +In the first approach, there's no configuration done by the developer and the `DaprJobsClient` is configured with the default settings. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); //Registers the `DaprJobsClient` to be injected as needed + +var app = builder.Build(); +``` + +Sometimes the developer will need to configure the created client using the various configuration options detailed above. This is done through an overload that passes in the `DaprJobsClientBuiler` and exposes methods for configuring the necessary options. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(daprJobsClientBuilder => { + //Set the API token + daprJobsClientBuilder.UseDaprApiToken("abc123"); + //Specify a non-standard HTTP endpoint + daprJobsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com"); +}); + +var app = builder.Build(); +``` + +Finally, it's possible that the developer may need to retrieve information from another service in order to populate these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the last overload: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Register a fictional service that retrieves secrets from somewhere +builder.Services.AddSingleton(); + +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => { + //Retrieve an instance of the `SecretService` from the service provider + var secretService = serviceProvider.GetRequiredService(); + var daprApiToken = secretService.GetSecret("DaprApiToken").Value; + + //Configure the `DaprJobsClientBuilder` + daprJobsClientBuilder.UseDaprApiToken(daprApiToken); +}); + +var app = builder.Build(); +``` + +## Understanding payload serialization on DaprJobsClient + +While there are many methods on the `DaprClient` that automatically serialize and deserialize data using the `System.Text.Json` serializer, this SDK takes a different philosophy. Instead, the relevant methods accept an optional payload of `ReadOnlyMemory` meaning that serialization is an exercise left to the developer and is not generally handled by the SDK. + +That said, there are some helper extension methods available for each of the scheduling methods. If you know that you want to use a type that's JSON-serializable, you can use the `Schedule*WithPayloadAsync` method for each scheduling type that accepts an `object` as a payload and an optional `JsonSerializerOptions` to use when serializing the value. This will convert the value to UTF-8 encoded bytes for you as a convenience. Here's an example of what this might look like when scheduling a Cron expression: + +```cs +public sealed record Doodad (string Name, int Value); + +//... + +var doodad = new Doodad("Thing", 100); +await daprJobsClient.ScheduleCronJobWithPayloadAsync("myJob", "5 * * * *", doodad); +``` + +In the same vein, if you have a plain string value, you can use an overload of the same method to serialize a string-typed payload and the JSON serialization step will be skipped and it'll only be encoded to an array of UTF-8 encoded bytes. Here's an exampe of what this might look like when scheduling a one-time job: + +```cs +var now = DateTime.UtcNow; +var oneWeekFromNow = now.AddDays(7); +await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!"); +``` + +The `JobDetails` type returns the data as a `ReadOnlyMemory?` so the developer has the freedom to deserialize as they wish, but there are again two helper extensions included that can deserialize this to either a JSON-compatible type or a string. Both methods assume that the developer encoded the originally scheduled job (perhaps using the helper serialization methods) as these methods will not force the bytes to represent something they're not. + +To deserialize the bytes to a string, the following helper method can be used: +```cs +if (jobDetails.Payload is not null) +{ + string payloadAsString = jobDetails.Payload.DeserializeToString(); //If successful, returns a string value with the value +} +``` + +To deserialize JSON-encoded UTF-8 bytes to the corresponding type, the following helper method can be used. An overload argument is available that permits the developer to pass in their own `JsonSerializerOptions` to be applied during deserialization. + +```cs +public sealed record Doodad (string Name, int Value); + +//... + +if (jobDetails.Payload is not null) +{ + var deserializedDoodad = jobDetails.Payload.DeserializeFromJsonBytes(); +} +``` + +## Error handling + +Methods on `DaprJobsClient` will throw a `DaprJobsServiceException` if an issue is encountered between the SDK and the Jobs API service running on the Dapr sidecar. If a failure is encountered because of a poorly formatted request made to the Jobs API service through this SDK, a `DaprMalformedJobException` will be thrown. In case of illegal argument values, the appropriate standard exception will be thrown (e.g. `ArgumentOutOfRangeException` or `ArgumentNullException`) with the name of the offending argument. And for anything else, a `DaprException` will be thrown. + +The most common cases of failure will be related to: + +- Incorrect argument formatting while engaging with the Jobs API +- Transient failures such as a networking problem +- Invalid data, such as a failure to deserialize a value into a type it wasn't originally serialized from + +In any of these cases, you can examine more exception details through the `.InnerException` property. \ No newline at end of file From 8d59928edeb56ed6a1281598768bf3e14ef66061 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 03:14:39 -0500 Subject: [PATCH 60/64] Updated packages to fix build errors because of everything's dependency on Dapr.Client. Signed-off-by: Whit Waldo --- src/Dapr.Actors/Dapr.Actors.csproj | 2 +- test/Dapr.Actors.Test/Dapr.Actors.Test.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 3fb63ea2..4801fba9 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8852dd46..4ad43f22 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -10,7 +10,7 @@ all - + From 4a101a6163c3b52723c11ae429448193f63dd013 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 03:18:40 -0500 Subject: [PATCH 61/64] Forced to update these packages to 8.0.0 because of other transitive references through project references. This is unfortunate because it requires some updates for nullability changes which was hopefully out of scope for this initative. Signed-off-by: Whit Waldo --- .../Dapr.Extensions.Configuration.csproj | 4 ++-- .../Dapr.Extensions.Configuration.Test.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 71fd0153..3ff1c35d 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -1,4 +1,4 @@ - + enable @@ -18,7 +18,7 @@ - + \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 7d11d5c4..da3d4a75 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -25,7 +25,7 @@ - + \ No newline at end of file From ffdb47bc67aa5e7f85be88306f76c0e49364bbfb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 03:25:25 -0500 Subject: [PATCH 62/64] Fixed build errors introduced by the transient reference to Microsoft.Extensions.Http 8.0.0 by rolling that version back to 3.1.32 (updated Dapr.Extensions.Configuration to the latest minor release of 3.1.32 from 3.1.2 in the process). Signed-off-by: Whit Waldo --- src/Dapr.Common/Dapr.Common.csproj | 2 +- .../Dapr.Extensions.Configuration.csproj | 2 +- .../Dapr.Extensions.Configuration.Test.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index ef8c037e..0a0d00f9 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 3ff1c35d..98fdef5b 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -18,7 +18,7 @@ - + \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index da3d4a75..0bfab656 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -25,7 +25,7 @@ - + \ No newline at end of file From 8eb57f51edffa2ba7b5091a1ac8c15ea96208e34 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 16 Jul 2024 04:17:06 -0500 Subject: [PATCH 63/64] Fixed issues in the unit tests caused by over-eager fix on the Dapr API token Signed-off-by: Whit Waldo --- src/Dapr.Common/DaprGenericClientBuilder.cs | 5 +---- .../DaprJobsServiceCollectionExtensionsTest.cs | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index a23f181e..2f19fb45 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -211,10 +211,7 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) { httpClient.Timeout = this.Timeout; } - - //Set the API token in the HttpClient default headers even if it's an empty string - httpClient.DefaultRequestHeaders.Add("dapr-api-token", DaprApiToken); - + return (channel, httpClient, httpEndpoint); } diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index a0e668dc..1d7aab22 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -24,8 +24,7 @@ public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() DaprJobsGrpcClient daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; Assert.Null(daprJobClient!.apiTokenHeader); - Assert.True(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var apiTokenValue)); - Assert.Equal("", apiTokenValue.First()); + Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); } [Fact] From 2e26e1302d3c2c4550dcd0873dd6f58ed25357a9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 24 Jul 2024 20:20:30 -0500 Subject: [PATCH 64/64] Fixed incorrect unit test Signed-off-by: Whit Waldo --- .../DaprJobsServiceCollectionExtensionsTest.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs index 1d7aab22..6bdf5edc 100644 --- a/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs +++ b/test/Dapr.Jobs.Test/DaprJobsServiceCollectionExtensionsTest.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Net.Http; using Dapr.Jobs.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -59,12 +58,10 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() var serviceProvider = services.BuildServiceProvider(); var client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; - //Validate it's set on the HttpClient - var apiTokenValue = client.httpClient.DefaultRequestHeaders.GetValues("dapr-api-token").First(); - Assert.Equal("abcdef", apiTokenValue); - - //Validate it's set in the apiTokenHeader property + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); Assert.NotNull(client.apiTokenHeader); + Assert.True(client.apiTokenHeader.HasValue); Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); }