From 86a6c1c671ed941727932ac861b2679b953b0145 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:39:02 -0500 Subject: [PATCH 01/42] First pass implementing standalone pubsub client for Dapr (focusing on using streaming subscriptions). Signed-off-by: Whit Waldo --- all.sln | 30 ++ src/Dapr.Common/Dapr.Common.csproj | 19 ++ src/Dapr.Common/DaprException.cs | 55 ++++ src/Dapr.Common/DaprGenericClientBuilder.cs | 226 +++++++++++++ src/Dapr.Messaging/Dapr.Messaging.csproj | 16 + .../PublishSubscribe/ConnectionManager.cs | 69 ++++ .../DaprPublishSubscribeClient.cs | 51 +++ .../DaprPublishSubscribeGrpcClient.cs | 81 +++++ .../DaprSubscriptionOptions.cs | 18 ++ .../PublishSubscribe/MessageHandlingPolicy.cs | 21 ++ .../PublishSubscribeReceiver.cs | 303 ++++++++++++++++++ ...ishSubscribeServiceCollectionExtensions.cs | 38 +++ .../PublishSubscribe/TopicMessage.cs | 74 +++++ .../PublishSubscribe/TopicMessageAction.cs | 34 ++ 14 files changed, 1035 insertions(+) create mode 100644 src/Dapr.Common/Dapr.Common.csproj create mode 100644 src/Dapr.Common/DaprException.cs create mode 100644 src/Dapr.Common/DaprGenericClientBuilder.cs create mode 100644 src/Dapr.Messaging/Dapr.Messaging.csproj create mode 100644 src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs diff --git a/all.sln b/all.sln index 22804785..b36691fb 100644 --- a/all.sln +++ b/all.sln @@ -118,6 +118,15 @@ 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 +<<<<<<< Updated upstream +======= +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{250F0236-2014-4DD8-A688-CD25EE299FA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{CE506C30-5701-47C9-A86E-39D796B8DF35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" +EndProject +>>>>>>> Stashed changes Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -290,6 +299,21 @@ 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 +<<<<<<< Updated upstream +======= + {250F0236-2014-4DD8-A688-CD25EE299FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {250F0236-2014-4DD8-A688-CD25EE299FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {250F0236-2014-4DD8-A688-CD25EE299FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {250F0236-2014-4DD8-A688-CD25EE299FA3}.Release|Any CPU.Build.0 = Release|Any CPU + {CE506C30-5701-47C9-A86E-39D796B8DF35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE506C30-5701-47C9-A86E-39D796B8DF35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE506C30-5701-47C9-A86E-39D796B8DF35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE506C30-5701-47C9-A86E-39D796B8DF35}.Release|Any CPU.Build.0 = Release|Any CPU + {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.Build.0 = Release|Any CPU +>>>>>>> Stashed changes EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,6 +367,12 @@ 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} +<<<<<<< Updated upstream +======= + {250F0236-2014-4DD8-A688-CD25EE299FA3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CE506C30-5701-47C9-A86E-39D796B8DF35} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} +>>>>>>> Stashed changes EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj new file mode 100644 index 00000000..910f2af9 --- /dev/null +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dapr.Common/DaprException.cs b/src/Dapr.Common/DaprException.cs new file mode 100644 index 00000000..e63159b4 --- /dev/null +++ b/src/Dapr.Common/DaprException.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------ +// 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.Serialization; + +namespace Dapr +{ + /// + /// The base type of exceptions thrown by the Dapr .NET SDK. + /// + [Serializable] + public class DaprException : Exception + { + /// + /// Initializes a new instance of with the provided . + /// + /// The exception message. + public DaprException(string message) + : base(message) + { + } + /// + /// Initializes a new instance of with the provided + /// and . + /// + /// The exception message. + /// The inner exception. + public DaprException(string message, Exception innerException) + : base(message, innerException) + { + } + /// + /// Initializes a new instance of the class with a specified context. + /// + /// The object that contains serialized object data of the exception being thrown. + /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to GetObjectData +#endif + protected DaprException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs new file mode 100644 index 00000000..4338a0f9 --- /dev/null +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -0,0 +1,226 @@ +// ------------------------------------------------------------------------ +// 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. + /// + protected 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 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. + /// + /// + /// 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 out the inner DaprClient that provides the core shape of the + /// runtime gRPC client used by the consuming package. + /// + /// + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + { + 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; + } + + 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.Messaging/Dapr.Messaging.csproj b/src/Dapr.Messaging/Dapr.Messaging.csproj new file mode 100644 index 00000000..a6701549 --- /dev/null +++ b/src/Dapr.Messaging/Dapr.Messaging.csproj @@ -0,0 +1,16 @@ + + + + This package contains the reference assemblies for developing messaging services using Dapr. + enable + enable + + + + + + + + + + \ No newline at end of file diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs new file mode 100644 index 00000000..81a39aa5 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------ +// 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 Grpc.Core; +using C = Dapr.AppCallback.Autogen.Grpc.v1; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Maintains access to +/// +internal sealed class ConnectionManager : IAsyncDisposable +{ + /// + /// A reference to the DaprClient instance. + /// + private readonly P.Dapr.DaprClient _client; + /// + /// Used to ensure thread-safe operations against the stream. + /// + private readonly SemaphoreSlim _semaphore = new(1, 1); + /// + /// The stream connection between this instance and the Dapr sidecar. + /// + private AsyncDuplexStreamingCall? + _stream; + + public ConnectionManager(P.Dapr.DaprClient client) + { + _client = client; + } + + public async + Task> + GetStreamAsync(CancellationToken cancellationToken) + { + await _semaphore.WaitAsync(cancellationToken); + + try + { + return _stream ??= _client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); + } + finally + { + _semaphore.Release(); + } + } + + public async ValueTask DisposeAsync() + { + if (_stream is not null) + { + await _stream.RequestStream.CompleteAsync(); + } + + _semaphore.Dispose(); + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs new file mode 100644 index 00000000..b096bc2e --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; + +/// +/// +/// +public abstract class DaprPublishSubscribeClient +{ + /// + /// Dynamically subscribes to a Publish/Subscribe component and topic. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Configuration options. + /// Cancellation token. + /// + public abstract IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken); + + /// + /// Used to acknowledge receipt of a message and indicate how the Dapr sidecar should handle it. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// The identifier of the message to apply the action to. + /// Indicates the action to perform on the message. + /// Cancellation token. + /// + public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, + TopicMessageAction messageAction, CancellationToken cancellationToken); + + /// + /// Unsubscribes a streaming subscription for the specified Publish/Subscribe component and topic. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Cancellation token. + /// + public abstract Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken); +} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs new file mode 100644 index 00000000..2dbef602 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; + +/// +/// +/// +public sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient +{ + private readonly PublishSubscribeReceiverBuilder _builder; + + private readonly Dictionary<(string, string), PublishSubscribeReceiver> _clients = + new Dictionary<(string, string), PublishSubscribeReceiver>(); + + /// + /// + /// + /// + public DaprPublishSubscribeGrpcClient(PublishSubscribeReceiverBuilder builder) + { + _builder = builder; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, + CancellationToken cancellationToken) + { + var receiver = _builder.Build(pubsubName, topicName, options); + _clients[(pubsubName, topicName)] = receiver; + + return receiver.SubscribeAsync(cancellationToken); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public override async Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, + TopicMessageAction messageAction, CancellationToken cancellationToken) + { + if (!_clients.TryGetValue((pubsubName, topicName), out var receiver)) + { + throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); + } + + await receiver.AcknowledgeMessageAsync(messageId, messageAction, cancellationToken); + } + + public override async Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken) + { + if (!_clients.TryGetValue((pubsubName, topicName), out var receiver)) + { + throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); + } + + await receiver.DisposeAsync(); + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs new file mode 100644 index 00000000..a081d521 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs @@ -0,0 +1,18 @@ +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Options used to configure the dynamic Dapr subscription. +/// +/// Describes the policy to take on messages that have not been acknowledged within the timeout period. +public sealed record DaprSubscriptionOptions(MessageHandlingPolicy MessageHandlingPolicy) +{ + /// + /// Subscription metadata. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + + /// + /// The optional name of the dead-letter topic to send messages to. + /// + public string? DeadLetterTopic { get; init; } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs new file mode 100644 index 00000000..c4393aa6 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; + +/// +/// Defines the policy for handling streaming message subscriptions, including retry logic and timeout settings. +/// +/// The duration to wait before timing out a message handling operation. +/// The default action to take when a message handling operation times out. +public sealed record MessageHandlingPolicy(TimeSpan TimeoutDuration, TopicMessageAction DefaultMessageAction); diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs new file mode 100644 index 00000000..ddb567f2 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -0,0 +1,303 @@ +// ------------------------------------------------------------------------ +// 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; +using System.Threading.Channels; +using Dapr.Client.Autogen.Grpc.v1; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using C = Dapr.AppCallback.Autogen.Grpc.v1; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// A thread-safe implementation of a receiver for messages from a specified Dapr publish/subscribe component and +/// topic. +/// +internal sealed class PublishSubscribeReceiver : IAsyncDisposable +{ + /// + /// Maintains the stream connection to the Dapr sidecar for the subscription. + /// + private readonly ConnectionManager connectionManager; + /// + /// Used for logging purposes. + /// + private readonly ILogger? logger; + + /// + /// The name of the Dapr pubsub component. + /// + private readonly string pubsubName; + /// + /// The name of the topic to subscribe to. + /// + private readonly string topicName; + /// + /// Options allowing the behavior of the receiver to be configured. + /// + private readonly DaprSubscriptionOptions options; + /// + /// A channel used to decouple the messages received from the sidecar to their consumption. + /// + private readonly Channel channel = Channel.CreateUnbounded(); + /// + /// A collection of used to signal acknowledgement of received messages so a status + /// can be sent back to the sidecar indicating what behavior should happen to each. + /// + private readonly Dictionary> acknowledgementTasks = new(); + /// + /// A semaphore used to ensure thread-safe access to the dictionary. + /// + private readonly SemaphoreSlim acknowledgementSemaphore = new(1, 1); + + /// + /// Constructs a new instance of a instance. + /// + /// The name of the Dapr pubsub component. + /// The name of the topic to subscribe to. + /// Options allowing the behavior of the receiver to be configured. + /// + /// Used to create the logger instance. + internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubscriptionOptions options, P.Dapr.DaprClient daprClient, ILoggerFactory? loggerFactory) + { + this.pubsubName = pubsubName; + this.topicName = topicName; + this.options = options; + connectionManager = new ConnectionManager(daprClient); + + logger = loggerFactory?.CreateLogger() ?? + NullLoggerFactory.Instance.CreateLogger(); + } + + /// + /// Dynamically subscribes to messages on a PubSub topic provided by the Dapr sidecar. + /// + /// Cancellation token. + /// An containing messages provided by the sidecar. + public IAsyncEnumerable SubscribeAsync(CancellationToken cancellationToken) + { + _ = FetchDataFromSidecar(channel.Writer, cancellationToken); + return ReadMessagesFromChannelAsync(channel.Reader, cancellationToken); + } + + /// + /// Specifies the action that should be taken on the message after processing it. + /// + /// The identifier of the message to acknowledge. + /// The action to take on the message. + /// Cancellation token. + public async Task AcknowledgeMessageAsync(string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken) + { + var stream = await connectionManager.GetStreamAsync(cancellationToken); + await stream.RequestStream.WriteAsync(new SubscribeTopicEventsRequestAlpha1 + { + EventResponse = new SubscribeTopicEventsResponseAlpha1 + { + Id = messageId, + Status = new C.TopicEventResponse + { + Status = messageAction switch + { + TopicMessageAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicMessageAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus.Success, + TopicMessageAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new ArgumentOutOfRangeException(nameof(messageAction), messageAction, null) + } + } + } + }, cancellationToken); + + await acknowledgementSemaphore.WaitAsync(cancellationToken); + try + { + if (acknowledgementTasks.TryGetValue(messageId, out var tcs)) + { + tcs.SetResult(true); + acknowledgementTasks.Remove(messageId); + } + } + finally + { + acknowledgementSemaphore.Release(); + } + } + + /// + /// Reads the topic messages returned from the Dapr sidecar. + /// + /// The channel reader instance. + /// Cancellation token. + /// An containing the received messages from the Dapr sidecar. + private async IAsyncEnumerable ReadMessagesFromChannelAsync(ChannelReader reader, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (await reader.WaitToReadAsync(cancellationToken)) + { + while (reader.TryRead(out var message)) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(options!.MessageHandlingPolicy.TimeoutDuration); + + yield return message; + + try + { + //Wait for the message to be acknowledged + await WaitForAcknowledgementAsync(message.Id, cts.Token); + } + catch (OperationCanceledException) when (cts.Token.IsCancellationRequested) + { + //Handle the acknowledgement timeout using the specified default policy + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultMessageAction, + cancellationToken); + } + } + } + } + + /// + /// Sets up a timeout for message acknowledgement before the configured default action is applied + /// to each message. + /// + /// The identifier of the topic message. + /// Cancellation token. + /// + private async Task WaitForAcknowledgementAsync(string messageId, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await acknowledgementSemaphore.WaitAsync(cancellationToken); + + try + { + acknowledgementTasks[messageId] = tcs; + } + finally + { + acknowledgementSemaphore.Release(); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); + + await using (cancellationToken.Register(() => tcs.TrySetCanceled())) + { + await tcs.Task; + } + } + + /// + /// Retrieves the subscription stream data from the Dapr sidecar. + /// + /// The channel writer instance. + /// Cancellation token. + private async Task FetchDataFromSidecar(ChannelWriter channelWriter, CancellationToken cancellationToken) + { + try + { + var stream = await connectionManager.GetStreamAsync(cancellationToken); + var initialRequest = new P.SubscribeTopicEventsInitialRequestAlpha1() + { + PubsubName = pubsubName, + DeadLetterTopic = options?.DeadLetterTopic ?? string.Empty, + Topic = topicName + }; + + if (options?.Metadata.Count > 0) + { + foreach (var (key, value) in options.Metadata) + { + initialRequest.Metadata.Add(key, value); + } + } + + await stream.RequestStream.WriteAsync(new SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); + + await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) + { + var message = new TopicMessage + { + Id = response.Id, + Source = response.Source, + Type = response.Type, + SpecVersion = response.SpecVersion, + DataContentType = response.DataContentType, + Data = response.Data.Memory, + Topic = response.Topic, + PubSubName = response.PubsubName, + Path = response.Path, + Extensions = response.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) + }; + + await channelWriter.WriteAsync(message, cancellationToken); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && + cancellationToken.IsCancellationRequested) + { + //Ignore a remote cancellation due to our own cancellation + } + finally + { + channel.Writer.Complete(); + } + } + + /// + /// Disposes the various resources associated with the instance. + /// + /// + public async ValueTask DisposeAsync() + { + await connectionManager.DisposeAsync(); + channel.Writer.Complete(); + acknowledgementSemaphore.Dispose(); + } +} + +/// +/// +/// +public sealed class PublishSubscribeReceiverBuilder +{ + private readonly ILoggerFactory? loggerFactory; + private readonly P.Dapr.DaprClient daprClient; + + /// + /// + /// + /// + /// + public PublishSubscribeReceiverBuilder(ILoggerFactory? loggerFactory, P.Dapr.DaprClient daprClient) + { + this.loggerFactory = loggerFactory; + this.daprClient = daprClient; + } + + /// + /// + /// + /// + /// + /// + /// + public PublishSubscribeReceiver Build(string pubsubName, string topicName, + DaprSubscriptionOptions options) => + new(pubsubName, topicName, options, daprClient, loggerFactory); +} diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs new file mode 100644 index 00000000..f12e056c --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.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. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Contains extension methods for using Dapr Publish/Subscribe with dependency injection. +/// +public static class PublishSubscribeServiceCollectionExtensions +{ + public static IServiceCollection AddDaprPubSub(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + } + + public static IServiceCollection AddDaprPubSub(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.TryAddSingleton(serviceProvider => + { + + }); + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs new file mode 100644 index 00000000..be28ba45 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------ +// 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.WellKnownTypes; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// A message retrieved from a Dapr publish/subscribe topic. +/// +public sealed record TopicMessage +{ + /// + /// The unique identifier of the topic message. + /// + public string Id { get; init; } = default!; + + /// + /// Identifies the context in which an event happened, such as 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. + /// + public string Source { get; init; } = default!; + + /// + /// The type of event related to the originating occurrence. + /// + public string Type { get; init; } = default!; + + /// + /// The spec version of the CloudEvents specification. + /// + public string SpecVersion { get; init; } = default!; + + /// + /// The content type of the data. + /// + public string DataContentType { get; init; } = default!; + + /// + /// The content of the event. + /// + public ReadOnlyMemory Data { get; init; } + + /// + /// The name of the topic. + /// + public string Topic { get; init; } = default!; + + /// + /// The name of the Dapr publish/subscribe component. + /// + public string PubSubName { get; init; } = default!; + + /// + /// The matching path from the topic subscription/routes (if specified) for this event. + /// + public string? Path { get; init; } + + /// + /// A map of additional custom properties sent to the app. These are considered to be cloud event extensions. + /// + public Dictionary Extensions { get; init; } = new(); +} diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs new file mode 100644 index 00000000..23e7dee6 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; + +/// +/// Describes the various actions that can be taken on a topic message. +/// +public enum TopicMessageAction +{ + /// + /// Indicates the message was processed successfully and should be deleted from the pub/sub topic. + /// + Success, + /// + /// Indicates a failure while processing the message and that the message should be resent for a retry. + /// + Retry, + /// + /// Indicates a failure while processing the message and that the message should be dropped or sent to the + /// dead-letter topic if specified. + /// + Drop +} From 849a82875798a5b5cb3a276e2cc02482a618297f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:42:38 -0500 Subject: [PATCH 02/42] Added missing copyright notice Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprSubscriptionOptions.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs index a081d521..ae5d17aa 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs @@ -1,4 +1,17 @@ -namespace Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; /// /// Options used to configure the dynamic Dapr subscription. @@ -12,7 +25,7 @@ public sealed record DaprSubscriptionOptions(MessageHandlingPolicy MessageHandli public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); /// - /// The optional name of the dead-letter topic to send messages to. + /// The optional name of the dead-letter topic to send unprocessed messages to. /// public string? DeadLetterTopic { get; init; } } From 2d73e699a84c17d78af4cc9705b743cc68399f88 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:49:05 -0500 Subject: [PATCH 03/42] Fleshing out comments Signed-off-by: Whit Waldo --- .../DaprPublishSubscribeClient.cs | 6 +-- .../DaprPublishSubscribeGrpcClient.cs | 41 +++++++++++-------- .../PublishSubscribeReceiver.cs | 2 - 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index b096bc2e..46f93314 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -14,7 +14,7 @@ namespace Dapr.Messaging.PublishSubscribe; /// -/// +/// The base implementation of a Dapr pub/sub client. /// public abstract class DaprPublishSubscribeClient { @@ -25,7 +25,7 @@ public abstract class DaprPublishSubscribeClient /// The name of the topic to subscribe to. /// Configuration options. /// Cancellation token. - /// + /// An containing the various messages returned by the subscription. public abstract IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken); /// @@ -36,7 +36,6 @@ public abstract class DaprPublishSubscribeClient /// The identifier of the message to apply the action to. /// Indicates the action to perform on the message. /// Cancellation token. - /// public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken); @@ -46,6 +45,5 @@ public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName /// The name of the Publish/Subscribe component. /// The name of the topic to subscribe to. /// Cancellation token. - /// public abstract Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken); } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 2dbef602..f7db254a 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -18,28 +18,32 @@ namespace Dapr.Messaging.PublishSubscribe; /// public sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { + /// + /// Maintains a reference to the receiver builder factory. + /// private readonly PublishSubscribeReceiverBuilder _builder; - + /// + /// The various receiver clients created for each combination of Dapr pubsub component and topic name. + /// private readonly Dictionary<(string, string), PublishSubscribeReceiver> _clients = new Dictionary<(string, string), PublishSubscribeReceiver>(); /// - /// + /// Creates a new instance of a /// - /// public DaprPublishSubscribeGrpcClient(PublishSubscribeReceiverBuilder builder) { _builder = builder; } /// - /// + /// Dynamically subscribes to a Publish/Subscribe component and topic. /// - /// - /// - /// - /// - /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Configuration options. + /// Cancellation token. + /// An containing the various messages returned by the subscription. public override IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken) { @@ -50,14 +54,13 @@ public override IAsyncEnumerable SubscribeAsync(string pubsubName, } /// - /// + /// Used to acknowledge receipt of a message and indicate how the Dapr sidecar should handle it. /// - /// - /// - /// - /// - /// - /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// The identifier of the message to apply the action to. + /// Indicates the action to perform on the message. + /// Cancellation token. public override async Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken) { @@ -69,6 +72,12 @@ public override async Task AcknowledgeMessageAsync(string pubsubName, string top await receiver.AcknowledgeMessageAsync(messageId, messageAction, cancellationToken); } + /// + /// Unsubscribes a streaming subscription for the specified Publish/Subscribe component and topic. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Cancellation token. public override async Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken) { if (!_clients.TryGetValue((pubsubName, topicName), out var receiver)) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index ddb567f2..66febd46 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,9 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Runtime.CompilerServices; using System.Threading.Channels; -using Dapr.Client.Autogen.Grpc.v1; using Grpc.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; From bed6e4de538e3f9a8f8828981cbf2c934ce1b53d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:49:39 -0500 Subject: [PATCH 04/42] Added Dapr.Protos project to solution as a central place to put the Dapr prototypes Signed-off-by: Whit Waldo --- all.sln | 20 +- properties/IsExternalInit.cs | 2 +- src/Dapr.Protos/Dapr.Protos.csproj | 22 + .../Protos/dapr/proto/common/v1/common.proto | 160 +++ .../dapr/proto/dapr/v1/appcallback.proto | 343 +++++ .../Protos/dapr/proto/dapr/v1/dapr.proto | 1234 +++++++++++++++++ 6 files changed, 1769 insertions(+), 12 deletions(-) create mode 100644 src/Dapr.Protos/Dapr.Protos.csproj create mode 100644 src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto create mode 100644 src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto create mode 100644 src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto diff --git a/all.sln b/all.sln index b36691fb..a67eef51 100644 --- a/all.sln +++ b/all.sln @@ -118,15 +118,14 @@ 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 -<<<<<<< Updated upstream -======= -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{250F0236-2014-4DD8-A688-CD25EE299FA3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{250F0236-2014-4DD8-A688-CD25EE299FA3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{CE506C30-5701-47C9-A86E-39D796B8DF35}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{DF1B9FE2-9DBF-459F-9798-08951717583E}" EndProject ->>>>>>> Stashed changes Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -299,8 +298,6 @@ 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 -<<<<<<< Updated upstream -======= {250F0236-2014-4DD8-A688-CD25EE299FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {250F0236-2014-4DD8-A688-CD25EE299FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {250F0236-2014-4DD8-A688-CD25EE299FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -313,7 +310,10 @@ Global {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.Build.0 = Release|Any CPU ->>>>>>> Stashed changes + {DF1B9FE2-9DBF-459F-9798-08951717583E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF1B9FE2-9DBF-459F-9798-08951717583E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF1B9FE2-9DBF-459F-9798-08951717583E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF1B9FE2-9DBF-459F-9798-08951717583E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -367,12 +367,10 @@ 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} -<<<<<<< Updated upstream -======= {250F0236-2014-4DD8-A688-CD25EE299FA3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CE506C30-5701-47C9-A86E-39D796B8DF35} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} ->>>>>>> Stashed changes + {DF1B9FE2-9DBF-459F-9798-08951717583E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} 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.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj new file mode 100644 index 00000000..15041a82 --- /dev/null +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -0,0 +1,22 @@ + + + + enable + enable + This package contains the reference protos used by develop services using Dapr. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto new file mode 100644 index 00000000..4acf9159 --- /dev/null +++ b/src/Dapr.Protos/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; +} \ No newline at end of file diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto new file mode 100644 index 00000000..a8604036 --- /dev/null +++ b/src/Dapr.Protos/Protos/dapr/proto/dapr/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 {} \ No newline at end of file diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto new file mode 100644 index 00000000..f702491c --- /dev/null +++ b/src/Dapr.Protos/Protos/dapr/proto/dapr/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/dapr/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 From bdc5a036f19c5b66f61386392061be2a999111f1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:49:58 -0500 Subject: [PATCH 05/42] Added Nuget properties to project Signed-off-by: Whit Waldo --- src/Dapr.Messaging/Dapr.Messaging.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Dapr.Messaging/Dapr.Messaging.csproj b/src/Dapr.Messaging/Dapr.Messaging.csproj index a6701549..9380c97f 100644 --- a/src/Dapr.Messaging/Dapr.Messaging.csproj +++ b/src/Dapr.Messaging/Dapr.Messaging.csproj @@ -4,6 +4,10 @@ This package contains the reference assemblies for developing messaging services using Dapr. enable enable + Dapr.Messaging + Dapr Messaging SDK + Dapr Messaging SDK for building applications that utilize messaging components. + alpha From 1f43315cca43b093cd7e8f4334e09b5367aa9990 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 08:54:26 -0500 Subject: [PATCH 06/42] Restored missing using Signed-off-by: Whit Waldo --- src/Dapr.Messaging/Dapr.Messaging.csproj | 1 + .../PublishSubscribeReceiver.cs | 21 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Dapr.Messaging/Dapr.Messaging.csproj b/src/Dapr.Messaging/Dapr.Messaging.csproj index 9380c97f..6dc7dca0 100644 --- a/src/Dapr.Messaging/Dapr.Messaging.csproj +++ b/src/Dapr.Messaging/Dapr.Messaging.csproj @@ -1,6 +1,7 @@  + net6;net8 This package contains the reference assemblies for developing messaging services using Dapr. enable enable diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 66febd46..cbec3c5f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Runtime.CompilerServices; using System.Threading.Channels; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -100,9 +101,9 @@ public IAsyncEnumerable SubscribeAsync(CancellationToken cancellat public async Task AcknowledgeMessageAsync(string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken) { var stream = await connectionManager.GetStreamAsync(cancellationToken); - await stream.RequestStream.WriteAsync(new SubscribeTopicEventsRequestAlpha1 + await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 { - EventResponse = new SubscribeTopicEventsResponseAlpha1 + EventResponse = new P.SubscribeTopicEventsResponseAlpha1 { Id = messageId, Status = new C.TopicEventResponse @@ -221,7 +222,7 @@ private async Task FetchDataFromSidecar(ChannelWriter channelWrite } } - await stream.RequestStream.WriteAsync(new SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); + await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) { @@ -270,7 +271,7 @@ public async ValueTask DisposeAsync() } /// -/// +/// Factory method used to build an instance of a . /// public sealed class PublishSubscribeReceiverBuilder { @@ -278,10 +279,8 @@ public sealed class PublishSubscribeReceiverBuilder private readonly P.Dapr.DaprClient daprClient; /// - /// + /// Initializes a new instance of a . /// - /// - /// public PublishSubscribeReceiverBuilder(ILoggerFactory? loggerFactory, P.Dapr.DaprClient daprClient) { this.loggerFactory = loggerFactory; @@ -289,11 +288,11 @@ public PublishSubscribeReceiverBuilder(ILoggerFactory? loggerFactory, P.Dapr.Dap } /// - /// + /// Builds an instance of a . /// - /// - /// - /// + /// The name of the Dapr pub/sub component. + /// The name of the topic to subscribe to. + /// Configuration options. /// public PublishSubscribeReceiver Build(string pubsubName, string topicName, DaprSubscriptionOptions options) => From 0bab52caa4231df8e8c216cd83970419e82d9e8a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 09:59:16 -0500 Subject: [PATCH 07/42] Updated to use the standard Dapr client builder, added DI registration extension methods. Signed-off-by: Whit Waldo --- all.sln | 9 +- src/Dapr.Common/Dapr.Common.csproj | 1 - src/Dapr.Messaging/Dapr.Messaging.csproj | 2 +- .../DaprPublishSubscribeClient.cs | 15 +++ ...s => DaprPublishSubscribeClientBuilder.cs} | 30 ++--- .../DaprPublishSubscribeGrpcClient.cs | 20 ++-- ...ishSubscribeServiceCollectionExtensions.cs | 104 ++++++++++++++++++ .../PublishSubscribeReceiver.cs | 41 +------ 8 files changed, 148 insertions(+), 74 deletions(-) rename src/Dapr.Messaging/PublishSubscribe/{PublishSubscribeServiceCollectionExtensions.cs => DaprPublishSubscribeClientBuilder.cs} (51%) create mode 100644 src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs diff --git a/all.sln b/all.sln index a67eef51..88ebf04f 100644 --- a/all.sln +++ b/all.sln @@ -120,12 +120,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Cl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{250F0236-2014-4DD8-A688-CD25EE299FA3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{CE506C30-5701-47C9-A86E-39D796B8DF35}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{CE506C30-5701-47C9-A86E-39D796B8DF35}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{DF1B9FE2-9DBF-459F-9798-08951717583E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -310,10 +308,6 @@ Global {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.Build.0 = Release|Any CPU - {DF1B9FE2-9DBF-459F-9798-08951717583E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF1B9FE2-9DBF-459F-9798-08951717583E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF1B9FE2-9DBF-459F-9798-08951717583E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF1B9FE2-9DBF-459F-9798-08951717583E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -370,7 +364,6 @@ Global {250F0236-2014-4DD8-A688-CD25EE299FA3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CE506C30-5701-47C9-A86E-39D796B8DF35} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {DF1B9FE2-9DBF-459F-9798-08951717583E} = {27C5D71D-0721-4221-9286-B94AB07B58CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index 910f2af9..c0fb179b 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -1,7 +1,6 @@  - net8.0 enable enable diff --git a/src/Dapr.Messaging/Dapr.Messaging.csproj b/src/Dapr.Messaging/Dapr.Messaging.csproj index 6dc7dca0..19d69d10 100644 --- a/src/Dapr.Messaging/Dapr.Messaging.csproj +++ b/src/Dapr.Messaging/Dapr.Messaging.csproj @@ -1,7 +1,6 @@  - net6;net8 This package contains the reference assemblies for developing messaging services using Dapr. enable enable @@ -16,6 +15,7 @@ + \ No newline at end of file diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 46f93314..8fd5b1f5 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -46,4 +46,19 @@ public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName /// The name of the topic to subscribe to. /// Cancellation token. public abstract Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken); + + /// + /// Gets the Dapr API token header for the given token value. + /// + /// The value of the Dapr API token. + /// + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) + { + return null; + } + + return new KeyValuePair("dapr-api-token", apiToken); + } } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs similarity index 51% rename from src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs rename to src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index f12e056c..11bbeeda 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -11,28 +11,28 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Messaging.PublishSubscribe; /// -/// Contains extension methods for using Dapr Publish/Subscribe with dependency injection. +/// Builds a . /// -public static class PublishSubscribeServiceCollectionExtensions +public sealed class DaprPublishSubscribeClientBuilder : DaprGenericClientBuilder { - public static IServiceCollection AddDaprPubSub(this IServiceCollection services) + /// + /// 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 override DaprPublishSubscribeClient Build() { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - } - - public static IServiceCollection AddDaprPubSub(this IServiceCollection services, Action configure) - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); + var daprClientDependencies = this.BuildDaprClientDependencies(); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - services.TryAddSingleton(serviceProvider => - { - - }); + return new DaprPublishSubscribeGrpcClient(client); } } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index f7db254a..c544ff8c 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -11,29 +11,31 @@ // limitations under the License. // ------------------------------------------------------------------------ +using P = Dapr.Client.Autogen.Grpc.v1.Dapr; + namespace Dapr.Messaging.PublishSubscribe; /// -/// +/// A client for interacting with the Dapr endpoints. /// -public sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient +internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - /// - /// Maintains a reference to the receiver builder factory. - /// - private readonly PublishSubscribeReceiverBuilder _builder; /// /// The various receiver clients created for each combination of Dapr pubsub component and topic name. /// private readonly Dictionary<(string, string), PublishSubscribeReceiver> _clients = new Dictionary<(string, string), PublishSubscribeReceiver>(); + /// + /// The Dapr client. + /// + private readonly P.DaprClient _daprClient; /// /// Creates a new instance of a /// - public DaprPublishSubscribeGrpcClient(PublishSubscribeReceiverBuilder builder) + public DaprPublishSubscribeGrpcClient(P.DaprClient client) { - _builder = builder; + _daprClient = client; } /// @@ -47,7 +49,7 @@ public DaprPublishSubscribeGrpcClient(PublishSubscribeReceiverBuilder builder) public override IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken) { - var receiver = _builder.Build(pubsubName, topicName, options); + var receiver = new PublishSubscribeReceiver(pubsubName, topicName, options, _daprClient); _clients[(pubsubName, topicName)] = receiver; return receiver.SubscribeAsync(cancellationToken); diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs new file mode 100644 index 00000000..fadcd73d --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe.Extensions; + +/// +/// Contains extension methods for using Dapr Publish/Subscribe with dependency injection. +/// +public static class PublishSubscribeServiceCollectionExtensions +{ + /// + /// Adds Dapr Publish/Subscribe support to the service collection. + /// + /// The . + /// + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Register the IHttpClientFactory implementation + services.AddHttpClient(); + + services.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprPublishSubscribeClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + return builder.Build(); + }); + + return services; + } + + /// + /// Adds Dapr Publish/Subscribe support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the . + /// + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Register the IHttpClientFactory implementation + services.AddHttpClient(); + + services.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprPublishSubscribeClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(builder); + + return builder.Build(); + }); + + return services; + } + + /// + /// Adds Dapr Publish/Subscribe support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Register the IHttpClientFactory implementation + services.AddHttpClient(); + + services.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprPublishSubscribeClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + + return services; + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index cbec3c5f..c59bfad1 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -14,8 +14,6 @@ using System.Runtime.CompilerServices; using System.Threading.Channels; using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using C = Dapr.AppCallback.Autogen.Grpc.v1; using P = Dapr.Client.Autogen.Grpc.v1; @@ -31,10 +29,6 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// Maintains the stream connection to the Dapr sidecar for the subscription. /// private readonly ConnectionManager connectionManager; - /// - /// Used for logging purposes. - /// - private readonly ILogger? logger; /// /// The name of the Dapr pubsub component. @@ -69,16 +63,12 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// The name of the topic to subscribe to. /// Options allowing the behavior of the receiver to be configured. /// - /// Used to create the logger instance. - internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubscriptionOptions options, P.Dapr.DaprClient daprClient, ILoggerFactory? loggerFactory) + internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubscriptionOptions options, P.Dapr.DaprClient daprClient) { this.pubsubName = pubsubName; this.topicName = topicName; this.options = options; connectionManager = new ConnectionManager(daprClient); - - logger = loggerFactory?.CreateLogger() ?? - NullLoggerFactory.Instance.CreateLogger(); } /// @@ -269,32 +259,3 @@ public async ValueTask DisposeAsync() acknowledgementSemaphore.Dispose(); } } - -/// -/// Factory method used to build an instance of a . -/// -public sealed class PublishSubscribeReceiverBuilder -{ - private readonly ILoggerFactory? loggerFactory; - private readonly P.Dapr.DaprClient daprClient; - - /// - /// Initializes a new instance of a . - /// - public PublishSubscribeReceiverBuilder(ILoggerFactory? loggerFactory, P.Dapr.DaprClient daprClient) - { - this.loggerFactory = loggerFactory; - this.daprClient = daprClient; - } - - /// - /// Builds an instance of a . - /// - /// The name of the Dapr pub/sub component. - /// The name of the topic to subscribe to. - /// Configuration options. - /// - public PublishSubscribeReceiver Build(string pubsubName, string topicName, - DaprSubscriptionOptions options) => - new(pubsubName, topicName, options, daprClient, loggerFactory); -} From 90fe7b02e0383dcdd98b8feacb97cc27e8d8e19d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 10:01:33 -0500 Subject: [PATCH 08/42] Minor tweaks to conform to solution style Signed-off-by: Whit Waldo --- .../DaprPublishSubscribeClientBuilder.cs | 2 +- .../DaprPublishSubscribeGrpcClient.cs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index 11bbeeda..9f7a6d54 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -30,7 +30,7 @@ public sealed class DaprPublishSubscribeClientBuilder : DaprGenericClientBuilder /// public override DaprPublishSubscribeClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(); + var daprClientDependencies = BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); return new DaprPublishSubscribeGrpcClient(client); diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index c544ff8c..50e67960 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -23,19 +23,18 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien /// /// The various receiver clients created for each combination of Dapr pubsub component and topic name. /// - private readonly Dictionary<(string, string), PublishSubscribeReceiver> _clients = - new Dictionary<(string, string), PublishSubscribeReceiver>(); + private readonly Dictionary<(string, string), PublishSubscribeReceiver> clients = new(); /// /// The Dapr client. /// - private readonly P.DaprClient _daprClient; + private readonly P.DaprClient daprClient; /// /// Creates a new instance of a /// public DaprPublishSubscribeGrpcClient(P.DaprClient client) { - _daprClient = client; + daprClient = client; } /// @@ -49,8 +48,8 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) public override IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken) { - var receiver = new PublishSubscribeReceiver(pubsubName, topicName, options, _daprClient); - _clients[(pubsubName, topicName)] = receiver; + var receiver = new PublishSubscribeReceiver(pubsubName, topicName, options, daprClient); + clients[(pubsubName, topicName)] = receiver; return receiver.SubscribeAsync(cancellationToken); } @@ -66,7 +65,7 @@ public override IAsyncEnumerable SubscribeAsync(string pubsubName, public override async Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken) { - if (!_clients.TryGetValue((pubsubName, topicName), out var receiver)) + if (!clients.TryGetValue((pubsubName, topicName), out var receiver)) { throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); } @@ -82,7 +81,7 @@ public override async Task AcknowledgeMessageAsync(string pubsubName, string top /// Cancellation token. public override async Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken) { - if (!_clients.TryGetValue((pubsubName, topicName), out var receiver)) + if (!clients.TryGetValue((pubsubName, topicName), out var receiver)) { throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); } From 6d5ed77217ee76483ccb508f0220786f2bf1c57f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 10:37:42 -0500 Subject: [PATCH 09/42] Tweak to README to include additional package names Signed-off-by: Whit Waldo --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 948516fe..ca491fef 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,10 @@ This repo builds the following packages: - Dapr.AspNetCore - Dapr.Actors - Dapr.Actors.AspNetCore +- Dapr.Common - Dapr.Extensions.Configuration +- Dapr.Messaging +- Dapr.Protos - Dapr.Workflow ### Prerequisites From 32ce9bef2c0e7c6ee67ca74a025fc905cde07fbe Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 4 Sep 2024 10:54:35 -0500 Subject: [PATCH 10/42] Added test project for Dapr.Messaging Signed-off-by: Whit Waldo --- all.sln | 7 ++++ src/Dapr.Messaging/AssemblyInfo.cs | 3 ++ .../DaprPublishSubscribeGrpcClient.cs | 3 ++ .../Dapr.Messaging.Test.csproj | 36 +++++++++++++++++++ test/Dapr.Messaging.Test/Protos/test.proto | 32 +++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 src/Dapr.Messaging/AssemblyInfo.cs create mode 100644 test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj create mode 100644 test/Dapr.Messaging.Test/Protos/test.proto diff --git a/all.sln b/all.sln index 88ebf04f..256f1a80 100644 --- a/all.sln +++ b/all.sln @@ -124,6 +124,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Protos", "src\Dapr.Pro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{93C6ABAF-F4B7-4CA2-8734-565EF847668A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -308,6 +310,10 @@ Global {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}.Release|Any CPU.Build.0 = Release|Any CPU + {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -364,6 +370,7 @@ Global {250F0236-2014-4DD8-A688-CD25EE299FA3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CE506C30-5701-47C9-A86E-39D796B8DF35} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {93C6ABAF-F4B7-4CA2-8734-565EF847668A} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs new file mode 100644 index 00000000..f14d0926 --- /dev/null +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Messaging.Test")] diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 50e67960..3f7b2a12 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -29,6 +29,9 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien /// private readonly P.DaprClient daprClient; + // property exposed for testing purposes + internal P.DaprClient Client => daprClient; + /// /// Creates a new instance of a /// diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj new file mode 100644 index 00000000..b58307e1 --- /dev/null +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -0,0 +1,36 @@ + + + + + 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.Messaging.Test/Protos/test.proto b/test/Dapr.Messaging.Test/Protos/test.proto new file mode 100644 index 00000000..9763fb59 --- /dev/null +++ b/test/Dapr.Messaging.Test/Protos/test.proto @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------ +// 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"; + +option csharp_namespace = "Dapr.Client.Autogen.Test.Grpc.v1"; + +message TestRun { + repeated TestCase tests = 1; +} + +message TestCase { + string name = 1; +} + +message Request { + string RequestParameter = 1; +} + +message Response { + string Name = 1; +} \ No newline at end of file From 08526504e86b5cf4421dcaf8909828a722f64af3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 02:49:25 -0500 Subject: [PATCH 11/42] Added missing copyright statement Signed-off-by: Whit Waldo --- src/Dapr.Messaging/AssemblyInfo.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs index f14d0926..bdddbb38 100644 --- a/src/Dapr.Messaging/AssemblyInfo.cs +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -1,3 +1,16 @@ -using System.Runtime.CompilerServices; +// ------------------------------------------------------------------------ +// 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.Messaging.Test")] From 0cb3815c6ae251e4e8f7ed8a107e46faa75cc4d3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 02:52:09 -0500 Subject: [PATCH 12/42] Updated naming in file to match solution convention. Fleshed out missing XML comment. Signed-off-by: Whit Waldo --- .../PublishSubscribe/ConnectionManager.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs index 81a39aa5..91030593 100644 --- a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs +++ b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs @@ -18,52 +18,53 @@ namespace Dapr.Messaging.PublishSubscribe; /// -/// Maintains access to +/// Maintains the streaming connection to the Dapr sidecar so it can be repurposed without +/// multiple callers opening separate connections. /// internal sealed class ConnectionManager : IAsyncDisposable { /// /// A reference to the DaprClient instance. /// - private readonly P.Dapr.DaprClient _client; + private readonly P.Dapr.DaprClient client; /// /// Used to ensure thread-safe operations against the stream. /// - private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly SemaphoreSlim semaphore = new(1, 1); /// /// The stream connection between this instance and the Dapr sidecar. /// private AsyncDuplexStreamingCall? - _stream; + stream; public ConnectionManager(P.Dapr.DaprClient client) { - _client = client; + this.client = client; } public async Task> GetStreamAsync(CancellationToken cancellationToken) { - await _semaphore.WaitAsync(cancellationToken); + await semaphore.WaitAsync(cancellationToken); try { - return _stream ??= _client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); + return stream ??= client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); } finally { - _semaphore.Release(); + semaphore.Release(); } } public async ValueTask DisposeAsync() { - if (_stream is not null) + if (stream is not null) { - await _stream.RequestStream.CompleteAsync(); + await stream.RequestStream.CompleteAsync(); } - _semaphore.Dispose(); + semaphore.Dispose(); } } From 7edb9c5f42b54ef8ac5a4b0b0d8c70fe39f99186 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 03:03:32 -0500 Subject: [PATCH 13/42] Conforming to naming conventions Signed-off-by: Whit Waldo --- .../PublishSubscribe/PublishSubscribeReceiver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index c59bfad1..1728edd9 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -78,7 +78,7 @@ internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubsc /// An containing messages provided by the sidecar. public IAsyncEnumerable SubscribeAsync(CancellationToken cancellationToken) { - _ = FetchDataFromSidecar(channel.Writer, cancellationToken); + _ = FetchDataFromSidecarAsync(channel.Writer, cancellationToken); return ReadMessagesFromChannelAsync(channel.Reader, cancellationToken); } @@ -192,7 +192,7 @@ private async Task WaitForAcknowledgementAsync(string messageId, CancellationTok /// /// The channel writer instance. /// Cancellation token. - private async Task FetchDataFromSidecar(ChannelWriter channelWriter, CancellationToken cancellationToken) + private async Task FetchDataFromSidecarAsync(ChannelWriter channelWriter, CancellationToken cancellationToken) { try { From 219d458abe41703a71b9dde100b6c31d294823dc Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 03:16:53 -0500 Subject: [PATCH 14/42] Eliminated need for locks by using ConcurrentDictionary instead Signed-off-by: Whit Waldo --- .../PublishSubscribeReceiver.cs | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 1728edd9..55ce1276 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Threading.Channels; using Grpc.Core; @@ -50,11 +51,7 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// A collection of used to signal acknowledgement of received messages so a status /// can be sent back to the sidecar indicating what behavior should happen to each. /// - private readonly Dictionary> acknowledgementTasks = new(); - /// - /// A semaphore used to ensure thread-safe access to the dictionary. - /// - private readonly SemaphoreSlim acknowledgementSemaphore = new(1, 1); + private readonly ConcurrentDictionary> acknowledgementTasks = new(); /// /// Constructs a new instance of a instance. @@ -109,18 +106,9 @@ await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 } }, cancellationToken); - await acknowledgementSemaphore.WaitAsync(cancellationToken); - try - { - if (acknowledgementTasks.TryGetValue(messageId, out var tcs)) - { - tcs.SetResult(true); - acknowledgementTasks.Remove(messageId); - } - } - finally + if (acknowledgementTasks.TryRemove(messageId, out var tcs)) { - acknowledgementSemaphore.Release(); + tcs.SetResult(true); } } @@ -167,17 +155,8 @@ await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultM private async Task WaitForAcknowledgementAsync(string messageId, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await acknowledgementSemaphore.WaitAsync(cancellationToken); + acknowledgementTasks.AddOrUpdate(messageId, _ => tcs, (_, _) => tcs); - try - { - acknowledgementTasks[messageId] = tcs; - } - finally - { - acknowledgementSemaphore.Release(); - } - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); @@ -256,6 +235,5 @@ public async ValueTask DisposeAsync() { await connectionManager.DisposeAsync(); channel.Writer.Complete(); - acknowledgementSemaphore.Dispose(); } } From 1847ca9f13435a6e69f62de47d61cef6ee29f0c5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 03:32:03 -0500 Subject: [PATCH 15/42] Simplified registration extensions to minimize repeated implementations Signed-off-by: Whit Waldo --- ...ishSubscribeServiceCollectionExtensions.cs | 46 +++---------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index fadcd73d..57a6c99f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Runtime.InteropServices.ComTypes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -26,25 +27,8 @@ public static class PublishSubscribeServiceCollectionExtensions /// /// The . /// - public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - - //Register the IHttpClientFactory implementation - services.AddHttpClient(); - - services.TryAddSingleton(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - - var builder = new DaprPublishSubscribeClientBuilder(); - builder.UseHttpClientFactory(httpClientFactory); - - return builder.Build(); - }); - - return services; - } + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services) => + AddDaprPubSubClient(services, (_, _) => { }); /// /// Adds Dapr Publish/Subscribe support to the service collection. @@ -52,27 +36,9 @@ public static IServiceCollection AddDaprPubSubClient(this IServiceCollection ser /// The . /// Optionally allows greater configuration of the . /// - public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure) - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - - //Register the IHttpClientFactory implementation - services.AddHttpClient(); - - services.TryAddSingleton(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - - var builder = new DaprPublishSubscribeClientBuilder(); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(builder); - - return builder.Build(); - }); - - return services; - } + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, + Action? configure) => + services.AddDaprPubSubClient((_, builder) => configure?.Invoke(builder)); /// /// Adds Dapr Publish/Subscribe support to the service collection. From 848c4900ecd8cddb8a6bb005739702a2df8a700b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 20:12:05 -0500 Subject: [PATCH 16/42] Added public key to InternalsVisibleTo annotation Signed-off-by: Whit Waldo --- src/Dapr.Messaging/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs index bdddbb38..44295129 100644 --- a/src/Dapr.Messaging/AssemblyInfo.cs +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -13,4 +13,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Dapr.Messaging.Test")] +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] From b6810b02026d6599e5ea88a15138e668cea76fee Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 20:12:48 -0500 Subject: [PATCH 17/42] Significantly simplified dynamic pubsub implementation Signed-off-by: Whit Waldo --- .../PublishSubscribe/ConnectionManager.cs | 9 +- .../DaprPublishSubscribeClient.cs | 30 +--- .../DaprPublishSubscribeGrpcClient.cs | 62 ++------ ...ishSubscribeServiceCollectionExtensions.cs | 1 - .../PublishSubscribe/MessageHandlingPolicy.cs | 4 +- .../PublishSubscribeReceiver.cs | 150 +++++------------- .../PublishSubscribe/TopicMessage.cs | 50 ++---- .../PublishSubscribe/TopicMessageHandler.cs | 10 ++ ...essageAction.cs => TopicResponseAction.cs} | 2 +- 9 files changed, 93 insertions(+), 225 deletions(-) create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs rename src/Dapr.Messaging/PublishSubscribe/{TopicMessageAction.cs => TopicResponseAction.cs} (97%) diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs index 91030593..c4fd1e81 100644 --- a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs +++ b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs @@ -42,9 +42,12 @@ public ConnectionManager(P.Dapr.DaprClient client) this.client = client; } - public async - Task> - GetStreamAsync(CancellationToken cancellationToken) + /// + /// Retrieves or creates the bidirectional stream to the DaprClient for transacting pub/sub subscriptions. + /// + /// Cancellation token. + /// + public async Task> GetStreamAsync(CancellationToken cancellationToken) { await semaphore.WaitAsync(cancellationToken); diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 8fd5b1f5..55669ad8 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -21,32 +21,14 @@ public abstract class DaprPublishSubscribeClient /// /// Dynamically subscribes to a Publish/Subscribe component and topic. /// - /// The name of the Publish/Subscribe component. + /// The name of the Publish/Subscribe component. /// The name of the topic to subscribe to. /// Configuration options. + /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. - /// An containing the various messages returned by the subscription. - public abstract IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, CancellationToken cancellationToken); - - /// - /// Used to acknowledge receipt of a message and indicate how the Dapr sidecar should handle it. - /// - /// The name of the Publish/Subscribe component. - /// The name of the topic to subscribe to. - /// The identifier of the message to apply the action to. - /// Indicates the action to perform on the message. - /// Cancellation token. - public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, - TopicMessageAction messageAction, CancellationToken cancellationToken); - - /// - /// Unsubscribes a streaming subscription for the specified Publish/Subscribe component and topic. - /// - /// The name of the Publish/Subscribe component. - /// The name of the topic to subscribe to. - /// Cancellation token. - public abstract Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken); - + /// + public abstract IAsyncDisposable SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); + /// /// Gets the Dapr API token header for the given token value. /// @@ -55,9 +37,7 @@ public abstract Task AcknowledgeMessageAsync(string pubsubName, string topicName internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) { if (string.IsNullOrWhiteSpace(apiToken)) - { return null; - } return new KeyValuePair("dapr-api-token", apiToken); } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 3f7b2a12..d0234905 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -24,71 +24,37 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien /// The various receiver clients created for each combination of Dapr pubsub component and topic name. /// private readonly Dictionary<(string, string), PublishSubscribeReceiver> clients = new(); + /// - /// The Dapr client. + /// Maintains a single connection to the Dapr dynamic subscription endpoint. /// - private readonly P.DaprClient daprClient; - - // property exposed for testing purposes - internal P.DaprClient Client => daprClient; + private readonly ConnectionManager connectionManager; /// /// Creates a new instance of a /// public DaprPublishSubscribeGrpcClient(P.DaprClient client) { - daprClient = client; + connectionManager = new(client); } /// /// Dynamically subscribes to a Publish/Subscribe component and topic. /// - /// The name of the Publish/Subscribe component. + /// The name of the Publish/Subscribe component. /// The name of the topic to subscribe to. /// Configuration options. + /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. - /// An containing the various messages returned by the subscription. - public override IAsyncEnumerable SubscribeAsync(string pubsubName, string topicName, DaprSubscriptionOptions options, - CancellationToken cancellationToken) - { - var receiver = new PublishSubscribeReceiver(pubsubName, topicName, options, daprClient); - clients[(pubsubName, topicName)] = receiver; - - return receiver.SubscribeAsync(cancellationToken); - } - - /// - /// Used to acknowledge receipt of a message and indicate how the Dapr sidecar should handle it. - /// - /// The name of the Publish/Subscribe component. - /// The name of the topic to subscribe to. - /// The identifier of the message to apply the action to. - /// Indicates the action to perform on the message. - /// Cancellation token. - public override async Task AcknowledgeMessageAsync(string pubsubName, string topicName, string messageId, - TopicMessageAction messageAction, CancellationToken cancellationToken) - { - if (!clients.TryGetValue((pubsubName, topicName), out var receiver)) - { - throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); - } - - await receiver.AcknowledgeMessageAsync(messageId, messageAction, cancellationToken); - } - - /// - /// Unsubscribes a streaming subscription for the specified Publish/Subscribe component and topic. - /// - /// The name of the Publish/Subscribe component. - /// The name of the topic to subscribe to. - /// Cancellation token. - public override async Task UnsubscribeAsync(string pubsubName, string topicName, CancellationToken cancellationToken) + /// + public override IAsyncDisposable SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) { - if (!clients.TryGetValue((pubsubName, topicName), out var receiver)) - { - throw new Exception($"Unable to find receiver instance for specified publish/subscribe component name '{pubsubName}' and topic '{topicName}'."); - } + var key = (pubSubName, topicName); + if (clients.ContainsKey(key)) + throw new Exception( + $"A subscription has already been created for Dapr pub/sub component '{pubSubName}' and topic '{topicName}'"); - await receiver.DisposeAsync(); + clients[key] = new PublishSubscribeReceiver(pubSubName, topicName, options, connectionManager, messageHandler); + return clients[key]; } } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index 57a6c99f..b24203b3 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Runtime.InteropServices.ComTypes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs index c4393aa6..1abfed6f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs +++ b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs @@ -17,5 +17,5 @@ namespace Dapr.Messaging.PublishSubscribe; /// Defines the policy for handling streaming message subscriptions, including retry logic and timeout settings. /// /// The duration to wait before timing out a message handling operation. -/// The default action to take when a message handling operation times out. -public sealed record MessageHandlingPolicy(TimeSpan TimeoutDuration, TopicMessageAction DefaultMessageAction); +/// The default action to take when a message handling operation times out. +public sealed record MessageHandlingPolicy(TimeSpan TimeoutDuration, TopicResponseAction DefaultResponseAction); diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 55ce1276..cc8b1c35 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Collections.Concurrent; -using System.Runtime.CompilerServices; using System.Threading.Channels; using Grpc.Core; using C = Dapr.AppCallback.Autogen.Grpc.v1; @@ -26,15 +24,10 @@ namespace Dapr.Messaging.PublishSubscribe; /// internal sealed class PublishSubscribeReceiver : IAsyncDisposable { - /// - /// Maintains the stream connection to the Dapr sidecar for the subscription. - /// - private readonly ConnectionManager connectionManager; - /// /// The name of the Dapr pubsub component. /// - private readonly string pubsubName; + private readonly string pubSubName; /// /// The name of the topic to subscribe to. /// @@ -48,24 +41,29 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// private readonly Channel channel = Channel.CreateUnbounded(); /// - /// A collection of used to signal acknowledgement of received messages so a status - /// can be sent back to the sidecar indicating what behavior should happen to each. + /// The handler delegate responsible for processing the topic messages. /// - private readonly ConcurrentDictionary> acknowledgementTasks = new(); - + private readonly TopicMessageHandler messageHandler; + /// + /// Maintains the connection to the Dapr dynamic subscription endpoint. + /// + private readonly ConnectionManager connectionManager; + /// /// Constructs a new instance of a instance. /// - /// The name of the Dapr pubsub component. + /// The name of the Dapr Publish/Subscribe component. /// The name of the topic to subscribe to. /// Options allowing the behavior of the receiver to be configured. - /// - internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubscriptionOptions options, P.Dapr.DaprClient daprClient) + /// Maintains the connection to the Dapr dynamic subscription endpoint. + /// The delegate reflecting the action to take upon messages received by the subscription. + internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubscriptionOptions options, ConnectionManager connectionManager, TopicMessageHandler handler) { - this.pubsubName = pubsubName; + this.pubSubName = pubSubName; this.topicName = topicName; this.options = options; - connectionManager = new ConnectionManager(daprClient); + this.connectionManager = connectionManager; + this.messageHandler = handler; } /// @@ -73,99 +71,47 @@ internal PublishSubscribeReceiver(string pubsubName, string topicName, DaprSubsc /// /// Cancellation token. /// An containing messages provided by the sidecar. - public IAsyncEnumerable SubscribeAsync(CancellationToken cancellationToken) + public async Task SubscribeAsync(CancellationToken cancellationToken) { + //Retrieve the messages from the sidecar and write to the channel _ = FetchDataFromSidecarAsync(channel.Writer, cancellationToken); - return ReadMessagesFromChannelAsync(channel.Reader, cancellationToken); - } - /// - /// Specifies the action that should be taken on the message after processing it. - /// - /// The identifier of the message to acknowledge. - /// The action to take on the message. - /// Cancellation token. - public async Task AcknowledgeMessageAsync(string messageId, TopicMessageAction messageAction, CancellationToken cancellationToken) - { var stream = await connectionManager.GetStreamAsync(cancellationToken); - await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + //Read the messages one-by-one out of the channel + while (await channel.Reader.WaitToReadAsync(cancellationToken)) { - EventResponse = new P.SubscribeTopicEventsResponseAlpha1 - { - Id = messageId, - Status = new C.TopicEventResponse - { - Status = messageAction switch - { - TopicMessageAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, - TopicMessageAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus.Success, - TopicMessageAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, - _ => throw new ArgumentOutOfRangeException(nameof(messageAction), messageAction, null) - } - } - } - }, cancellationToken); - - if (acknowledgementTasks.TryRemove(messageId, out var tcs)) - { - tcs.SetResult(true); - } - } - - /// - /// Reads the topic messages returned from the Dapr sidecar. - /// - /// The channel reader instance. - /// Cancellation token. - /// An containing the received messages from the Dapr sidecar. - private async IAsyncEnumerable ReadMessagesFromChannelAsync(ChannelReader reader, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - while (await reader.WaitToReadAsync(cancellationToken)) - { - while (reader.TryRead(out var message)) + while (channel.Reader.TryRead(out var message)) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(options!.MessageHandlingPolicy.TimeoutDuration); + cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - yield return message; + //Evaluate the message and return an acknowledgement result + var messageAction = await messageHandler(message, cts.Token); - try - { - //Wait for the message to be acknowledged - await WaitForAcknowledgementAsync(message.Id, cts.Token); - } - catch (OperationCanceledException) when (cts.Token.IsCancellationRequested) + //Share the result with the sidecar + await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 { - //Handle the acknowledgement timeout using the specified default policy - await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultMessageAction, - cancellationToken); - } + EventResponse = new() + { + Id = message.Id, + Status = new() + { + Status = messageAction switch + { + TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus + .Success, + TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new InvalidOperationException( + $"Unrecognized topic acknowledgement action: {messageAction}") + } + } + } + }, cts.Token); } } } - /// - /// Sets up a timeout for message acknowledgement before the configured default action is applied - /// to each message. - /// - /// The identifier of the topic message. - /// Cancellation token. - /// - private async Task WaitForAcknowledgementAsync(string messageId, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - acknowledgementTasks.AddOrUpdate(messageId, _ => tcs, (_, _) => tcs); - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - - await using (cancellationToken.Register(() => tcs.TrySetCanceled())) - { - await tcs.Task; - } - } - /// /// Retrieves the subscription stream data from the Dapr sidecar. /// @@ -178,7 +124,7 @@ private async Task FetchDataFromSidecarAsync(ChannelWriter channel var stream = await connectionManager.GetStreamAsync(cancellationToken); var initialRequest = new P.SubscribeTopicEventsInitialRequestAlpha1() { - PubsubName = pubsubName, + PubsubName = pubSubName, DeadLetterTopic = options?.DeadLetterTopic ?? string.Empty, Topic = topicName }; @@ -195,16 +141,8 @@ private async Task FetchDataFromSidecarAsync(ChannelWriter channel await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) { - var message = new TopicMessage + var message = new TopicMessage(response.Id, response.Source, response.Type, response.SpecVersion, response.DataContentType, response.Topic, response.PubsubName) { - Id = response.Id, - Source = response.Source, - Type = response.Type, - SpecVersion = response.SpecVersion, - DataContentType = response.DataContentType, - Data = response.Data.Memory, - Topic = response.Topic, - PubSubName = response.PubsubName, Path = response.Path, Extensions = response.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) }; diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs index be28ba45..820a3abe 100644 --- a/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs @@ -18,57 +18,29 @@ namespace Dapr.Messaging.PublishSubscribe; /// /// A message retrieved from a Dapr publish/subscribe topic. /// -public sealed record TopicMessage +/// The unique identifier of the topic message. +/// Identifies the context in which an event happened, such as 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. +/// The type of event related to the originating occurrence. +/// The spec version of the CloudEvents specification. +/// The content type of the data. +/// The name of the topic. +/// The name of the Dapr publish/subscribe component. +public sealed record TopicMessage(string Id, string Source, string Type, string SpecVersion, string DataContentType, string Topic, string PubSubName) { - /// - /// The unique identifier of the topic message. - /// - public string Id { get; init; } = default!; - - /// - /// Identifies the context in which an event happened, such as 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. - /// - public string Source { get; init; } = default!; - - /// - /// The type of event related to the originating occurrence. - /// - public string Type { get; init; } = default!; - - /// - /// The spec version of the CloudEvents specification. - /// - public string SpecVersion { get; init; } = default!; - - /// - /// The content type of the data. - /// - public string DataContentType { get; init; } = default!; - /// /// The content of the event. /// public ReadOnlyMemory Data { get; init; } - /// - /// The name of the topic. - /// - public string Topic { get; init; } = default!; - - /// - /// The name of the Dapr publish/subscribe component. - /// - public string PubSubName { get; init; } = default!; - /// /// The matching path from the topic subscription/routes (if specified) for this event. /// public string? Path { get; init; } /// - /// A map of additional custom properties sent to the app. These are considered to be cloud event extensions. + /// A map of additional custom properties sent to the app. These are considered to be CloudEvent extensions. /// public Dictionary Extensions { get; init; } = new(); } diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs new file mode 100644 index 00000000..9c89c4a0 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs @@ -0,0 +1,10 @@ +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// The handler delegate responsible for processing the topic message. +/// +/// The message request to process. +/// Cancellation token. +/// The acknowledgement behavior to report back to the pub/sub endpoint about the message. +public delegate Task TopicMessageHandler(TopicMessage request, + CancellationToken cancellationToken = default); diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs b/src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs similarity index 97% rename from src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs rename to src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs index 23e7dee6..5a34f4cc 100644 --- a/src/Dapr.Messaging/PublishSubscribe/TopicMessageAction.cs +++ b/src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs @@ -16,7 +16,7 @@ namespace Dapr.Messaging.PublishSubscribe; /// /// Describes the various actions that can be taken on a topic message. /// -public enum TopicMessageAction +public enum TopicResponseAction { /// /// Indicates the message was processed successfully and should be deleted from the pub/sub topic. From 04fa4d5a1ff6aadc46684b10c9e760328f70cb2c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 21:15:25 -0500 Subject: [PATCH 18/42] Handling cancellation token timeout with configured action Signed-off-by: Whit Waldo --- .../PublishSubscribeReceiver.cs | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index cc8b1c35..41fc18bc 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Runtime.InteropServices.ComTypes; using System.Threading.Channels; using Grpc.Core; using C = Dapr.AppCallback.Autogen.Grpc.v1; @@ -48,7 +49,7 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// Maintains the connection to the Dapr dynamic subscription endpoint. /// private readonly ConnectionManager connectionManager; - + /// /// Constructs a new instance of a instance. /// @@ -73,10 +74,10 @@ internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubsc /// An containing messages provided by the sidecar. public async Task SubscribeAsync(CancellationToken cancellationToken) { - //Retrieve the messages from the sidecar and write to the channel - _ = FetchDataFromSidecarAsync(channel.Writer, cancellationToken); - var stream = await connectionManager.GetStreamAsync(cancellationToken); + //Retrieve the messages from the sidecar and write to the channel + _ = FetchDataFromSidecarAsync(stream, channel.Writer, cancellationToken); + //Read the messages one-by-one out of the channel while (await channel.Reader.WaitToReadAsync(cancellationToken)) { @@ -84,44 +85,66 @@ public async Task SubscribeAsync(CancellationToken cancellationToken) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - + //Evaluate the message and return an acknowledgement result var messageAction = await messageHandler(message, cts.Token); - //Share the result with the sidecar - await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + try { - EventResponse = new() + //Share the result with the sidecar + await AcknowledgeMessageAsync(stream, message.Id, messageAction, cts.Token); + } + catch (OperationCanceledException) + { + //Acknowledge the message using the configured default response action + await AcknowledgeMessageAsync(stream, message.Id, + options.MessageHandlingPolicy.DefaultResponseAction, cts.Token); + } + } + } + } + + /// + /// Acknowledges the indicated message back to the Dapr sidecar with an indicated behavior to take on the message. + /// + /// The stream connection to and from the Dream sidecar instance. + /// The identifier of the message the behavior is in reference to. + /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. + /// Cancellation token. + /// + private static async Task AcknowledgeMessageAsync(AsyncDuplexStreamingCall stream, string messageId, TopicResponseAction action, CancellationToken cancellationToken) + { + await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + { + EventResponse = new() + { + Id = messageId, + Status = new() + { + Status = action switch { - Id = message.Id, - Status = new() - { - Status = messageAction switch - { - TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus - .Success, - TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, - TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, - _ => throw new InvalidOperationException( - $"Unrecognized topic acknowledgement action: {messageAction}") - } - } + TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus + .Success, + TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new InvalidOperationException( + $"Unrecognized topic acknowledgement action: {action}") } - }, cts.Token); + } } - } + }, cancellationToken); } /// /// Retrieves the subscription stream data from the Dapr sidecar. /// + /// The stream connection to and from the Dream sidecar instance. /// The channel writer instance. /// Cancellation token. - private async Task FetchDataFromSidecarAsync(ChannelWriter channelWriter, CancellationToken cancellationToken) + private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall stream, ChannelWriter channelWriter, CancellationToken cancellationToken) { try { - var stream = await connectionManager.GetStreamAsync(cancellationToken); var initialRequest = new P.SubscribeTopicEventsInitialRequestAlpha1() { PubsubName = pubSubName, From d165b149552d5a4ba6c5cb2025a15989c12afbb5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 21:21:46 -0500 Subject: [PATCH 19/42] Updated name of method so it wouldn't be as confusing why the developer would have two back-to-back invocations of SubscribeAsync (especially since it's not an async implementation at the Grpc client level. Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprPublishSubscribeClient.cs | 2 +- .../PublishSubscribe/DaprPublishSubscribeGrpcClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 55669ad8..a50ef2c1 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -27,7 +27,7 @@ public abstract class DaprPublishSubscribeClient /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public abstract IAsyncDisposable SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); + public abstract IAsyncDisposable Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); /// /// Gets the Dapr API token header for the given token value. diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index d0234905..3b0bc7a0 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -47,7 +47,7 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override IAsyncDisposable SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) + public override IAsyncDisposable Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) { var key = (pubSubName, topicName); if (clients.ContainsKey(key)) From 5c3e1ce64e3e9b061d9214cf7fd8ed349fb9ae21 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 15 Sep 2024 21:34:37 -0500 Subject: [PATCH 20/42] Added another message handler to facilitate channel draining in case of cancellation where there's a buffer in the Channel Signed-off-by: Whit Waldo --- .../PublishSubscribeReceiver.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 41fc18bc..6cc3d9a6 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -79,29 +79,41 @@ public async Task SubscribeAsync(CancellationToken cancellationToken) _ = FetchDataFromSidecarAsync(stream, channel.Writer, cancellationToken); //Read the messages one-by-one out of the channel - while (await channel.Reader.WaitToReadAsync(cancellationToken)) + try { - while (channel.Reader.TryRead(out var message)) + while (await channel.Reader.WaitToReadAsync(cancellationToken)) { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - - //Evaluate the message and return an acknowledgement result - var messageAction = await messageHandler(message, cts.Token); - - try + while (channel.Reader.TryRead(out var message)) { - //Share the result with the sidecar - await AcknowledgeMessageAsync(stream, message.Id, messageAction, cts.Token); - } - catch (OperationCanceledException) - { - //Acknowledge the message using the configured default response action - await AcknowledgeMessageAsync(stream, message.Id, - options.MessageHandlingPolicy.DefaultResponseAction, cts.Token); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); + + //Evaluate the message and return an acknowledgement result + var messageAction = await messageHandler(message, cts.Token); + + try + { + //Share the result with the sidecar + await AcknowledgeMessageAsync(stream, message.Id, messageAction, cts.Token); + } + catch (OperationCanceledException) + { + //Acknowledge the message using the configured default response action + await AcknowledgeMessageAsync(stream, message.Id, + options.MessageHandlingPolicy.DefaultResponseAction, cts.Token); + } } } } + catch (OperationCanceledException) + { + //Drain the remaining messages with the default action in the order in which they were queued + while (channel.Reader.TryRead(out var message)) + { + await AcknowledgeMessageAsync(stream, message.Id, options.MessageHandlingPolicy.DefaultResponseAction, + CancellationToken.None); + } + } } /// From 85ef0f8a608fef391cdeb5fac4ac6ce0f994f7ca Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 11:42:20 -0500 Subject: [PATCH 21/42] Handling potential race condition, corrected return type to be PublishSubscribeReceiver instead of IAsyncDisposable Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprPublishSubscribeGrpcClient.cs | 9 +++++---- .../PublishSubscribe/PublishSubscribeReceiver.cs | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 3b0bc7a0..0e13e7bc 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Concurrent; using P = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.Messaging.PublishSubscribe; @@ -23,7 +24,7 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien /// /// The various receiver clients created for each combination of Dapr pubsub component and topic name. /// - private readonly Dictionary<(string, string), PublishSubscribeReceiver> clients = new(); + private readonly ConcurrentDictionary<(string, string), Lazy> clients = new(); /// /// Maintains a single connection to the Dapr dynamic subscription endpoint. @@ -47,14 +48,14 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override IAsyncDisposable Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) + public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) { var key = (pubSubName, topicName); if (clients.ContainsKey(key)) throw new Exception( $"A subscription has already been created for Dapr pub/sub component '{pubSubName}' and topic '{topicName}'"); - clients[key] = new PublishSubscribeReceiver(pubSubName, topicName, options, connectionManager, messageHandler); - return clients[key]; + clients[key] = new Lazy(new PublishSubscribeReceiver(pubSubName, topicName, options, connectionManager, messageHandler)); + return clients[key].Value; } } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 6cc3d9a6..59fef911 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Runtime.InteropServices.ComTypes; using System.Threading.Channels; using Grpc.Core; using C = Dapr.AppCallback.Autogen.Grpc.v1; From 218f604e53ffdfc872ba79c792f1b33cbb50d028 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 12:26:12 -0500 Subject: [PATCH 22/42] No longer limiting a single subscriber for each component/topic combination Signed-off-by: Whit Waldo --- .../DaprPublishSubscribeGrpcClient.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 0e13e7bc..bfc56c4f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -21,11 +21,6 @@ namespace Dapr.Messaging.PublishSubscribe; /// internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - /// - /// The various receiver clients created for each combination of Dapr pubsub component and topic name. - /// - private readonly ConcurrentDictionary<(string, string), Lazy> clients = new(); - /// /// Maintains a single connection to the Dapr dynamic subscription endpoint. /// @@ -48,14 +43,5 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) - { - var key = (pubSubName, topicName); - if (clients.ContainsKey(key)) - throw new Exception( - $"A subscription has already been created for Dapr pub/sub component '{pubSubName}' and topic '{topicName}'"); - - clients[key] = new Lazy(new PublishSubscribeReceiver(pubSubName, topicName, options, connectionManager, messageHandler)); - return clients[key].Value; - } + public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) => new(pubSubName, topicName, options, connectionManager, messageHandler); } From a7c4d5dc1f0c092442245d5852d5d420abcf8813 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:00:03 -0500 Subject: [PATCH 23/42] Eliminated restriction for one subscription per component/topic. Added channel to the shared ConnectionManager to handle message acknowledgements and to process inbound acknowledgements sequentially to the shared stream instead of from each separate receiver. Signed-off-by: Whit Waldo --- .../PublishSubscribe/ConnectionManager.cs | 75 ++++++++++++++++++- .../PublishSubscribeReceiver.cs | 32 ++------ 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs index c4fd1e81..edf18d91 100644 --- a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs +++ b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Threading.Channels; using Grpc.Core; using C = Dapr.AppCallback.Autogen.Grpc.v1; using P = Dapr.Client.Autogen.Grpc.v1; @@ -34,12 +35,28 @@ internal sealed class ConnectionManager : IAsyncDisposable /// /// The stream connection between this instance and the Dapr sidecar. /// - private AsyncDuplexStreamingCall? - stream; + private AsyncDuplexStreamingCall? stream; + /// + /// Maintains the various acknowledgements for each message. + /// + /// + /// Storing the acknowledgements here so we can ensure that a single writer handles sending them back over the stream. This class + /// isn't public + /// + private readonly Channel acknowledgements = Channel.CreateUnbounded(); public ConnectionManager(P.Dapr.DaprClient client) { this.client = client; + + //Processes each acknowledgement from the channel reader as they are provided by the various PublishSubscribeReceiver instances. + Task.Run(async () => + { + await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync()) + { + await ProcessAcknowledgementAsync(acknowledgement); + } + }); } /// @@ -61,8 +78,55 @@ public ConnectionManager(P.Dapr.DaprClient client) } } + /// + /// Acknowledges a provided message with an intended action to perform on it based on how it was either + /// handled or whether it timed out. + /// + /// The identifier of the message the behavior is in reference to. + /// The behavior to take on the message. + /// + /// + public async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior) + { + var action = behavior switch + { + TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus + .Success, + TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new InvalidOperationException( + $"Unrecognized topic acknowledgement action: {behavior}") + }; + + var acknowledgement = new TopicAcknowledgement(messageId, action); + await acknowledgements.Writer.WriteAsync(acknowledgement); + } + + /// + /// Processes each of the acknowledgement messages. + /// + /// Information about the message and the action to take on it. + private async Task ProcessAcknowledgementAsync(TopicAcknowledgement acknowledgement) + { + var messageStream = await GetStreamAsync(CancellationToken.None); + await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + { + EventResponse = new() + { + Id = acknowledgement.MessageId, Status = new() { Status = acknowledgement.Action } + } + }); + } + public async ValueTask DisposeAsync() { + //Flush the remaining acknowledgements, but start by marking the writer as complete so we don't get any more of them + acknowledgements.Writer.Complete(); + await foreach (var message in acknowledgements.Reader.ReadAllAsync()) + { + await ProcessAcknowledgementAsync(message); + } + if (stream is not null) { await stream.RequestStream.CompleteAsync(); @@ -70,4 +134,11 @@ public async ValueTask DisposeAsync() semaphore.Dispose(); } + + /// + /// Reflects the action to take on a given message identifier. + /// + /// The identifier of the message. + /// The action to take on the message in the acknowledgement request. + private sealed record TopicAcknowledgement(string MessageId, C.TopicEventResponse.Types.TopicEventResponseStatus Action); } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 59fef911..d15c2b20 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -93,13 +93,12 @@ public async Task SubscribeAsync(CancellationToken cancellationToken) try { //Share the result with the sidecar - await AcknowledgeMessageAsync(stream, message.Id, messageAction, cts.Token); + await AcknowledgeMessageAsync(message.Id, messageAction); } catch (OperationCanceledException) { //Acknowledge the message using the configured default response action - await AcknowledgeMessageAsync(stream, message.Id, - options.MessageHandlingPolicy.DefaultResponseAction, cts.Token); + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); } } } @@ -109,8 +108,7 @@ await AcknowledgeMessageAsync(stream, message.Id, //Drain the remaining messages with the default action in the order in which they were queued while (channel.Reader.TryRead(out var message)) { - await AcknowledgeMessageAsync(stream, message.Id, options.MessageHandlingPolicy.DefaultResponseAction, - CancellationToken.None); + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); } } } @@ -118,32 +116,12 @@ await AcknowledgeMessageAsync(stream, message.Id, options.MessageHandlingPolicy. /// /// Acknowledges the indicated message back to the Dapr sidecar with an indicated behavior to take on the message. /// - /// The stream connection to and from the Dream sidecar instance. /// The identifier of the message the behavior is in reference to. /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. - /// Cancellation token. /// - private static async Task AcknowledgeMessageAsync(AsyncDuplexStreamingCall stream, string messageId, TopicResponseAction action, CancellationToken cancellationToken) + private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction action) { - await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 - { - EventResponse = new() - { - Id = messageId, - Status = new() - { - Status = action switch - { - TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus - .Success, - TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, - TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, - _ => throw new InvalidOperationException( - $"Unrecognized topic acknowledgement action: {action}") - } - } - } - }, cancellationToken); + await connectionManager.AcknowledgeMessageAsync(messageId, action); } /// From 152c14e516ba168fc51990bf194703d864b2e4f1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:09:08 -0500 Subject: [PATCH 24/42] Updated to latest protos Signed-off-by: Whit Waldo --- .../Protos/dapr/proto/common/v1/common.proto | 4 +- .../dapr/proto/dapr/v1/appcallback.proto | 2 +- .../Protos/dapr/proto/dapr/v1/dapr.proto | 105 ++++++++++++------ 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 4acf9159..dd412985 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -26,7 +26,7 @@ option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; // 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 +// `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 { @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} \ No newline at end of file +} diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto index a8604036..3e98b536 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto @@ -340,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} \ No newline at end of file +message HealthCheckResponse {} diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto index f702491c..904b29a6 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto @@ -19,7 +19,7 @@ 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/dapr/v1/appcallback.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "DaprProtos"; @@ -61,7 +61,7 @@ service Dapr { // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic // events from it. - rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream TopicEventRequest) {} + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream SubscribeTopicEventsResponseAlpha1) {} // Invokes binding data to specific output bindings rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} @@ -428,17 +428,17 @@ message BulkPublishResponseFailedEntry { // 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. +// messages must be event processed responses. message SubscribeTopicEventsRequestAlpha1 { oneof subscribe_topic_events_request_type { - SubscribeTopicEventsInitialRequestAlpha1 initial_request = 1; - SubscribeTopicEventsResponseAlpha1 event_response = 2; + SubscribeTopicEventsRequestInitialAlpha1 initial_request = 1; + SubscribeTopicEventsRequestProcessedAlpha1 event_processed = 2; } } -// SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the -// details for subscribing to a topic via streaming. -message SubscribeTopicEventsInitialRequestAlpha1 { +// SubscribeTopicEventsRequestInitialAlpha1 is the initial message containing +// the details for subscribing to a topic via streaming. +message SubscribeTopicEventsRequestInitialAlpha1 { // The name of the pubsub component string pubsub_name = 1; @@ -456,9 +456,9 @@ message SubscribeTopicEventsInitialRequestAlpha1 { optional string dead_letter_topic = 4; } -// SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +// SubscribeTopicEventsRequestProcessedAlpha1 is the message containing the // subscription to a topic. -message SubscribeTopicEventsResponseAlpha1 { +message SubscribeTopicEventsRequestProcessedAlpha1 { // id is the unique identifier for the subscription request. string id = 1; @@ -466,6 +466,21 @@ message SubscribeTopicEventsResponseAlpha1 { TopicEventResponse status = 2; } + +// SubscribeTopicEventsResponseAlpha1 is a message returned from daprd +// when subscribing to a topic via streaming. +message SubscribeTopicEventsResponseAlpha1 { + oneof subscribe_topic_events_response_type { + SubscribeTopicEventsResponseInitialAlpha1 initial_response = 1; + TopicEventRequest event_message = 2; + } +} + +// SubscribeTopicEventsResponseInitialAlpha1 is the initial response from daprd +// when subscribing to a topic. +message SubscribeTopicEventsResponseInitialAlpha1 {} + + // InvokeBindingRequest is the message to send data to output bindings message InvokeBindingRequest { // The name of the output binding to invoke. @@ -478,6 +493,7 @@ message InvokeBindingRequest { // // 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. @@ -824,11 +840,11 @@ message TryLockRequest { // // 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 + // 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. + // 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. + // 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. @@ -865,7 +881,7 @@ message SubtleGetKeyRequest { // 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 @@ -1047,7 +1063,7 @@ message EncryptRequestOptions { // 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"]; + 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). @@ -1178,25 +1194,52 @@ message ShutdownRequest { // Empty } -// Job is the definition of a job. +// Job is the definition of a job. At least one of schedule or due_time must be +// provided but can also be provided together. 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; + string name = 1 [json_name = "name"]; - // Job data. - google.protobuf.Any data = 6; + // schedule is an optional schedule at which the job is to be run. + // Accepts both systemd timer style cron expressions, as well as human + // readable '@' prefixed period strings as defined below. + // + // Systemd timer style cron accepts 6 fields: + // seconds | minutes | hours | day of month | month | day of week + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // + // "0 30 * * * *" - every hour on the half hour + // "0 15 3 * * *" - every day at 03:15 + // + // Period string expressions: + // Entry | Description | Equivalent To + // ----- | ----------- | ------------- + // @every `` | Run every `` (e.g. '@every 1h30m') | N/A + // @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + // @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + // @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + // @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + // @hourly | Run once an hour, beginning of hour | 0 0 * * * * + optional string schedule = 2 [json_name = "schedule"]; + + // repeats is the optional number of times in which the job should be + // triggered. If not set, the job will run indefinitely or until expiration. + optional uint32 repeats = 3 [json_name = "repeats"]; + + // due_time is the optional time at which the job should be active, or the + // "one shot" time if other scheduling type fields are not provided. Accepts + // a "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string due_time = 4 [json_name = "dueTime"]; + + // ttl is the optional time to live or expiration of the job. Accepts a + // "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string ttl = 5 [json_name = "ttl"]; + + // payload is the serialized job payload that will be sent to the recipient + // when the job is triggered. + google.protobuf.Any data = 6 [json_name = "data"]; } // ScheduleJobRequest is the message to create/schedule the job. From 3af961a7a1ee8bb40db6de036ccec4a317e3cce6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:27:34 -0500 Subject: [PATCH 25/42] Updates to implementation to reflect latest protos Signed-off-by: Whit Waldo --- .../PublishSubscribe/ConnectionManager.cs | 14 +++++++++----- .../PublishSubscribe/PublishSubscribeReceiver.cs | 12 ++++++------ src/Dapr.Protos/Dapr.Protos.csproj | 4 ++-- .../proto/{dapr => runtime}/v1/appcallback.proto | 0 .../dapr/proto/{dapr => runtime}/v1/dapr.proto | 0 .../Dapr.Messaging.Test/Dapr.Messaging.Test.csproj | 4 ++++ 6 files changed, 21 insertions(+), 13 deletions(-) rename src/Dapr.Protos/Protos/dapr/proto/{dapr => runtime}/v1/appcallback.proto (100%) rename src/Dapr.Protos/Protos/dapr/proto/{dapr => runtime}/v1/dapr.proto (100%) diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs index edf18d91..6cb52c89 100644 --- a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs +++ b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs @@ -35,7 +35,7 @@ internal sealed class ConnectionManager : IAsyncDisposable /// /// The stream connection between this instance and the Dapr sidecar. /// - private AsyncDuplexStreamingCall? stream; + private AsyncDuplexStreamingCall? stream; /// /// Maintains the various acknowledgements for each message. /// @@ -64,7 +64,7 @@ public ConnectionManager(P.Dapr.DaprClient client) /// /// Cancellation token. /// - public async Task> GetStreamAsync(CancellationToken cancellationToken) + public async Task> GetStreamAsync(CancellationToken cancellationToken) { await semaphore.WaitAsync(cancellationToken); @@ -109,11 +109,15 @@ public async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction private async Task ProcessAcknowledgementAsync(TopicAcknowledgement acknowledgement) { var messageStream = await GetStreamAsync(CancellationToken.None); - await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1() { - EventResponse = new() + EventProcessed = new() { - Id = acknowledgement.MessageId, Status = new() { Status = acknowledgement.Action } + Id = acknowledgement.MessageId, + Status = new() + { + Status = acknowledgement.Action + } } }); } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index d15c2b20..691a7f8f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -130,14 +130,14 @@ private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction /// The stream connection to and from the Dream sidecar instance. /// The channel writer instance. /// Cancellation token. - private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall stream, ChannelWriter channelWriter, CancellationToken cancellationToken) + private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall stream, ChannelWriter channelWriter, CancellationToken cancellationToken) { try { - var initialRequest = new P.SubscribeTopicEventsInitialRequestAlpha1() + var initialRequest = new P.SubscribeTopicEventsRequestInitialAlpha1() { PubsubName = pubSubName, - DeadLetterTopic = options?.DeadLetterTopic ?? string.Empty, + DeadLetterTopic = options.DeadLetterTopic ?? string.Empty, Topic = topicName }; @@ -153,10 +153,10 @@ private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall f.Key, kvp => kvp.Value) + Path = response.EventMessage.Path, + Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) }; await channelWriter.WriteAsync(message, cancellationToken); diff --git a/src/Dapr.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj index 15041a82..2de69cb1 100644 --- a/src/Dapr.Protos/Dapr.Protos.csproj +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto similarity index 100% rename from src/Dapr.Protos/Protos/dapr/proto/dapr/v1/appcallback.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto diff --git a/src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto similarity index 100% rename from src/Dapr.Protos/Protos/dapr/proto/dapr/v1/dapr.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj index b58307e1..65b2c071 100644 --- a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -33,4 +33,8 @@ + + + + \ No newline at end of file From 6e17cd3d54b386fa387e13537f3c59aa3312baa4 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:29:40 -0500 Subject: [PATCH 26/42] Removed unused method Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprPublishSubscribeClient.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index a50ef2c1..29e86a45 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -28,17 +28,4 @@ public abstract class DaprPublishSubscribeClient /// Cancellation token. /// public abstract IAsyncDisposable Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); - - /// - /// Gets the Dapr API token header for the given token value. - /// - /// The value of the Dapr API token. - /// - internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) - { - if (string.IsNullOrWhiteSpace(apiToken)) - return null; - - return new KeyValuePair("dapr-api-token", apiToken); - } } From 562298b570513ab2af166fa3934f7721c4430502 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:35:13 -0500 Subject: [PATCH 27/42] Removed unused usings Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprPublishSubscribeGrpcClient.cs | 1 - src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index bfc56c4f..4d0b11f3 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Collections.Concurrent; using P = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.Messaging.PublishSubscribe; diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 691a7f8f..f21ae2dd 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -13,7 +13,6 @@ using System.Threading.Channels; using Grpc.Core; -using C = Dapr.AppCallback.Autogen.Grpc.v1; using P = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Messaging.PublishSubscribe; From 448c89b59e4f451036c6ffce45d849a7c5206b1d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 13:56:31 -0500 Subject: [PATCH 28/42] Added the only unit test I could identify that is within capabilities of the current mocking technology (e.g. can't mock static or sealed types) Signed-off-by: Whit Waldo --- .../PublishSubscribeReceiverTests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs new file mode 100644 index 00000000..8faf668b --- /dev/null +++ b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs @@ -0,0 +1,55 @@ +using System; +using Dapr.Messaging.PublishSubscribe; +using Moq; + +namespace Dapr.Messaging.Test.PublishSubscribe; + +public sealed class PublishSubscribeReceiverTests +{ + [Fact] + public void Constructor_ShouldInitializeFields() + { + var daprClient = new Mock(); + var connectionManager = new ConnectionManager(daprClient.Object); + var handlerMock = new Mock(); + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(30), TopicResponseAction.Retry)); + + var receiver = new PublishSubscribeReceiver("pubsub", "dapr", options, connectionManager, handlerMock.Object); + + Assert.NotNull(receiver); + } + + //[Fact] + //public async Task SubscribeAsync_ShouldHandleMessages() + //{ + // var connectionManagerMock = new Mock(); //Won't work - can't mock sealed types + // var handlerMock = new Mock(); + // var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(15), TopicResponseAction.Retry)); + // var receiver = new PublishSubscribeReceiver("pubsub", "dapr", options, connectionManagerMock.Object, + // handlerMock.Object); + // var cancellationToken = new CancellationToken(); + + // connectionManagerMock.Setup(cm => cm.GetStreamAsync(It.IsAny())) + // .ReturnsAsync(Mock + // .Of>()); + + // await receiver.SubscribeAsync(cancellationToken); + + // //Assert various behaviors + //} + + //[Fact] + //public async Task DisposeAsync_ShouldDisposeResources() + //{ + // var connectionManagerMock = new Mock(); //Won't work - can't mock sealed types + // var handlerMock = new Mock(); + // var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(15), TopicResponseAction.Retry)); + // var receiver = new PublishSubscribeReceiver("pubsub", "dapr", options, connectionManagerMock.Object, + // handlerMock.Object); + + // await receiver.DisposeAsync(); + + // connectionManagerMock.Verify(cm => cm.DisposeAsync(), Times.Once); + //} +} From e18e49ce9e46a2ea087705c47725c3593cd9cd1f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 14:30:16 -0500 Subject: [PATCH 29/42] Fixing accessibility problem Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprPublishSubscribeClient.cs | 2 +- src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 29e86a45..2c0c256f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -27,5 +27,5 @@ public abstract class DaprPublishSubscribeClient /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public abstract IAsyncDisposable Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); + public abstract PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index f21ae2dd..289d67a0 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -21,7 +21,7 @@ namespace Dapr.Messaging.PublishSubscribe; /// A thread-safe implementation of a receiver for messages from a specified Dapr publish/subscribe component and /// topic. /// -internal sealed class PublishSubscribeReceiver : IAsyncDisposable +public sealed class PublishSubscribeReceiver : IAsyncDisposable { /// /// The name of the Dapr pubsub component. From 033a27a8346b0d10328acf1623970464eecc5966 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 14:36:31 -0500 Subject: [PATCH 30/42] Added example demonstrating the streaming subscription functionality Signed-off-by: Whit Waldo --- all.sln | 9 +++++- .../StreamingSubscriptionExample/Program.cs | 32 +++++++++++++++++++ .../StreamingSubscriptionExample.csproj | 14 ++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs create mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj diff --git a/all.sln b/all.sln index 256f1a80..20462bf3 100644 --- a/all.sln +++ b/all.sln @@ -124,7 +124,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Protos", "src\Dapr.Pro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{93C6ABAF-F4B7-4CA2-8734-565EF847668A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{93C6ABAF-F4B7-4CA2-8734-565EF847668A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamingSubscriptionExample", "examples\Client\PublishSubscribe\StreamingSubscriptionExample\StreamingSubscriptionExample.csproj", "{E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -314,6 +316,10 @@ Global {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Debug|Any CPU.Build.0 = Debug|Any CPU {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Release|Any CPU.ActiveCfg = Release|Any CPU {93C6ABAF-F4B7-4CA2-8734-565EF847668A}.Release|Any CPU.Build.0 = Release|Any CPU + {E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -371,6 +377,7 @@ Global {CE506C30-5701-47C9-A86E-39D796B8DF35} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {9AB7EB9D-82CB-4BC6-B895-4F52F8DC489F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {93C6ABAF-F4B7-4CA2-8734-565EF847668A} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {E748C988-1F5F-4A34-9A5C-2EE2B6CD1BA2} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs new file mode 100644 index 00000000..90fc934a --- /dev/null +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs @@ -0,0 +1,32 @@ +using Dapr.Messaging.PublishSubscribe; + +var daprMessagingClientBuilder = new DaprPublishSubscribeClientBuilder(); +var daprMessagingClient = daprMessagingClientBuilder.Build(); + +//Processor for each of the messages returned from the subscription +async Task HandleMessage(TopicMessage message, CancellationToken cancellationToken = default) +{ + try + { + //Do something with the message + return await Task.FromResult(TopicResponseAction.Success); + } + catch + { + return await Task.FromResult(TopicResponseAction.Retry); + } +} + +//Create a dynamic streaming subscription +var subscription = daprMessagingClient.Register("pubsub", "myTopic", + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(15), TopicResponseAction.Retry)), + HandleMessage, CancellationToken.None); + +//Subscribe to messages on it with a timeout of 30 seconds +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +await subscription.SubscribeAsync(cancellationTokenSource.Token); + +await Task.Delay(TimeSpan.FromMinutes(1)); + +//When you're done with the subscription, simply dispose of it +await subscription.DisposeAsync(); diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj new file mode 100644 index 00000000..5ca19035 --- /dev/null +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + From d8969d0ad6e9e7648902944104d88e615a3c224c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 14:37:12 -0500 Subject: [PATCH 31/42] Retargeting to .NET 6 instead of .NET 8 Signed-off-by: Whit Waldo --- .../StreamingSubscriptionExample.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj index 5ca19035..2646a8da 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net6 enable enable From a9f645c92db7719cf59a22c60e89d0bb84dcffe2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 16 Sep 2024 14:42:00 -0500 Subject: [PATCH 32/42] Added sample deserialization - could be improved with client support Signed-off-by: Whit Waldo --- .../PublishSubscribe/StreamingSubscriptionExample/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs index 90fc934a..7988e131 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs @@ -1,4 +1,5 @@ -using Dapr.Messaging.PublishSubscribe; +using System.Text; +using Dapr.Messaging.PublishSubscribe; var daprMessagingClientBuilder = new DaprPublishSubscribeClientBuilder(); var daprMessagingClient = daprMessagingClientBuilder.Build(); @@ -9,6 +10,7 @@ async Task HandleMessage(TopicMessage message, Cancellation try { //Do something with the message + Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span)); return await Task.FromResult(TopicResponseAction.Success); } catch From d0efaed136339edbba6804bb82bb5bdc8c86d687 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 12:20:26 -0500 Subject: [PATCH 33/42] Removed singleton ConnectionManager as non-conforming to the spec. Added flag to ensure double-subscription doesn't happen to cancel stream (with multiple initial requests), updated message draining during disposal and updated unit tests. Signed-off-by: Whit Waldo --- .../PublishSubscribe/ConnectionManager.cs | 148 ------------------ .../DaprPublishSubscribeGrpcClient.cs | 9 +- .../PublishSubscribeReceiver.cs | 133 ++++++++++++++-- .../Dapr.Messaging.Test.csproj | 4 - .../PublishSubscribeReceiverTests.cs | 3 +- 5 files changed, 123 insertions(+), 174 deletions(-) delete mode 100644 src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs diff --git a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs b/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs deleted file mode 100644 index 6cb52c89..00000000 --- a/src/Dapr.Messaging/PublishSubscribe/ConnectionManager.cs +++ /dev/null @@ -1,148 +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.Threading.Channels; -using Grpc.Core; -using C = Dapr.AppCallback.Autogen.Grpc.v1; -using P = Dapr.Client.Autogen.Grpc.v1; - -namespace Dapr.Messaging.PublishSubscribe; - -/// -/// Maintains the streaming connection to the Dapr sidecar so it can be repurposed without -/// multiple callers opening separate connections. -/// -internal sealed class ConnectionManager : IAsyncDisposable -{ - /// - /// A reference to the DaprClient instance. - /// - private readonly P.Dapr.DaprClient client; - /// - /// Used to ensure thread-safe operations against the stream. - /// - private readonly SemaphoreSlim semaphore = new(1, 1); - /// - /// The stream connection between this instance and the Dapr sidecar. - /// - private AsyncDuplexStreamingCall? stream; - /// - /// Maintains the various acknowledgements for each message. - /// - /// - /// Storing the acknowledgements here so we can ensure that a single writer handles sending them back over the stream. This class - /// isn't public - /// - private readonly Channel acknowledgements = Channel.CreateUnbounded(); - - public ConnectionManager(P.Dapr.DaprClient client) - { - this.client = client; - - //Processes each acknowledgement from the channel reader as they are provided by the various PublishSubscribeReceiver instances. - Task.Run(async () => - { - await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync()) - { - await ProcessAcknowledgementAsync(acknowledgement); - } - }); - } - - /// - /// Retrieves or creates the bidirectional stream to the DaprClient for transacting pub/sub subscriptions. - /// - /// Cancellation token. - /// - public async Task> GetStreamAsync(CancellationToken cancellationToken) - { - await semaphore.WaitAsync(cancellationToken); - - try - { - return stream ??= client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); - } - finally - { - semaphore.Release(); - } - } - - /// - /// Acknowledges a provided message with an intended action to perform on it based on how it was either - /// handled or whether it timed out. - /// - /// The identifier of the message the behavior is in reference to. - /// The behavior to take on the message. - /// - /// - public async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior) - { - var action = behavior switch - { - TopicResponseAction.Success => C.TopicEventResponse.Types.TopicEventResponseStatus - .Success, - TopicResponseAction.Retry => C.TopicEventResponse.Types.TopicEventResponseStatus.Retry, - TopicResponseAction.Drop => C.TopicEventResponse.Types.TopicEventResponseStatus.Drop, - _ => throw new InvalidOperationException( - $"Unrecognized topic acknowledgement action: {behavior}") - }; - - var acknowledgement = new TopicAcknowledgement(messageId, action); - await acknowledgements.Writer.WriteAsync(acknowledgement); - } - - /// - /// Processes each of the acknowledgement messages. - /// - /// Information about the message and the action to take on it. - private async Task ProcessAcknowledgementAsync(TopicAcknowledgement acknowledgement) - { - var messageStream = await GetStreamAsync(CancellationToken.None); - await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1() - { - EventProcessed = new() - { - Id = acknowledgement.MessageId, - Status = new() - { - Status = acknowledgement.Action - } - } - }); - } - - public async ValueTask DisposeAsync() - { - //Flush the remaining acknowledgements, but start by marking the writer as complete so we don't get any more of them - acknowledgements.Writer.Complete(); - await foreach (var message in acknowledgements.Reader.ReadAllAsync()) - { - await ProcessAcknowledgementAsync(message); - } - - if (stream is not null) - { - await stream.RequestStream.CompleteAsync(); - } - - semaphore.Dispose(); - } - - /// - /// Reflects the action to take on a given message identifier. - /// - /// The identifier of the message. - /// The action to take on the message in the acknowledgement request. - private sealed record TopicAcknowledgement(string MessageId, C.TopicEventResponse.Types.TopicEventResponseStatus Action); -} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 4d0b11f3..3c1da481 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -20,17 +20,14 @@ namespace Dapr.Messaging.PublishSubscribe; /// internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - /// - /// Maintains a single connection to the Dapr dynamic subscription endpoint. - /// - private readonly ConnectionManager connectionManager; + private readonly P.DaprClient daprClient; /// /// Creates a new instance of a /// public DaprPublishSubscribeGrpcClient(P.DaprClient client) { - connectionManager = new(client); + daprClient = client; } /// @@ -42,5 +39,5 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) => new(pubSubName, topicName, options, connectionManager, messageHandler); + public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) => new(pubSubName, topicName, options, messageHandler, daprClient); } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 289d67a0..f8de9184 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System.Threading.Channels; +using Dapr.AppCallback.Autogen.Grpc.v1; using Grpc.Core; using P = Dapr.Client.Autogen.Grpc.v1; @@ -40,28 +41,44 @@ public sealed class PublishSubscribeReceiver : IAsyncDisposable /// private readonly Channel channel = Channel.CreateUnbounded(); /// + /// Maintains the various acknowledgements for each message. + /// + private readonly Channel acknowledgements = Channel.CreateUnbounded(); + /// + /// The stream connection between this instance and the Dapr sidecar. + /// + private AsyncDuplexStreamingCall? clientStream; + /// + /// Used to ensure thread-safe operations against the stream. + /// + private readonly SemaphoreSlim semaphore = new(1, 1); + /// /// The handler delegate responsible for processing the topic messages. /// private readonly TopicMessageHandler messageHandler; /// - /// Maintains the connection to the Dapr dynamic subscription endpoint. + /// A reference to the DaprClient instance. /// - private readonly ConnectionManager connectionManager; - + private readonly P.Dapr.DaprClient client; + /// + /// Flag that prevents the developer from accidentally subscribing more than once from the same receiver. + /// + private bool hasInitialized; + /// /// Constructs a new instance of a instance. /// /// The name of the Dapr Publish/Subscribe component. /// The name of the topic to subscribe to. /// Options allowing the behavior of the receiver to be configured. - /// Maintains the connection to the Dapr dynamic subscription endpoint. /// The delegate reflecting the action to take upon messages received by the subscription. - internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubscriptionOptions options, ConnectionManager connectionManager, TopicMessageHandler handler) + /// A reference to the DaprClient instance. + internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler handler, P.Dapr.DaprClient client) { + this.client = client; this.pubSubName = pubSubName; this.topicName = topicName; this.options = options; - this.connectionManager = connectionManager; this.messageHandler = handler; } @@ -72,11 +89,22 @@ internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubsc /// An containing messages provided by the sidecar. public async Task SubscribeAsync(CancellationToken cancellationToken) { - var stream = await connectionManager.GetStreamAsync(cancellationToken); - //Retrieve the messages from the sidecar and write to the channel + //Prevents the receiver from performing the subscribe operation more than once (as the multiple initialization messages would cancel the stream). + if (hasInitialized) + return; + hasInitialized = true; + + var stream = await GetStreamAsync(cancellationToken); + //Retrieve the messages from the sidecar and write to the messages channel _ = FetchDataFromSidecarAsync(stream, channel.Writer, cancellationToken); - - //Read the messages one-by-one out of the channel + + //Processes each acknowledgement from the acknowledgement channel reader as it's populated + await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync(cancellationToken)) + { + await ProcessAcknowledgementAsync(acknowledgement); + } + + //Read the messages one-by-one out of the messages channel try { while (await channel.Reader.WaitToReadAsync(cancellationToken)) @@ -112,15 +140,46 @@ public async Task SubscribeAsync(CancellationToken cancellationToken) } } + /// + /// Retrieves or creates the bidirectional stream to the DaprClient for transacting pub/sub subscriptions. + /// + /// Cancellation token. + /// + private async + Task> + GetStreamAsync(CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken); + + try + { + return clientStream ??= client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); + } + finally + { + semaphore.Release(); + } + } + /// /// Acknowledges the indicated message back to the Dapr sidecar with an indicated behavior to take on the message. /// /// The identifier of the message the behavior is in reference to. - /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. + /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. /// - private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction action) + private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior) { - await connectionManager.AcknowledgeMessageAsync(messageId, action); + var action = behavior switch + { + TopicResponseAction.Success => TopicEventResponse.Types.TopicEventResponseStatus.Success, + TopicResponseAction.Retry => TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicResponseAction.Drop => TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new InvalidOperationException( + $"Unrecognized topic acknowledgement action: {behavior}") + }; + + var acknowledgement = new TopicAcknowledgement(messageId, action); + await acknowledgements.Writer.WriteAsync(acknowledgement); } /// @@ -182,7 +241,53 @@ private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall public async ValueTask DisposeAsync() { - await connectionManager.DisposeAsync(); + //Stop processing new events channel.Writer.Complete(); + + try + { + //Process any remaining messages on the channel + await channel.Reader.Completion; + } + catch (OperationCanceledException) + { + // Handled + } + + //Flush the remaining acknowledgements, but start by marking the writer as complete + acknowledgements.Writer.Complete(); + try + { + //Process any remaining acknowledgements on the channel + await acknowledgements.Reader.Completion; + } + catch (OperationCanceledException) + { + //Handled + } + } + + /// + /// Processes each of the acknowledgement messages. + /// + /// Information about the message and action to take on it. + /// + private async Task ProcessAcknowledgementAsync(TopicAcknowledgement acknowledgement) + { + var messageStream = await GetStreamAsync(CancellationToken.None); + await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + { + EventProcessed = new() + { + Id = acknowledgement.MessageId, Status = new() { Status = acknowledgement.Action } + } + }); } + + /// + /// Reflects the action to take on a given message identifier. + /// + /// The identifier of the message. + /// The action to take on the message in the acknowledgement request. + private sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); } diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj index 65b2c071..b58307e1 100644 --- a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -33,8 +33,4 @@ - - - - \ No newline at end of file diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs index 8faf668b..0f346a39 100644 --- a/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs +++ b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs @@ -10,12 +10,11 @@ public sealed class PublishSubscribeReceiverTests public void Constructor_ShouldInitializeFields() { var daprClient = new Mock(); - var connectionManager = new ConnectionManager(daprClient.Object); var handlerMock = new Mock(); var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(30), TopicResponseAction.Retry)); - var receiver = new PublishSubscribeReceiver("pubsub", "dapr", options, connectionManager, handlerMock.Object); + var receiver = new PublishSubscribeReceiver("pubsub", "dapr", options, handlerMock.Object, daprClient.Object); Assert.NotNull(receiver); } From dd22e835dad84fd0a95ad3185515e2d1dc69407e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 12:25:46 -0500 Subject: [PATCH 34/42] Added default cancellation token value Signed-off-by: Whit Waldo --- src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index f8de9184..1624a55f 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -87,7 +87,7 @@ internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubsc /// /// Cancellation token. /// An containing messages provided by the sidecar. - public async Task SubscribeAsync(CancellationToken cancellationToken) + public async Task SubscribeAsync(CancellationToken cancellationToken = default) { //Prevents the receiver from performing the subscribe operation more than once (as the multiple initialization messages would cancel the stream). if (hasInitialized) From 521396f60b87c975c3a849f451d2401b75b94a97 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 12:31:13 -0500 Subject: [PATCH 35/42] Minor refactoring so cancellation exceptions can be handled in a single try/catch block instead of having more than one Signed-off-by: Whit Waldo --- .../PublishSubscribe/PublishSubscribeReceiver.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 1624a55f..7d5a64ff 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -98,15 +98,15 @@ public async Task SubscribeAsync(CancellationToken cancellationToken = default) //Retrieve the messages from the sidecar and write to the messages channel _ = FetchDataFromSidecarAsync(stream, channel.Writer, cancellationToken); - //Processes each acknowledgement from the acknowledgement channel reader as it's populated - await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync(cancellationToken)) - { - await ProcessAcknowledgementAsync(acknowledgement); - } - - //Read the messages one-by-one out of the messages channel try { + //Processes each acknowledgement from the acknowledgement channel reader as it's populated + await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync(cancellationToken)) + { + await ProcessAcknowledgementAsync(acknowledgement); + } + + //Read the messages one-by-one out of the messages channel while (await channel.Reader.WaitToReadAsync(cancellationToken)) { while (channel.Reader.TryRead(out var message)) From ef418c061cca6c95c008ef9c52e5a621d7906718 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 12:56:24 -0500 Subject: [PATCH 36/42] Refactoring to ensure that channels are drained successfully even if cancellation token throws. Minor perf improvments as spotted. Added/fixed comments. Signed-off-by: Whit Waldo --- .../PublishSubscribeReceiver.cs | 199 +++++++++--------- 1 file changed, 100 insertions(+), 99 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 7d5a64ff..aa9359e4 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Threading; using System.Threading.Channels; using Dapr.AppCallback.Autogen.Grpc.v1; using Grpc.Core; @@ -39,11 +40,11 @@ public sealed class PublishSubscribeReceiver : IAsyncDisposable /// /// A channel used to decouple the messages received from the sidecar to their consumption. /// - private readonly Channel channel = Channel.CreateUnbounded(); + private readonly Channel topicMessagesChannel = Channel.CreateUnbounded(); /// /// Maintains the various acknowledgements for each message. /// - private readonly Channel acknowledgements = Channel.CreateUnbounded(); + private readonly Channel acknowledgementsChannel = Channel.CreateUnbounded(); /// /// The stream connection between this instance and the Dapr sidecar. /// @@ -61,9 +62,13 @@ public sealed class PublishSubscribeReceiver : IAsyncDisposable /// private readonly P.Dapr.DaprClient client; /// - /// Flag that prevents the developer from accidentally subscribing more than once from the same receiver. + /// Flag that prevents the developer from accidentally initializing the subscription more than once from the same receiver. /// - private bool hasInitialized; + private bool hasInitialized = false; + /// + /// Flag that ensures the instance is only disposed a single time. + /// + private bool isDisposed = false; /// /// Constructs a new instance of a instance. @@ -95,48 +100,21 @@ public async Task SubscribeAsync(CancellationToken cancellationToken = default) hasInitialized = true; var stream = await GetStreamAsync(cancellationToken); + //Retrieve the messages from the sidecar and write to the messages channel - _ = FetchDataFromSidecarAsync(stream, channel.Writer, cancellationToken); + var fetchMessagesTask = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken); + + //Process the messages as they're written to either channel + var acknowledgementProcessorTask = ProcessAcknowledgementChannelMessagesAsync(cancellationToken); + var topicMessageProcessorTask = ProcessTopicChannelMessagesAsync(cancellationToken); try { - //Processes each acknowledgement from the acknowledgement channel reader as it's populated - await foreach (var acknowledgement in acknowledgements.Reader.ReadAllAsync(cancellationToken)) - { - await ProcessAcknowledgementAsync(acknowledgement); - } - - //Read the messages one-by-one out of the messages channel - while (await channel.Reader.WaitToReadAsync(cancellationToken)) - { - while (channel.Reader.TryRead(out var message)) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - - //Evaluate the message and return an acknowledgement result - var messageAction = await messageHandler(message, cts.Token); - - try - { - //Share the result with the sidecar - await AcknowledgeMessageAsync(message.Id, messageAction); - } - catch (OperationCanceledException) - { - //Acknowledge the message using the configured default response action - await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); - } - } - } + await Task.WhenAll(fetchMessagesTask, acknowledgementProcessorTask, topicMessageProcessorTask); } catch (OperationCanceledException) { - //Drain the remaining messages with the default action in the order in which they were queued - while (channel.Reader.TryRead(out var message)) - { - await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); - } + await DisposeAsync(); } } @@ -144,10 +122,8 @@ public async Task SubscribeAsync(CancellationToken cancellationToken = default) /// Retrieves or creates the bidirectional stream to the DaprClient for transacting pub/sub subscriptions. /// /// Cancellation token. - /// - private async - Task> - GetStreamAsync(CancellationToken cancellationToken) + /// The stream connection. + private async Task> GetStreamAsync(CancellationToken cancellationToken) { await semaphore.WaitAsync(cancellationToken); @@ -179,59 +155,97 @@ private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction }; var acknowledgement = new TopicAcknowledgement(messageId, action); - await acknowledgements.Writer.WriteAsync(acknowledgement); + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); } /// - /// Retrieves the subscription stream data from the Dapr sidecar. + /// Processes each acknowledgement from the acknowledgement channel reader as it's populated. /// - /// The stream connection to and from the Dream sidecar instance. - /// The channel writer instance. /// Cancellation token. - private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall stream, ChannelWriter channelWriter, CancellationToken cancellationToken) + private async Task ProcessAcknowledgementChannelMessagesAsync(CancellationToken cancellationToken) { - try - { - var initialRequest = new P.SubscribeTopicEventsRequestInitialAlpha1() - { - PubsubName = pubSubName, - DeadLetterTopic = options.DeadLetterTopic ?? string.Empty, - Topic = topicName - }; - - if (options?.Metadata.Count > 0) + var messageStream = await GetStreamAsync(cancellationToken); + await foreach (var acknowledgement in acknowledgementsChannel.Reader.ReadAllAsync(cancellationToken)) + { + await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 { - foreach (var (key, value) in options.Metadata) + EventProcessed = new() { - initialRequest.Metadata.Add(key, value); + Id = acknowledgement.MessageId, + Status = new() { Status = acknowledgement.Action } } - } + }, cancellationToken); + } + } + + /// + /// Processes each topic messages from the channel as it's populated. + /// + /// Cancellation token. + private async Task ProcessTopicChannelMessagesAsync(CancellationToken cancellationToken) + { + await foreach (var message in topicMessagesChannel.Reader.ReadAllAsync(cancellationToken)) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); - await stream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); + //Evaluate the message and return an acknowledgement result + var messageAction = await messageHandler(message, cts.Token); - await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) + try { - var message = new TopicMessage(response.EventMessage.Id, response.EventMessage.Source, response.EventMessage.Type, response.EventMessage.SpecVersion, response.EventMessage.DataContentType, response.EventMessage.Topic, response.EventMessage.PubsubName) - { - Path = response.EventMessage.Path, - Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) - }; - - await channelWriter.WriteAsync(message, cancellationToken); + //Share the result with the sidecar + await AcknowledgeMessageAsync(message.Id, messageAction); + } + catch (OperationCanceledException) + { + //Acknowledge the message using the configured default response action + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); } } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + } + + /// + /// Retrieves the subscription stream data from the Dapr sidecar. + /// + /// The stream connection to and from the Dapr sidecar instance. + /// The channel writer instance. + /// Cancellation token. + private async Task FetchDataFromSidecarAsync( + AsyncDuplexStreamingCall stream, + ChannelWriter channelWriter, CancellationToken cancellationToken) + { + //Build out the initial topic events request + var initialRequest = new P.SubscribeTopicEventsRequestInitialAlpha1() { - //Ignore our own cancellation - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && - cancellationToken.IsCancellationRequested) + PubsubName = pubSubName, DeadLetterTopic = options.DeadLetterTopic ?? string.Empty, Topic = topicName + }; + + if (options?.Metadata.Count > 0) { - //Ignore a remote cancellation due to our own cancellation + foreach (var (key, value) in options.Metadata) + { + initialRequest.Metadata.Add(key, value); + } } - finally + + //Send this request to the Dapr sidecar + await stream.RequestStream.WriteAsync( + new P.SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); + + //Each time a message is received from the stream, push it into the topic messages channel + await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) { - channel.Writer.Complete(); + var message = + new TopicMessage(response.EventMessage.Id, response.EventMessage.Source, response.EventMessage.Type, + response.EventMessage.SpecVersion, response.EventMessage.DataContentType, + response.EventMessage.Topic, response.EventMessage.PubsubName) + { + Path = response.EventMessage.Path, + Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) + }; + + await channelWriter.WriteAsync(message, cancellationToken); } } @@ -241,13 +255,17 @@ private async Task FetchDataFromSidecarAsync(AsyncDuplexStreamingCall public async ValueTask DisposeAsync() { + if (isDisposed) + return; + isDisposed = true; + //Stop processing new events - channel.Writer.Complete(); + topicMessagesChannel.Writer.Complete(); try { //Process any remaining messages on the channel - await channel.Reader.Completion; + await topicMessagesChannel.Reader.Completion; } catch (OperationCanceledException) { @@ -255,11 +273,11 @@ public async ValueTask DisposeAsync() } //Flush the remaining acknowledgements, but start by marking the writer as complete - acknowledgements.Writer.Complete(); + acknowledgementsChannel.Writer.Complete(); try { //Process any remaining acknowledgements on the channel - await acknowledgements.Reader.Completion; + await acknowledgementsChannel.Reader.Completion; } catch (OperationCanceledException) { @@ -267,23 +285,6 @@ public async ValueTask DisposeAsync() } } - /// - /// Processes each of the acknowledgement messages. - /// - /// Information about the message and action to take on it. - /// - private async Task ProcessAcknowledgementAsync(TopicAcknowledgement acknowledgement) - { - var messageStream = await GetStreamAsync(CancellationToken.None); - await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 - { - EventProcessed = new() - { - Id = acknowledgement.MessageId, Status = new() { Status = acknowledgement.Action } - } - }); - } - /// /// Reflects the action to take on a given message identifier. /// From 3addf750ca818a66ffcc39213f2b0187579c2ef2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 13:06:40 -0500 Subject: [PATCH 37/42] Cleaning up unnecessary iniitalization values, usings and null access operators Signed-off-by: Whit Waldo --- .../PublishSubscribe/PublishSubscribeReceiver.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index aa9359e4..a0262872 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Threading; using System.Threading.Channels; using Dapr.AppCallback.Autogen.Grpc.v1; using Grpc.Core; @@ -64,11 +63,11 @@ public sealed class PublishSubscribeReceiver : IAsyncDisposable /// /// Flag that prevents the developer from accidentally initializing the subscription more than once from the same receiver. /// - private bool hasInitialized = false; + private bool hasInitialized; /// /// Flag that ensures the instance is only disposed a single time. /// - private bool isDisposed = false; + private bool isDisposed; /// /// Constructs a new instance of a instance. @@ -221,7 +220,7 @@ private async Task FetchDataFromSidecarAsync( PubsubName = pubSubName, DeadLetterTopic = options.DeadLetterTopic ?? string.Empty, Topic = topicName }; - if (options?.Metadata.Count > 0) + if (options.Metadata.Count > 0) { foreach (var (key, value) in options.Metadata) { From 1cb6855720c1918b455497672dcb1df3b71b6e44 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 15:20:34 -0500 Subject: [PATCH 38/42] Modified message drain to only drain acknowledgements and even then, be constrained by a configurable timespan in how long it waits. Added some try/catch blocks to handle messages being written when the writer has been completed in case a Disposal happens mid-processing. Signed-off-by: Whit Waldo --- .../DaprSubscriptionOptions.cs | 6 ++++ .../PublishSubscribeReceiver.cs | 29 +++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs index ae5d17aa..26e16331 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs @@ -28,4 +28,10 @@ public sealed record DaprSubscriptionOptions(MessageHandlingPolicy MessageHandli /// The optional name of the dead-letter topic to send unprocessed messages to. /// public string? DeadLetterTopic { get; init; } + + /// + /// The maximum amount of time to take to dispose of acknowledgement messages after the cancellation token has + /// been signaled. + /// + public TimeSpan MaximumCleanupTimeout { get; init; } = TimeSpan.FromSeconds(30); } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index a0262872..9a92d44e 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -244,7 +244,14 @@ await stream.RequestStream.WriteAsync( Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) }; - await channelWriter.WriteAsync(message, cancellationToken); + try + { + await channelWriter.WriteAsync(message, cancellationToken); + } + catch (Exception) + { + // Handle being unable to write because the writer is completed due to an active DisposeAsync operation + } } } @@ -258,25 +265,17 @@ public async ValueTask DisposeAsync() return; isDisposed = true; - //Stop processing new events + //Stop processing new events - we'll leave any messages yet unseen as unprocessed and + //Dapr will handle as necessary when they're not acknowledged topicMessagesChannel.Writer.Complete(); - try - { - //Process any remaining messages on the channel - await topicMessagesChannel.Reader.Completion; - } - catch (OperationCanceledException) - { - // Handled - } - - //Flush the remaining acknowledgements, but start by marking the writer as complete + //Flush the remaining acknowledgements, but start by marking the writer as complete so it doesn't receive any other messages either acknowledgementsChannel.Writer.Complete(); + try { - //Process any remaining acknowledgements on the channel - await acknowledgementsChannel.Reader.Completion; + //Process any remaining acknowledgements on the channel without exceeding the configured maximum clean up time + await acknowledgementsChannel.Reader.Completion.WaitAsync(options.MaximumCleanupTimeout); } catch (OperationCanceledException) { From 462e9a16414f3fca95147ae983b52c841c6913fa Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 15:44:34 -0500 Subject: [PATCH 39/42] Updated to eliminate the double call for 'Register.SubscribeAsync' as the GrpcClient creates the PublishSubscribeReceiver, then calls SubscribeAsync internally. Renamed Register to `SubscribeAsync` and updated return type + example Signed-off-by: Whit Waldo --- .../StreamingSubscriptionExample/Program.cs | 13 +++++-------- .../PublishSubscribe/DaprPublishSubscribeClient.cs | 2 +- .../DaprPublishSubscribeGrpcClient.cs | 7 ++++++- .../PublishSubscribe/PublishSubscribeReceiver.cs | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs index 7988e131..c2a6fb21 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs @@ -19,14 +19,11 @@ async Task HandleMessage(TopicMessage message, Cancellation } } -//Create a dynamic streaming subscription -var subscription = daprMessagingClient.Register("pubsub", "myTopic", - new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(15), TopicResponseAction.Retry)), - HandleMessage, CancellationToken.None); - -//Subscribe to messages on it with a timeout of 30 seconds -var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); -await subscription.SubscribeAsync(cancellationTokenSource.Token); +//Create a dynamic streaming subscription and subscribe with a timeout of 20 seconds +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(20)); +var subscription = await daprMessagingClient.SubscribeAsync("pubsub", "myTopic", + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)), + HandleMessage, cancellationTokenSource.Token); await Task.Delay(TimeSpan.FromMinutes(1)); diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 2c0c256f..5d933a86 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -27,5 +27,5 @@ public abstract class DaprPublishSubscribeClient /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public abstract PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); + public abstract Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken); } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 3c1da481..fa552d7e 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -39,5 +39,10 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override PublishSubscribeReceiver Register(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) => new(pubSubName, topicName, options, messageHandler, daprClient); + public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken) + { + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient); + await receiver.SubscribeAsync(cancellationToken); + return receiver; + } } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 9a92d44e..7ac07de0 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -91,7 +91,7 @@ internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubsc /// /// Cancellation token. /// An containing messages provided by the sidecar. - public async Task SubscribeAsync(CancellationToken cancellationToken = default) + internal async Task SubscribeAsync(CancellationToken cancellationToken = default) { //Prevents the receiver from performing the subscribe operation more than once (as the multiple initialization messages would cancel the stream). if (hasInitialized) From f16effcd163254df3ea490fd42c8b3ceb6ec93cb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 15:48:51 -0500 Subject: [PATCH 40/42] Removed unnecessary call to DisposeAsync Signed-off-by: Whit Waldo --- src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 7ac07de0..591aa5fa 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -113,7 +113,7 @@ internal async Task SubscribeAsync(CancellationToken cancellationToken = default } catch (OperationCanceledException) { - await DisposeAsync(); + // Will be cleaned up during DisposeAsync } } From 67a637bb5ea5e2bc19f81c2b2846e6232778126a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 16:02:05 -0500 Subject: [PATCH 41/42] Updated to apply some performance improvements to the channels at instantiation. Added support to let developer specify a maximum number of messages that can be queued for processing (blocking new Dapr from submitting more to the replica) and tweaked how messages are written in the subscription loop to accommodate this. Signed-off-by: Whit Waldo --- .../PublishSubscribe/DaprSubscriptionOptions.cs | 6 ++++++ .../PublishSubscribe/PublishSubscribeReceiver.cs | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs index 26e16331..aea35d39 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs @@ -29,6 +29,12 @@ public sealed record DaprSubscriptionOptions(MessageHandlingPolicy MessageHandli /// public string? DeadLetterTopic { get; init; } + /// + /// If populated, this reflects the maximum number of messages that can be queued for processing on the replica. By default, + /// no maximum boundary is enforced. + /// + public int? MaximumQueuedMessages { get; init; } + /// /// The maximum amount of time to take to dispose of acknowledgement messages after the cancellation token has /// been signaled. diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 591aa5fa..4cc1ea80 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -24,6 +24,11 @@ namespace Dapr.Messaging.PublishSubscribe; /// public sealed class PublishSubscribeReceiver : IAsyncDisposable { + private readonly static UnboundedChannelOptions UnboundedChannelOptions = new UnboundedChannelOptions + { + SingleWriter = true, SingleReader = true + }; + /// /// The name of the Dapr pubsub component. /// @@ -39,11 +44,11 @@ public sealed class PublishSubscribeReceiver : IAsyncDisposable /// /// A channel used to decouple the messages received from the sidecar to their consumption. /// - private readonly Channel topicMessagesChannel = Channel.CreateUnbounded(); + private readonly Channel topicMessagesChannel; /// /// Maintains the various acknowledgements for each message. /// - private readonly Channel acknowledgementsChannel = Channel.CreateUnbounded(); + private readonly Channel acknowledgementsChannel = Channel.CreateUnbounded(UnboundedChannelOptions); /// /// The stream connection between this instance and the Dapr sidecar. /// @@ -84,6 +89,12 @@ internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubsc this.topicName = topicName; this.options = options; this.messageHandler = handler; + topicMessagesChannel = options.MaximumQueuedMessages is > 0 + ? Channel.CreateBounded(new BoundedChannelOptions((int)options.MaximumQueuedMessages) + { + SingleReader = true, SingleWriter = true, FullMode = BoundedChannelFullMode.Wait + }) + : Channel.CreateUnbounded(UnboundedChannelOptions); } /// @@ -246,6 +257,7 @@ await stream.RequestStream.WriteAsync( try { + await channelWriter.WaitToWriteAsync(cancellationToken); await channelWriter.WriteAsync(message, cancellationToken); } catch (Exception) From a67185d5c8bc7b1cd64cf1e3cef85dee84ac2249 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 25 Sep 2024 23:49:33 -0500 Subject: [PATCH 42/42] Added cancellation token to acknowledgement channel writer Signed-off-by: Whit Waldo --- .../PublishSubscribe/PublishSubscribeReceiver.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 4cc1ea80..7bb7b814 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -152,8 +152,9 @@ internal async Task SubscribeAsync(CancellationToken cancellationToken = default /// /// The identifier of the message the behavior is in reference to. /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. + /// Cancellation token. /// - private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior) + private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior, CancellationToken cancellationToken) { var action = behavior switch { @@ -165,7 +166,7 @@ private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction }; var acknowledgement = new TopicAcknowledgement(messageId, action); - await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement, cancellationToken); } /// @@ -205,12 +206,12 @@ private async Task ProcessTopicChannelMessagesAsync(CancellationToken cancellati try { //Share the result with the sidecar - await AcknowledgeMessageAsync(message.Id, messageAction); + await AcknowledgeMessageAsync(message.Id, messageAction, cancellationToken); } catch (OperationCanceledException) { //Acknowledge the message using the configured default response action - await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction); + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction, cancellationToken); } } }