From 8adafceeff1e0d8bfd0a9e4975bbe22c193f8651 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 22 Mar 2025 14:16:05 -0500 Subject: [PATCH 01/11] Alternative proposal for Actor PubSub implementation Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 367 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 20250322-actors-pubsub.md diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md new file mode 100644 index 0000000..6a5128b --- /dev/null +++ b/20250322-actors-pubsub.md @@ -0,0 +1,367 @@ +# Alternate Actor PubSub Implementation Proposal +- Author(s): @joshvanl, @whitwaldo + +## Overview +This proposal introduces PubSub capabilities for Dapr Actors, enabling actor instances to subscribe to routed messages +directly regardless of what provided the messages. Actors will be spawned, if they are not already, when they receive +an actor PubSub message that matches their registered routing filters. Because actors are virtual, individual actor +instances can subscribe to a PubSub topic or queue, but they will only receive the messages sent through it following +that subscription. + +This proposal differs from the [original Actors PubSub proposal](https://github.com/dapr/proposals/pull/74) in the +following ways: +- There is no designated PubSub component in actors +- Declared subscriptions to a whole actor type will only push messages to instances activated before that point +- Individual actor instances can set up their own subscriptions independent of type +- A given actor instance can subscribe to more than one PubSub component +- A given actor instance can subscribe to the same PubSub component more than once (e.g. to apply multiple filters) +- Actors can programmatically unsubscribe from streaming PubSub subscriptions +- Filtering is applied as part of the subscription so it's readily registered on the runtime for optimal routing +- Rather than change how events are sent into PubSub to specify where they go, this instead adds a subscription in +actors that require no changes to inbound events. This allows actors to subscribe to any existing PubSub events without +change even if not originally created with actor subscriptions in mind. This is useful later on when this functionality +is extended to trigger workflows and workflow events. + +This proposal seeks to implement more of a broadcast capability facilitating decoupling between the publisher and +any subscribers. The publisher does not know who is subscribing to the message(s) and it's very possible that there's +more than one subscriber across actor types and instances allowing more than one actor to react to the same message. + +This does not seek to implement actor message passing or fire-and-forget asynchronous communication. While valuable +in their own right, these ideas are reserved for a separate proposal. + +## Background +Today, Dapr provides PubSub functionality for applications but lacks native PuSub integration for actors. This limitation +means that rather than have an actor react specifically to a message it's targeting, additional logic must be written to +observe the trigger and, in turn, activate the actor to perform any necessary downstream actions. By enabling Actor PubSub, +actors can publish and subscribe directly to filtered broadcast messages, improving scalability and communication +reliability. + +### Considerations +There are a number of scenarios or use cases which could benefit from an "Actor PubSub" feature. These include implicitly +or explicitly subscribing to topics as an actor and enabling any Dapr-enabled service to publish messages that individual +actors can pick up and execute, potentially in bulk. + +Other scenarios can be considered in future iterations. + +## Implementation Details + +### Component +This proposal diverges from the original in that there is no distinct actor PubSub component. Rather, any existing Dapr +PubSub component is valid to be used as-is without changes. This limits the amount of new work necessitated by developers +to use this new capability and repurposes already existing functionality to enable new use cases. + +To be clear, this means that the original proposal's "actorpubsub" annotation on a single pubsub component would **NOT** +apply here as there would be no "central" pubsub component used. Rather, this proposal is a broadening of the existing +PubSub functionality paired specifically with existing Actor use cases. + +### Actor Type Subscription +Like the original proposal, on actor type registration, if the specified PubSub component is available, the Dapr runtime +will subscribe to the specified queue/topic indicated. This topic string will take the format of: + +``` +dapr.actors||$namespace||$actorType +``` + +Namespace is included to allow for multi-tenancy support for each PubSub broker across namespaces. The app ID is not +included as actor types transcend app IDs in a namespace. Upon un-registration of an actor type, the Dapr runtime will +unsubscribe from all registered PubSub queues/topics for that actor type. + +To address the issue of virtual actors theoretically always existing, I propose we take +[the approach of Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/streaming/?pivots=orleans-7-0#stream-semantics) +(which similarly utilizes virtual actors) and specify that it's only when an actor type or instance subscribes to a +stream, once the subscription is resolved, that it will receive all events published after that subscription. + +While Orleans' concept of rewindable streams to receive events prior to subscription is intriguing, it is outside +the scope of this proposal. + +### Actor Instance Subscription +Actors can also create subscriptions on an individual instance level to improve performance. Rather than invoking +every one of a potentially enormous number of actors to receive a message, by limiting subscriptions to individual +instances, it can dramatically lighten the load of the Dapr runtime in serving these messages, as well as the load on +clients services, especially when paired with runtime-side event routing. + +By allowing that individual actors opt-in to receiving not only any message, but allowing multiple +subscriptions even to the same pubsub source (potentially applying different filters), this simplifies the concept and +clearly identifies which actor instances need to be rehydrated when applicable messages are received. + +Today, Dapr supports applications subscribing to PubSub queue and topic events via: +- `Programmatic subscriptions` by returning the subscription config on the app channel on app health ready +- `Declarative subscriptions` by specifying subscriptions via YAML manifests in Self-Hosted and Kubernetes modes +- `Streaming subscriptions` by dynamically registering new subscriptions for receipt via gRPC-based long-lived connections + +Each of these existing concepts remain consistent through this proposal as well and will be demonstrated below. + +### Declarative Actor Subscriptions +Declarative subscriptions are described in YAML and provided both at application startup and via "hot reload" thereafter +without needing a restart. These subscriptions can be made to whole actors per the aforementioned restrictions or +to individual actor IDs. + +The file format is very similar to the style used for typical PubSub declarative subscriptions, but adds the actor type, +method and optional instance. + +This example uses a YAML component file named `my-subscription.yaml`. + +```yaml +apiVersion: dapr.io/v2alpha1 +kind: ActorSubscription +metadata: + name: order +spec: + topic: orders + routes: + actor_type: myactor + method: processorders + actor_id: + - 1 + - 2 + - 3 + - 4 + pusubname: pubsub +``` + +Here, the subscription is called `order` and: +- Uses the PubSub component called `pubsub` to subscribe to the topic called `orders` +- Defines the routes to the actors by sending all topic messages to the actor type `myactor` by invoking the method + `processorders` but only on the actor IDs `1`, `2`, `3`, and `4`. +- If the `scopes` field were set, it could further scope this subscription for access only by the apps with the specified + identifier, even if actor types existed in a broader set of apps. + +The implementation on the actor will require annotations provided by the SDKs that register the appropriate routes and +metadata at startup to accommodate these requests. This is described in the following `Programmatic` section - to be +clear, while declarative subscriptions can be modified after runtime by hot-reloading the YAML components, the use +of the static programmatic routes themselves absent this declarative component will not themselves be dynamic. This +is identical to the existing PubSub paradigm in Dapr. + + +### Programmatic Actor Instance Subscriptions +Programmatic subscriptions are declared within the actors' code and the specific implementation will vary by SDK. +Here, I describe a proposed implementation using the Dapr .NET SDK. It takes an approach similar to that of +subscriptions using the existing PubSub API in order to promote consistency and leverage existing developer investment +in Dapr familiarity. + +Because the subscription is executed only when a specific actor instance is executed, a programmatic subscription +cannot subscribe an entire actor type to the published messages. Rather, it will subscribe only the actor ID of the +instance it's running as when initialized. + +Accordingly, these subscriptions are intended to be used only within types inheriting from `Actor`. + +```cs +[Topic("pubsub", "orders")] +public async Task ProcessOrders(Order order) +{ + //Logic + return order; +} +``` + +Like the declarative subscription, this identifies a route on the actor that: +- Maps to the `pubsub` component +- Subscribes to the `orders` topic or queue nme +- Handles subscriptions using the `ProcessOrders` method implemented on the actor + +When used without a paired declarative subscription, these routes are identified exclusively at startup and are **not** +to be considered dynamic. This is the +[same guidance](https://docs.dapr.io/developing-applications/building-blocks/pubsub/subscription-methods/#programmatic-subscriptions) +currently given for Dapr PubSub programmatic subscriptions as well, so nothing has changed here. + +### Streaming Actor Subscriptions +Like programmatic actor subscriptions, streaming subscriptions are created within the actor code and the specific +implementation will vary by SDK. Here, rather than provide an example of the downstream implementation, I'll stick to +the prototype signature. Unlike programmatic subscriptions, these subscriptions can subscribe a whole actor type or +one or more actor IDs and as such, do not need to be implemented within an actor or host the actor type to use, but +can be. + +## Publish Actor Event API +There is no proposed change from the existing Dapr PubSub implementation for publishing an event into PubSub +irrespective of what is subscribed to the event. + +### Dapr Subscription Manager +Rather than change how events are published through PubSub, my proposal simply changes how the subscriptions are +set up to facilitate the potentially massive scalability concerns this capability introduces. Consider this: Today +a service may have a dozen subscriptions apiece, so across a dozen such services there are a few hundred such +subscriptions registered with Dapr. + +But by allowing subscriptions on a per-actor ID basis, a service with a dozen actor types and thousands (or millions) +of actor instances apiece, each subscribing to one or more PubSub endpoints would massively increase the number +of standing subscriptions. + +To that end, I propose that Dapr build a Subscription Manager that's semi-compliant with the current CloudEvents +[subscription specification](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md) + +A given subscription manager is responsible for one or more subscriptions assigned it, though this assignment can be +optimized to group like sources and subjects to minimize unnecessary scans for inbound events. + +The design choice has been previously made to use the Common Expression Language for routing and this proposal does +not diverge from this decision. Accordingly, as this **DOES** differ from the CloudEvents specification, the example +below changes the `filter` property to `rules` and updates the signature accordingly and puts the sink in the +`rule` body accordingly along with an optional default endpoint to which the delivery is made irrespective of any +other rules. + +A subscription, per this specification, is modeled using JSON as follows: + +```json +{ + // Required: The unique identifier of the subscription in the scope of the subscription manager + "id": "[a subscription manager scoped unique string]", + // Required: The source to which the subsription is related (in Dapr, the pubsub component name + "source": "", + // Required: The topic the subscription is configured for + "subject": "", + // Optional: An array of filter expressions that evaluate to true or false. Delivery should be performed to the + // identified sink only if the CEL expression evaluates to true. Rules are evaluated indepednently of one another. + "routes": { + "rules": [ + // If either of the `match` or `sink` properties are provided, both must be + { + // Required: The CEL expression that must evaluate as true to match on this route + "match": "", + // Required: The destination actor to which events MUST be sent if the expression is true + // The last segment, "/" is optional and should only be present for a subscription that is specific to an actor instance + "sink": "https://dapr.io/events///" + }, + // Optional: The destination actor to which events MUST be sent if the expression is true + // The last segment, "/" is optional and should only be present for a subscription that is specific to an actor instance + { + "default": "https://dapr.io/events///" + } + ] + }, + // Required: The identifier of the delivery protocol, whether HTTP or gRPC + "protocol": "HTTP" +} +``` + +Whether a PubSub subscription is implemented via the HTTP or gRPC protocols, it should result in the creation of the above +subscription so that there is no substantive difference in how one subscription is registered versus another, lessening +the burden on the runtime to juggle different approaches. + +#### Streaming Subscriptions API +New actor event subscriptions should be registered using the following proto: + +```proto +service Dapr { + rpc SubscribeActorEventAlpha1(SubscribeActorEventRequestAlpha1) returns (google.protobuf.Empty) {} +} + +// The message containing the details for subscribing an actor to a topic via streaming or otherwise +message SubscribeActorEventRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeActorTopicEventsRequestInitialApha1 initial_request = 1; + SubscribeActorTopicEventsRequestProcessedAlpah1 event_processed = 2; + } +} + +// The initial message containing the details for subscribing an actor to a topic via streaming or otherwise +message SubscribeActorTopicEventsRequestInitialAlpha1 { + // The name of the PubSub component the subscription applies to + // Required. + string pubsub_name = 1; + // The name of the topic being subscribed to. + // Required. + string topic_name = 2; + // Registers the sink to deliver the messages to. + // Required. + repeated oneof subscribe_actor_event_request_filter_type { + // Registers a filtered subscription with a CEL expression that must evaluate as true + SubscribeActorTopicEventsRequestFilterAlpha1 + // Registers a non-filtered subscription + SubscribeActorTopicSinkAlpha1 + } = 6; +} + +// Describes the CEL expression and sink to pass an actor event to when the CEL evaluates as true +message SubscribeActorTopicEventsRequestFilterAlpha1 { + // The CEL expression to evaluate + // Required. + string expression = 1; + + // The destination to send data to when the expression evaluates to true + // Required. + SubscribeActorTopicSinkAlpha1 sink = 2; +} + +// Describes the actor endpoint to deliver the actor event to +message SubscribeActorTopicSinkAlpha1 { + // The type of the actor to publish to. + // Required. + string actor_type = 1; + + // The endpoint of the actor to invoke with the published data + // Required. + string actor_method = 2; + + // The optional list of one or more actor identifiers subscribing to the event (optional). + repeated string actor_id = 3; +} + +// The message containing the subscription to a topic +message SubscribeActorTopicEventsRequestProcessedAlpha1 { + // The unique identifier of the subscription from the subscription manager + string id = 1; + + // The status of the result of the subscription request + TopicEventResponse status = 2; +} +``` + +### Event Message Routing +Upon the Daprd runtime receiving an actor PubSub message, the runtime will wrap the message with a CloudEvent envelope +as usual. The PubSub component used will be the one specified in the subscription (both component and queue/topic name). + +If no matching component exists, an appropriately typed error will be returned to the client. + +The Actor PubSub message CloudEvent envelope may look like the following example embodying JSON content: +```json +{ + "specversion": "1.0", + "type": "io.dapr.event.sent", + "source": "pubsub", + "subject": "orders", + "id": "5929aaac-a5e2-4ca1-859c-edfe73f11565", + "time": "1970-01-01T00:00:00Z", + "datacontenttype": "application/json", + "data": { + "message": "Hello, World!" + } +} +``` + +Here, this diverges from the original proposal in that it specifies the PubSub component in the `source` field, but also +puts the name of the queue or topic in the `subject` field, compliant with the CloudEvent +[specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) for this field. + +Upon the daprd runtime receiving an actor PubSub message, the runtime will unwrap the CloudEvent envelope and evaluate +the message against the registered subscriptions in the subscription manager to filter which subscriptions are registered +to this PubSub source and subject. + +At that point: +- Send the message to any actor sinks that specified a default route +- Evaluate each rule in each matching subscription and if true, send the message to the sink specified + +The data payload will be serialized just as it is for normal subscriptions today. + +#### Breaking changes from existing Dapr PubSub +When the message is successfully sent to all subscribers from daprd, the PubSub broker component should receive a +delivered response and Dapr resiliency policies should be relied on at that point to ensure that subscriptions receive +the message (or don't). This aligns with the +[delivery guarantees of Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/implementation/messaging-delivery-guarantees). + +#### Unknown implementation details +I am not familiar with how Dapr currently manages streaming subscriptions in a distributed fashion, but I would suggest +augmenting that to support this implementation. Ideally, this should be implemented so that setting up and removing +subscriptions at runtime is a lightweight operation as these subscriptions may be quite short-lived. + +#### Final notes +- All filtering should occur on the runtime to avoid activating actors unnecessarily. +- If a registered sink activates an actor that hasn't previously been activated, it should activate it like any other inbound + request would. +- PubSub invocations should follow the turn-based limitations inherent to Daor Actors already. As such, this may require + the creation of an inbox of sorts for each actor to handle queued messages pending successful acknowledgement by the + actor. + +SDKs will need to be updated to support receiving Actor PubSub messages. +No changes to SDKs will need to be made to support _sending_ PubSub messages to actors or other endpoints. + +## Feature Lifecycle Outline +- Add `SubscribeActorEventAlpha1` API +- Update SDKs to support `SubscribeActorEventAlpha1` API \ No newline at end of file From 4fac4cef74e21505acc4e8d4a9ec30aad72b567b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 25 Mar 2025 02:30:59 -0500 Subject: [PATCH 02/11] Update 20250322-actors-pubsub.md Co-authored-by: Anton Troshin Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 6a5128b..1c39a8c 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -156,7 +156,7 @@ public async Task ProcessOrders(Order order) Like the declarative subscription, this identifies a route on the actor that: - Maps to the `pubsub` component -- Subscribes to the `orders` topic or queue nme +- Subscribes to the `orders` topic or queue name - Handles subscriptions using the `ProcessOrders` method implemented on the actor When used without a paired declarative subscription, these routes are identified exclusively at startup and are **not** From 072049ba165c2952fe2e5090e6118a557c677104 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 25 Mar 2025 02:31:46 -0500 Subject: [PATCH 03/11] Update 20250322-actors-pubsub.md Co-authored-by: Anton Troshin Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 1c39a8c..ffcf3e2 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -246,8 +246,8 @@ service Dapr { // The message containing the details for subscribing an actor to a topic via streaming or otherwise message SubscribeActorEventRequestAlpha1 { oneof subscribe_topic_events_request_type { - SubscribeActorTopicEventsRequestInitialApha1 initial_request = 1; - SubscribeActorTopicEventsRequestProcessedAlpah1 event_processed = 2; + SubscribeActorTopicEventsRequestInitialAlpha1 initial_request = 1; + SubscribeActorTopicEventsRequestProcessedAlpha1 event_processed = 2; } } From a7433185c3d6d45d4b0d9b4754aca3f7aa90764c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 25 Mar 2025 02:31:53 -0500 Subject: [PATCH 04/11] Update 20250322-actors-pubsub.md Co-authored-by: Anton Troshin Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index ffcf3e2..1bb448f 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -355,7 +355,7 @@ subscriptions at runtime is a lightweight operation as these subscriptions may b - All filtering should occur on the runtime to avoid activating actors unnecessarily. - If a registered sink activates an actor that hasn't previously been activated, it should activate it like any other inbound request would. -- PubSub invocations should follow the turn-based limitations inherent to Daor Actors already. As such, this may require +- PubSub invocations should follow the turn-based limitations inherent to Dapr Actors already. As such, this may require the creation of an inbox of sorts for each actor to handle queued messages pending successful acknowledgement by the actor. From 7322ca02c24260c46cd5d587db2a8e89e8ae84bf Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 31 Mar 2025 05:08:42 -0500 Subject: [PATCH 05/11] Removed existing CEL filtering in favor of strictly using the CloudEvent subscription filtering specification. Added HTTP and updated gRPC specification for subscription management. Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 629 +++++++++++++++++++++++++++++++------- 1 file changed, 526 insertions(+), 103 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 1bb448f..cd5deb1 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -186,47 +186,58 @@ of actor instances apiece, each subscribing to one or more PubSub endpoints woul of standing subscriptions. To that end, I propose that Dapr build a Subscription Manager that's semi-compliant with the current CloudEvents -[subscription specification](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md) +[subscription specification (working draft)](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md) A given subscription manager is responsible for one or more subscriptions assigned it, though this assignment can be optimized to group like sources and subjects to minimize unnecessary scans for inbound events. -The design choice has been previously made to use the Common Expression Language for routing and this proposal does -not diverge from this decision. Accordingly, as this **DOES** differ from the CloudEvents specification, the example -below changes the `filter` property to `rules` and updates the signature accordingly and puts the sink in the -`rule` body accordingly along with an optional default endpoint to which the delivery is made irrespective of any -other rules. +While a previous iteration of this proposal suggested using the Common Expression Language already used with Dapr +PubSub, it isn't quite as flexible as the proposed filtering mechanism in the above-referenced CloudEvents subscription +specification. Rather than mix and match standards and since the decision was previously made to support CloudEvents, I +propose that we wholeheartedly embrace the CloudEvents specification and drop use of the Common Expression Language (CEL) +routing we've previously used. The rest of this proposal has been updated to reflect this change. Support for +[CloudEvents SQL expressions](https://github.com/cloudevents/spec/blob/main/cesql/spec.md) is outside of the scope of +this proposal and is reserved as a future incremental improvement for this functionality. -A subscription, per this specification, is modeled using JSON as follows: +A subscription, per this specification and using each of the required filter dialects, is modeled using JSON as follows: ```json { // Required: The unique identifier of the subscription in the scope of the subscription manager - "id": "[a subscription manager scoped unique string]", - // Required: The source to which the subsription is related (in Dapr, the pubsub component name - "source": "", - // Required: The topic the subscription is configured for - "subject": "", - // Optional: An array of filter expressions that evaluate to true or false. Delivery should be performed to the - // identified sink only if the CEL expression evaluates to true. Rules are evaluated indepednently of one another. - "routes": { - "rules": [ - // If either of the `match` or `sink` properties are provided, both must be - { - // Required: The CEL expression that must evaluate as true to match on this route - "match": "", - // Required: The destination actor to which events MUST be sent if the expression is true - // The last segment, "/" is optional and should only be present for a subscription that is specific to an actor instance - "sink": "https://dapr.io/events///" - }, - // Optional: The destination actor to which events MUST be sent if the expression is true - // The last segment, "/" is optional and should only be present for a subscription that is specific to an actor instance - { - "default": "https://dapr.io/events///" - } - ] + // This is used to perform management operations on the subscription going forward (e.g. get, update, delete) + "id": "[a subscription manager-scoped unique string]", + // Optional: Provided by the user when creating the event + "source": "/myApp/sensors/ww-12345/poll", + // Optional: The types of events the subscriber is interested in receiving. If specified on a subscription request, + // all events generated MUST have a CloudEvents type property that matches one of the provided values + "types": [ + "io.dapr.event.sent", + "io.dapr.workflow.activity.started" + ], + // Required: Required only for Dapr purposes to provide source metadata for the event (component and topic) + "config": { + // Required: The name of the PubSub component the event is sourced from + "component": "", + // Required: The name of the PubSub topic/queue the event is sourced from + "topic": "" }, - // Required: The identifier of the delivery protocol, whether HTTP or gRPC + // Optional: If specified, an array of at least one ilter expression that evaluates to true or false. Delivery + // should be performedonly if ALL of the expressions evaluate as true. + // Must support the following dialects: + // - exact: The value of the matching CloudEvent attribute MUST exactly match the specified value (case-sensitive) + // - prefix: The value of the matching CloudEvent attribute MUST exactly start with the specified value (case-sensitive) + // - suffix: The value of the matching CloudEvent attribute MUST exactly end with the specified value (case-sensitive) + // - all: Expressed as an array of at least one expression, all nested filter expressions must evaluate to true for the expression to be true + // - any: Expressed as an array of at least one expression, any of the nested filter expressions must evaluate to true for the expression to be true + // - not: The result of the filter expression is the inverse of the nested expression (e.g. if the expression evaluates as true, the result is false). + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "" is optional and should only be present for a subscription that is specific to an actor instance + "sink": "https://dapr.io/events///", + // Required: The identifier of the delivery protocol used by Dapr "protocol": "HTTP" } ``` @@ -235,72 +246,474 @@ Whether a PubSub subscription is implemented via the HTTP or gRPC protocols, it subscription so that there is no substantive difference in how one subscription is registered versus another, lessening the burden on the runtime to juggle different approaches. -#### Streaming Subscriptions API -New actor event subscriptions should be registered using the following proto: +#### Subscription Management Operations +The following are the operations that should be exposed in the API for the management of subscriptions in Dapr. -```proto +##### Creating a Subscription +It is expected that at a minimum, an implementation of this proposal includes the capability to create a subscription. + +###### HTTP Specification +Create a subscription using HTTP by defining the information contained in the subscription object defined above. + +```cURL +POST http://localhost:/v1.0/actor-subscriptions/create +``` + +Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. Rather than include +redundant parameters in the POST request, all information about the PubSub component name, the topic and other necessary +information will be derived from the request body instead. + +```json +{ + // Optional: The unique identifier of the subscription in the scope of the subscription manager. If not set by + // the developer, it should be set by the runtime when registering the subscription. + // This is used to perform management operations on the subscription going forward (e.g. get, update, delete) + "id": "[a subscription manager-scoped unique string]", + // Required: Identifies the PubSub component and topic from which the event is sourced from + "source": "/", + // Optional: The types of events the subscriber is interested in receiving. If specified on a subscription request, + // all events generated MUST have a CloudEvents type property that matches one of the provided values + "types": [ + "io.dapr.event.sent", + "io.dapr.workflow.activity.started" + ], + // Optional: If specified, an array of at least one ilter expression that evaluates to true or false. Delivery + // should be performedonly if ALL of the expressions evaluate as true. + // Must support the following dialects: + // - exact: The value of the matching CloudEvent attribute MUST exactly match the specified value (case-sensitive) + // - prefix: The value of the matching CloudEvent attribute MUST exactly start with the specified value (case-sensitive) + // - suffix: The value of the matching CloudEvent attribute MUST exactly end with the specified value (case-sensitive) + // - all: Expressed as an array of at least one expression, all nested filter expressions must evaluate to true for the expression to be true + // - any: Expressed as an array of at least one expression, any of the nested filter expressions must evaluate to true for the expression to be true + // - not: The result of the filter expression is the inverse of the nested expression (e.g. if the expression evaluates as true, the result is false). + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "" is optional and should only be present for a subscription that is specific to an actor instance + "sink": "https://dapr.io/events///" +} +``` + +The following are the HTTP response codes that should be provided in response to this request: + +| Code | Description | Notes | +| -- | -- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 202 | Accepted | - | +| 400 | Request was malformed | Where possible, the runtime should seek to provide information in the body of the response as a string value indicating what about the request was malformed so the SDK can share this with the developer. | +| 409 | ID provided is already in use by another registered subscription | Changing the ID alone is not a guarantee that the request isn't otherwise malformed and won't throw a 400 on the next attempt. | | | | +| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | + +Additionally, if the runtime returns a `202 Accepted`, the body should contain the following as a JSON value so that the +SDKs can extract the ID of the subscription even if it was not specified: +```json +{ + "id": "" +} +``` + +###### gRPC Specification +The following defines the gRPC prototypes necessary to implement this functionality at a minimum: + +```protobuf service Dapr { - rpc SubscribeActorEventAlpha1(SubscribeActorEventRequestAlpha1) returns (google.protobuf.Empty) {} + rpc SubscribeActorEventAlpha1(SubscribeActorEventRequestAlpha1) returns (SubscribeActorEventResponseAlpha1) {} } -// The message containing the details for subscribing an actor to a topic via streaming or otherwise +// The message containing the details for subscribing an actor to a topic via streaming message SubscribeActorEventRequestAlpha1 { - oneof subscribe_topic_events_request_type { - SubscribeActorTopicEventsRequestInitialAlpha1 initial_request = 1; - SubscribeActorTopicEventsRequestProcessedAlpha1 event_processed = 2; - } + // Optional: The identifier of the subscription. If not specified, this will be set by the runtime. + string subscription_id = 1; + // Required: The name of the PubSub component the subscription applied to. + string pubsub_name = 2; + // Required: The name of the topic being subscribed to. + string topic_name = 3; + // Optional: The types of events the subscription is filtered to receiving (evaluated against the CloudEvent `type` property) - must match at least one + repeated string types = 4; + // Optional: The filters to apply to the subscription - all must evaluate as true to allow event delivery to subscription + repeated oneof filters { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } = 5; + // Required: The destination actor type or instance that is subscribing to the event + string sink = 6; +} + +// Identifies a key/value pair of a CloudEvent attribute key to retrieve the value to compare to the provided +// value with as part of the filter evaluation. +message SubscribeFilterMappingAlpha1 { + // Required: The name of the CloudEvent attribute to retrieve the value from for filter evaluation + string attribute_key = 1; + // Required: The string value to perform the filter evaluation against + string value = 2; +} + +// To evaluate as true, the value of the matching CloudEvents attributes MUST all exactly match with the associated +// string value specified (case-sensitive). +message SubscribeActorExactEventFilterAlpha1 { + // Required: The source of each value that should be evaluated as part of this filter expression + repeated SubscribeFilterMappingAlpha1 filter; +} + +// To evaluate as true, the value of the matching CloudEvents attributes must all exactly match the start of the +// associated string value specified (case-sensitive). +message SubscribeActorPrefixEventFilterAlpha1 { + // Required: The source of each value that should be evaluated as part of this filter expression + repeated SubscribeFilterMappingAlpha1 filter; +} + +// To evaluate as true, the value of the matching CloudEvents attributes must all exactly match the end of the +// associated string value specified (case-sensitive). +message SubscribeActorSuffixEventFilterAlpha1 { + // Required: The source of each value that should be evaluated as part of this filter expression + repeated SubscribeFilterMappingAlpha1 filter; +} + +// To evaluate as true, each of the filter expressions provided must evaluate as true. At least one filter expression +// is required to be provided in this filter. +message SubscribeActorAllEventFilterAlpha1 { + // Required: The filters to evaluate. + repeated oneof filters { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } = 1; +} + +// To evaluate as true, at least one of the filter expressions provided must evaluate as true. At least one filter +// expression is required to be provided in this filter. +message SubscribeActorAnyEventFilterAlpha1 { + // Required: The filters to evaluate. + repeated oneof filters { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } = 1; +} + +// This must include one nested filter expression where the result of this filter is to provide the inverse of +// that filter result. For example, if the nested expression returns `true`, this should then return `false`. +message SubscribeActorNotEventFilterAlpha1 { + // Required: The filter to evaluate. + oneof filter { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } +} + +// The message containing the response to an actor subscription event request if the response returns a 202. +SubscribeActorEventResponseAlpha1 { + oneof response { + // Sent if the actor subscription is successfully registered + SubscribeActorEventSubscriptionSuccesssResponseAlpha1 + // Sent if the actor subscription is not successfully registered + SubscribeActorEventSubscriptionFailureResponseAlpha1 + } = 1 +} + +// The message containing information about the successfully registered actor pubsub subscription +SubscribeActorEventSubscriptionSuccesssResponseAlpha1 { + // Required: The identifier of the registered subscription reflecting the value provided by the user or assigned by the runtime. + string id = 1 +} + +// The message containing information about the failure to register the actor pubsub subscription +SubscribeActorEventSubscriptionFailureResponseAlpha1 { + // Optional : A message containing additional information about what went wrong. + string message = 1 +} +``` + +##### Retrieving a Subscription +It is expected that at a minimum, an implementation of this proposal will include the capability to retrieve an existing +subscription. + +###### HTTP Specification +```cURL +GET http://localhost:/v1.0/actor-subscriptions/manage/ +``` + +Note that a valid subscription ID can only contain alphanumeric characters, underscores and dashes. + +The following are the HTTP response code that should be provided in response to this request: +| Code | Description | +| -- | -- | +| 200 | Returns the subscription object in the response body as a JSON object | +| 404 | Unable to locate a subscription object with the specified actor subscription identifier | +| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | + +If the runtime returns a `200 OK`, the body should contain the following as a JSON value matching the value of the subscription +as presently registered. +```json +{ + "id": "[a subscription manager-scoped unique string]", + "source": "/", + "types": [ + "io.dapr.event.sent", + "io.dapr.workflow.activity.started" + ], + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "//" +} +``` + +###### gRPC Specification +The following defines the gRPC prototypes necessary to implement this functionality at a minimum: + +```protobuf +service Dapr { + rpc GetSubscribeActorEventAlpha1(GetSubscribeActorRequest) returns (GetSubscribeActorResponse) {} +} + +// This message contains the request to retrieve a registered Actor PubSub subscription +message GetSubscribeActorRequest { + // Required: The identifier of the Actor PubSub subscription. + string subscription_id = 1; } -// The initial message containing the details for subscribing an actor to a topic via streaming or otherwise -message SubscribeActorTopicEventsRequestInitialAlpha1 { - // The name of the PubSub component the subscription applies to - // Required. - string pubsub_name = 1; - // The name of the topic being subscribed to. - // Required. - string topic_name = 2; - // Registers the sink to deliver the messages to. - // Required. - repeated oneof subscribe_actor_event_request_filter_type { - // Registers a filtered subscription with a CEL expression that must evaluate as true - SubscribeActorTopicEventsRequestFilterAlpha1 - // Registers a non-filtered subscription - SubscribeActorTopicSinkAlpha1 - } = 6; +// This message contains the response containing a registered Actor PubSub subscription +message GetSubscribeActorResponse { + // Required: The identifier of the subscription. If not specified, this will be set by the runtime. + string subscription_id = 1; + // Required: The name of the PubSub component the subscription applied to. + string pubsub_name = 2; + // Required: The name of the topic being subscribed to. + string topic_name = 3; + // Optional: The types of events the subscription is filtered to receiving (evaluated against the CloudEvent `type` property) - must match at least one + repeated string types = 4; + // Optional: The filters to apply to the subscription - all must evaluate as true to allow event delivery to subscription + repeated oneof filters { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } = 5; + // Required: The destination actor type or instance that is subscribing to the event + string sink = 6; } +``` + +##### Deleting a Subscription +It is expected that at a minimum, an implementation of this proposal includes the capability to delete a subscription. + +###### HTTP Specification +Delete an existing Actor PubSub subscription by making a `DELETE` request to the following endpoint: + +```cURL +DELETE http://localhost:/v1.0/actor-subscriptions/manage/ +``` + +Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. + +The following are the HTTP response codes that should be provided in response to this request: + +| Code | Description | +| -- |----------------------------------------------------------| +| 202 | The subscription has been deleted with immediate effect. | +| 404 | The specified subscription identifier could not be located, so no changes were made. | +| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | + +No body should be returned in the response for any of the possible HTTP status codes. + +###### gRPC Specification +The following defines the gRPC prototypes necessary to implement this functionality at a minimum: +```protobuf +service Dapr { + rpc DeleteSubscribeActorEventAlpha1(DeleteSubscribeActorEventRequestAlpha1) returns (google.protobuf.Empty) {} +} + +// This message defines the request to make to delete a registered Actor PubSub subscription +message DeleteSubscribeActorEventRequestAlpha1 { + // Required: The identifier of the subscription to delete + string subscription_id = 1; +} +``` + +##### Updating a Subscription +While not absolutely required for an initial implementation of this proposal, having a means of updating a subscription +would be ideal and would at least halve the number of requests otherwise necessary to do the same as a workaround +(e.g. get a subscription, change the necessary properties, delete the subscription, create a subscription with the +newly updated properties). + +###### HTTP Specification +Update an existing Actor PubSub subscription by replacing the existing subscription with updated properties through a +PUT request with the changed values. Ideally, this should be preceded with a GET request to retrieve the latest +properties of the request, but this should not be enforced or validated by the runtime as it's outside of scope and +can be added later if there's appropriate demand (e.g. etag support). + +```cURL +PUT http://localhost/v1.0/actor-subscriptions/manage/ +``` + +Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. The following details +an example of the expected body of this PUT request: + +```json +{ + "id": "[a subscription manager-scoped unique string]", + "source": "/", + "types": [ + "io.dapr.event.sent", + "io.dapr.workflow.activity.started" + ], + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "//" +} +``` + +The following are the HTTP response codes that should be provided in response to this request: + +| Code | Description | +| 202 | Accepted and will take immediate effect | +| 400 | Request was malformed | Where possible, the runtime should seek to provide information in the body of the response as a string value indicating what about the request was malformed so the SDK can share this with the developer. | +| 404 | Unable to locate an existing subscription for the indicated actor subscription ID so no changes made. This should not register a new subscription as a fallback. | +| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | + -// Describes the CEL expression and sink to pass an actor event to when the CEL evaluates as true -message SubscribeActorTopicEventsRequestFilterAlpha1 { - // The CEL expression to evaluate - // Required. - string expression = 1; - - // The destination to send data to when the expression evaluates to true - // Required. - SubscribeActorTopicSinkAlpha1 sink = 2; +###### gRPC Specification +The following defines the gRPC prototypes necessary to implement this functionality at a minimum: + +```protobuf +service Dapr { + rpc UpdateSubscribeActorEventAlpha1(UpdateSubscribeActorEventRequestAlpha1) returns (SubscribeActorEventResponseAlpha1) {} +} + +// The message containing the details for updating an existing actor pubsub subscription. Note that this differs +// from SubscribeActorEventRequestAlpha1 only in that the subscription_id property is required. +message UpdateSubscribeActorEventRequestAlpha1 { + // Required: The identifier of the subscription being updated. + string subscription_id = 1; + // Required: The name of the PubSub component the subscription applied to. + string pubsub_name = 2; + // Required: The name of the topic being subscribed to. + string topic_name = 3; + // Optional: The types of events the subscription is filtered to receiving (evaluated against the CloudEvent `type` property) - must match at least one + repeated string types = 4; + // Optional: The filters to apply to the subscription - all must evaluate as true to allow event delivery to subscription + repeated oneof filters { + SubscribeActorExactEventFilterAlpha1 + SubscribeActorPrefixEventFilterAlpha1 + SubscribeActorSuffixEventFilterAlpha1 + SubscribeActorAllEventFilterAlpha1 + SubscribeActorAnyEventFilterAlpha1 + SubscribeActorNotEventFilterAlpha1 + } = 5; + // Required: The destination actor type or instance that is subscribing to the event + string sink = 6; +} +``` + +##### Query for a list of Subscriptions +While not absolutely required for an initial implementation of this proposal, having a means of getting a list of existing +subscriptions would be ideal and especially until we have more specialized states better capable of handling lists of +values, would simplify management of since-unnecessary subscriptions. This needn't include any detailed filtering in the +initial release and instead should only filter by actor type and instance, if specified at all. + +###### HTTP Specification +Query the existing Actor PubSub subscriptions by making a GET request to the following endpoint: +```cURL +// List all +GET http://localhost:/v1.0/actor-subscriptions/query + +// List by actor type +GET http://localhost:/v1.0/actor-subscriptions/query/actor/ + +// List by actor instance +GET http://localhost:/v1.0/actor-subscriptions/query/actor//instance/ +``` + +The following are the HTTP response codes that should be provided in response to this request: +| Code | Description | +| -- | -- | +| 200 | Returns the one or more subscription objects associated with the query in the response body as JSON. | +| 404 | Unable to locate any registered subscriptions for the specified endpoint filter. | +| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | + +If the runtime returns a `200 OK`, the body should contain the following as a JSON array comprised of each of the +subscription objects: + +```json +[ + { + "id": "[a subscription manager-scoped unique string]", + "source": "/", + "types": [ + "io.dapr.event.sent" + ], + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "//" + }, + { + "id": "[another subscription manager-scoped unique string]", + "source": "/", + "types": [ + "io.dapr.workflow.activity.started" + ], + "filters": [ + {"exact": {"type": "io.dapr.event.sent"}}, + {"prefix": {"type": "io.dapr.event.", "subject": "//" + } +] +``` + +##### gRPC Specification +The following defines the gRPC prototypes necessary to implement this functionality at a minimum: + +```protobuf +service Dapr { + rpc QuerySubscribeActorEventAlpha1(QueryActorEventSubscriptionRequestAlpha1) returns (QueryActorEventSubscriptionResponseAlpha1) {} } -// Describes the actor endpoint to deliver the actor event to -message SubscribeActorTopicSinkAlpha1 { - // The type of the actor to publish to. - // Required. - string actor_type = 1; - - // The endpoint of the actor to invoke with the published data - // Required. - string actor_method = 2; - - // The optional list of one or more actor identifiers subscribing to the event (optional). - repeated string actor_id = 3; +// The message used to create a query for existing Actor PubSub subscriptions +message QueryActorEventSubscriptionRequestAlpha1 { + // Optional: The actor type to filter the query by. + string actor_type = 1; + // Optional: The actor identifier to filter the query by. + string actor_id = 2; } -// The message containing the subscription to a topic -message SubscribeActorTopicEventsRequestProcessedAlpha1 { - // The unique identifier of the subscription from the subscription manager - string id = 1; - - // The status of the result of the subscription request - TopicEventResponse status = 2; +// The message containing the response to a query for existing Actor PubSub subscriptions +message QueryActorEventSubscriptionResponseAlpha1 { + // Required: Should include at least one subscription object, otherwise this query should have returned a 404 status + repeated QueryActorEventSubscriptionIdentifierAlpha1 subscriptions = 1; +} + +// The message containing the subscription identifier and actor scoping metadata +message QueryActorEventSubscriptionIdentifierAlpha1 { + // Required: The identifier of the subscription. + string subscription_id = 1; + // Required: The type of the actor the subscription is associated with. + string actor_type = 2; + // Optional: The identifier of the actor the subscription is associated with. + string actor_id = 3; } ``` @@ -313,32 +726,29 @@ If no matching component exists, an appropriately typed error will be returned t The Actor PubSub message CloudEvent envelope may look like the following example embodying JSON content: ```json { - "specversion": "1.0", + "specversion": "1.0.2", "type": "io.dapr.event.sent", - "source": "pubsub", + "source": "/", "subject": "orders", "id": "5929aaac-a5e2-4ca1-859c-edfe73f11565", "time": "1970-01-01T00:00:00Z", "datacontenttype": "application/json", - "data": { - "message": "Hello, World!" - } + "data": "{\"name\": \"something_happened\"}" } ``` -Here, this diverges from the original proposal in that it specifies the PubSub component in the `source` field, but also -puts the name of the queue or topic in the `subject` field, compliant with the CloudEvent -[specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) for this field. - -Upon the daprd runtime receiving an actor PubSub message, the runtime will unwrap the CloudEvent envelope and evaluate -the message against the registered subscriptions in the subscription manager to filter which subscriptions are registered -to this PubSub source and subject. +Here, this diverges from the original proposal in that it specifies the PubSub component and topic in the `source` +field and leaves the subject and type fields optionally populated by the developer, compliant with the CloudEvent +[specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) for each field. -At that point: -- Send the message to any actor sinks that specified a default route -- Evaluate each rule in each matching subscription and if true, send the message to the sink specified +Upon the daprd runtime receiving an actor PubSub message, the runtime will: +- Unwrap the CloudEvent envelope and evaluate which subscriptions are a match for the indicated `type` value +- Evaluate the `filters` for each matching subscription and discard those subscriptions that don't evaluate as true +for all provided expressions in the subscription filters property +- Send the message to the sink specified in each filtered subscription -The data payload will be serialized just as it is for normal subscriptions today. +The data payload should be serialized per the latest (v1.0.2) +[CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md). #### Breaking changes from existing Dapr PubSub When the message is successfully sent to all subscribers from daprd, the PubSub broker component should receive a @@ -346,15 +756,22 @@ delivered response and Dapr resiliency policies should be relied on at that poin the message (or don't). This aligns with the [delivery guarantees of Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/implementation/messaging-delivery-guarantees). +If an attempted delivery is made to a sink that features an actor type that is not registered in the placement service, +the subscription should be automatically deleted to conserve resources and the action logged for posterity. + #### Unknown implementation details I am not familiar with how Dapr currently manages streaming subscriptions in a distributed fashion, but I would suggest augmenting that to support this implementation. Ideally, this should be implemented so that setting up and removing subscriptions at runtime is a lightweight operation as these subscriptions may be quite short-lived. +As indicated in the proposed HTTP and gRPC specifications above, this implementation should diverge from the existing +Jobs API style in that creating new subscriptions with an ID that's been previously specified should not overwrite +the existing subscription. Rather, subscriptions must be explicitly deleted or otherwise updated to make changes. + #### Final notes - All filtering should occur on the runtime to avoid activating actors unnecessarily. -- If a registered sink activates an actor that hasn't previously been activated, it should activate it like any other inbound - request would. +- If a registered sink activates an actor that hasn't previously been activated, it should activate it like any other + inbound request would. - PubSub invocations should follow the turn-based limitations inherent to Dapr Actors already. As such, this may require the creation of an inbox of sorts for each actor to handle queued messages pending successful acknowledgement by the actor. @@ -364,4 +781,10 @@ No changes to SDKs will need to be made to support _sending_ PubSub messages to ## Feature Lifecycle Outline - Add `SubscribeActorEventAlpha1` API -- Update SDKs to support `SubscribeActorEventAlpha1` API \ No newline at end of file +- Update SDKs to support `SubscribeActorEventAlpha1` API + +## Changelog +| Date | Change | +| -- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 3/31/2025 | Removed existing CEL filtering in favor of strictly using CloudEvent filtering and specification per the Subscriptions API ([0.1 working draft](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md)) | +| 3/31/2025 | Added HTTP and updated gRPC specification for subscription management | \ No newline at end of file From bb49e761a7f354b309c5637941216173418a5b20 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 31 Mar 2025 05:18:14 -0500 Subject: [PATCH 06/11] Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release. Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 98 +++++++-------------------------------- 1 file changed, 16 insertions(+), 82 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index cd5deb1..2dc2ec2 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -51,7 +51,7 @@ PubSub component is valid to be used as-is without changes. This limits the amou to use this new capability and repurposes already existing functionality to enable new use cases. To be clear, this means that the original proposal's "actorpubsub" annotation on a single pubsub component would **NOT** -apply here as there would be no "central" pubsub component used. Rather, this proposal is a broadening of the existing +apply here as there would be no "central" PubSub component used. Rather, this proposal is a broadening of the existing PubSub functionality paired specifically with existing Actor use cases. ### Actor Type Subscription @@ -81,7 +81,7 @@ instances, it can dramatically lighten the load of the Dapr runtime in serving t clients services, especially when paired with runtime-side event routing. By allowing that individual actors opt-in to receiving not only any message, but allowing multiple -subscriptions even to the same pubsub source (potentially applying different filters), this simplifies the concept and +subscriptions even to the same PubSub source (potentially applying different filters), this simplifies the concept and clearly identifies which actor instances need to be rehydrated when applicable messages are received. Today, Dapr supports applications subscribing to PubSub queue and topic events via: @@ -89,87 +89,20 @@ Today, Dapr supports applications subscribing to PubSub queue and topic events v - `Declarative subscriptions` by specifying subscriptions via YAML manifests in Self-Hosted and Kubernetes modes - `Streaming subscriptions` by dynamically registering new subscriptions for receipt via gRPC-based long-lived connections -Each of these existing concepts remain consistent through this proposal as well and will be demonstrated below. - -### Declarative Actor Subscriptions -Declarative subscriptions are described in YAML and provided both at application startup and via "hot reload" thereafter -without needing a restart. These subscriptions can be made to whole actors per the aforementioned restrictions or -to individual actor IDs. - -The file format is very similar to the style used for typical PubSub declarative subscriptions, but adds the actor type, -method and optional instance. - -This example uses a YAML component file named `my-subscription.yaml`. - -```yaml -apiVersion: dapr.io/v2alpha1 -kind: ActorSubscription -metadata: - name: order -spec: - topic: orders - routes: - actor_type: myactor - method: processorders - actor_id: - - 1 - - 2 - - 3 - - 4 - pusubname: pubsub -``` - -Here, the subscription is called `order` and: -- Uses the PubSub component called `pubsub` to subscribe to the topic called `orders` -- Defines the routes to the actors by sending all topic messages to the actor type `myactor` by invoking the method - `processorders` but only on the actor IDs `1`, `2`, `3`, and `4`. -- If the `scopes` field were set, it could further scope this subscription for access only by the apps with the specified - identifier, even if actor types existed in a broader set of apps. - -The implementation on the actor will require annotations provided by the SDKs that register the appropriate routes and -metadata at startup to accommodate these requests. This is described in the following `Programmatic` section - to be -clear, while declarative subscriptions can be modified after runtime by hot-reloading the YAML components, the use -of the static programmatic routes themselves absent this declarative component will not themselves be dynamic. This -is identical to the existing PubSub paradigm in Dapr. - - -### Programmatic Actor Instance Subscriptions -Programmatic subscriptions are declared within the actors' code and the specific implementation will vary by SDK. -Here, I describe a proposed implementation using the Dapr .NET SDK. It takes an approach similar to that of -subscriptions using the existing PubSub API in order to promote consistency and leverage existing developer investment -in Dapr familiarity. - -Because the subscription is executed only when a specific actor instance is executed, a programmatic subscription -cannot subscribe an entire actor type to the published messages. Rather, it will subscribe only the actor ID of the -instance it's running as when initialized. - -Accordingly, these subscriptions are intended to be used only within types inheriting from `Actor`. - -```cs -[Topic("pubsub", "orders")] -public async Task ProcessOrders(Order order) -{ - //Logic - return order; -} -``` - -Like the declarative subscription, this identifies a route on the actor that: -- Maps to the `pubsub` component -- Subscribes to the `orders` topic or queue name -- Handles subscriptions using the `ProcessOrders` method implemented on the actor - -When used without a paired declarative subscription, these routes are identified exclusively at startup and are **not** -to be considered dynamic. This is the -[same guidance](https://docs.dapr.io/developing-applications/building-blocks/pubsub/subscription-methods/#programmatic-subscriptions) -currently given for Dapr PubSub programmatic subscriptions as well, so nothing has changed here. +After discussion with other maintainers in the last couple of sync calls, this proposal has been narrowed to only +support streaming subscriptions in its initial implementation. Future need for Programmatic or Declarative subscriptions +can be evaluated for a future Dapr release. ### Streaming Actor Subscriptions -Like programmatic actor subscriptions, streaming subscriptions are created within the actor code and the specific -implementation will vary by SDK. Here, rather than provide an example of the downstream implementation, I'll stick to -the prototype signature. Unlike programmatic subscriptions, these subscriptions can subscribe a whole actor type or -one or more actor IDs and as such, do not need to be implemented within an actor or host the actor type to use, but -can be. +Like existing Dapr streaming PubSub subscriptions, such subscriptions are created to apply to actor code and the specific +implementation will vary by SDK. Unlike programmatic subscriptions, these subscriptions can subscribe a whole actor +type or one or more actor IDs and as such, do not need to be within an actor or even host the actor type to be +registered, but can be. + +Such subscriptions are created via either an HTTP or gRPC implementation as described in the implementation section +later in this proposal and resemble the existing streaming pubsub subscription model with changes to support the latest +versions of the [(WIP) CloudEvents subscription specification](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md) +and 1.0.2, of the [CloudEvents message specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md). ## Publish Actor Event API There is no proposed change from the existing Dapr PubSub implementation for publishing an event into PubSub @@ -787,4 +720,5 @@ No changes to SDKs will need to be made to support _sending_ PubSub messages to | Date | Change | | -- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 3/31/2025 | Removed existing CEL filtering in favor of strictly using CloudEvent filtering and specification per the Subscriptions API ([0.1 working draft](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md)) | -| 3/31/2025 | Added HTTP and updated gRPC specification for subscription management | \ No newline at end of file +| 3/31/2025 | Added HTTP and updated gRPC specification for subscription management | +| 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | \ No newline at end of file From 900358b37cffcaba87ef5162f69a6b00905493ed Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 31 Mar 2025 05:26:55 -0500 Subject: [PATCH 07/11] Tweaked feature lifecycle section Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 2dc2ec2..dd2eb9e 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -713,8 +713,8 @@ SDKs will need to be updated to support receiving Actor PubSub messages. No changes to SDKs will need to be made to support _sending_ PubSub messages to actors or other endpoints. ## Feature Lifecycle Outline -- Add `SubscribeActorEventAlpha1` API -- Update SDKs to support `SubscribeActorEventAlpha1` API +- Add new prototype implementation to runtime +- Update SDKs to support new API and protos ## Changelog | Date | Change | From 318e8799d1a85ab1f783da2269510eb5413197b0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 31 Mar 2025 05:28:17 -0500 Subject: [PATCH 08/11] Fixed changelog table layout Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index dd2eb9e..22fc85d 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -717,8 +717,8 @@ No changes to SDKs will need to be made to support _sending_ PubSub messages to - Update SDKs to support new API and protos ## Changelog -| Date | Change | -| -- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Date | Change | +| -- | -- | | 3/31/2025 | Removed existing CEL filtering in favor of strictly using CloudEvent filtering and specification per the Subscriptions API ([0.1 working draft](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md)) | -| 3/31/2025 | Added HTTP and updated gRPC specification for subscription management | -| 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | \ No newline at end of file +| 3/31/2025 | Added HTTP and updated gRPC specification for subscription management +| 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | \ No newline at end of file From 11e1aa31e52bc0eb142dae91d432cfb00fd1f653 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 8 Apr 2025 09:42:31 -0500 Subject: [PATCH 09/11] Removed HTTP API from proposal Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 211 ++------------------------------------ 1 file changed, 7 insertions(+), 204 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 22fc85d..3f45df4 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -180,72 +180,15 @@ subscription so that there is no substantive difference in how one subscription the burden on the runtime to juggle different approaches. #### Subscription Management Operations -The following are the operations that should be exposed in the API for the management of subscriptions in Dapr. +The following are the operations that should be exposed in the API for the management of subscriptions in Dapr. While a +previous iteration of this proposal provided for a REST-like HTTP API, this has since been modified following further +discussion to instead favor a gRPC-only implementation as it's a more natural fit for our goals here: +- A streaming connection requires that there be a specific client on the other end to receive messages +- It allows for long-lived bidirectional connections improving performance ##### Creating a Subscription It is expected that at a minimum, an implementation of this proposal includes the capability to create a subscription. -###### HTTP Specification -Create a subscription using HTTP by defining the information contained in the subscription object defined above. - -```cURL -POST http://localhost:/v1.0/actor-subscriptions/create -``` - -Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. Rather than include -redundant parameters in the POST request, all information about the PubSub component name, the topic and other necessary -information will be derived from the request body instead. - -```json -{ - // Optional: The unique identifier of the subscription in the scope of the subscription manager. If not set by - // the developer, it should be set by the runtime when registering the subscription. - // This is used to perform management operations on the subscription going forward (e.g. get, update, delete) - "id": "[a subscription manager-scoped unique string]", - // Required: Identifies the PubSub component and topic from which the event is sourced from - "source": "/", - // Optional: The types of events the subscriber is interested in receiving. If specified on a subscription request, - // all events generated MUST have a CloudEvents type property that matches one of the provided values - "types": [ - "io.dapr.event.sent", - "io.dapr.workflow.activity.started" - ], - // Optional: If specified, an array of at least one ilter expression that evaluates to true or false. Delivery - // should be performedonly if ALL of the expressions evaluate as true. - // Must support the following dialects: - // - exact: The value of the matching CloudEvent attribute MUST exactly match the specified value (case-sensitive) - // - prefix: The value of the matching CloudEvent attribute MUST exactly start with the specified value (case-sensitive) - // - suffix: The value of the matching CloudEvent attribute MUST exactly end with the specified value (case-sensitive) - // - all: Expressed as an array of at least one expression, all nested filter expressions must evaluate to true for the expression to be true - // - any: Expressed as an array of at least one expression, any of the nested filter expressions must evaluate to true for the expression to be true - // - not: The result of the filter expression is the inverse of the nested expression (e.g. if the expression evaluates as true, the result is false). - "filters": [ - {"exact": {"type": "io.dapr.event.sent"}}, - {"prefix": {"type": "io.dapr.event.", "subject": "" is optional and should only be present for a subscription that is specific to an actor instance - "sink": "https://dapr.io/events///" -} -``` - -The following are the HTTP response codes that should be provided in response to this request: - -| Code | Description | Notes | -| -- | -- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 202 | Accepted | - | -| 400 | Request was malformed | Where possible, the runtime should seek to provide information in the body of the response as a string value indicating what about the request was malformed so the SDK can share this with the developer. | -| 409 | ID provided is already in use by another registered subscription | Changing the ID alone is not a guarantee that the request isn't otherwise malformed and won't throw a 400 on the next attempt. | | | | -| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | - -Additionally, if the runtime returns a `202 Accepted`, the body should contain the following as a JSON value so that the -SDKs can extract the ID of the subscription even if it was not specified: -```json -{ - "id": "" -} -``` - ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -376,38 +319,6 @@ SubscribeActorEventSubscriptionFailureResponseAlpha1 { It is expected that at a minimum, an implementation of this proposal will include the capability to retrieve an existing subscription. -###### HTTP Specification -```cURL -GET http://localhost:/v1.0/actor-subscriptions/manage/ -``` - -Note that a valid subscription ID can only contain alphanumeric characters, underscores and dashes. - -The following are the HTTP response code that should be provided in response to this request: -| Code | Description | -| -- | -- | -| 200 | Returns the subscription object in the response body as a JSON object | -| 404 | Unable to locate a subscription object with the specified actor subscription identifier | -| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | - -If the runtime returns a `200 OK`, the body should contain the following as a JSON value matching the value of the subscription -as presently registered. -```json -{ - "id": "[a subscription manager-scoped unique string]", - "source": "/", - "types": [ - "io.dapr.event.sent", - "io.dapr.workflow.activity.started" - ], - "filters": [ - {"exact": {"type": "io.dapr.event.sent"}}, - {"prefix": {"type": "io.dapr.event.", "subject": "//" -} -``` - ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -449,25 +360,6 @@ message GetSubscribeActorResponse { ##### Deleting a Subscription It is expected that at a minimum, an implementation of this proposal includes the capability to delete a subscription. -###### HTTP Specification -Delete an existing Actor PubSub subscription by making a `DELETE` request to the following endpoint: - -```cURL -DELETE http://localhost:/v1.0/actor-subscriptions/manage/ -``` - -Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. - -The following are the HTTP response codes that should be provided in response to this request: - -| Code | Description | -| -- |----------------------------------------------------------| -| 202 | The subscription has been deleted with immediate effect. | -| 404 | The specified subscription identifier could not be located, so no changes were made. | -| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | - -No body should be returned in the response for any of the possible HTTP status codes. - ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: ```protobuf @@ -488,44 +380,6 @@ would be ideal and would at least halve the number of requests otherwise necessa (e.g. get a subscription, change the necessary properties, delete the subscription, create a subscription with the newly updated properties). -###### HTTP Specification -Update an existing Actor PubSub subscription by replacing the existing subscription with updated properties through a -PUT request with the changed values. Ideally, this should be preceded with a GET request to retrieve the latest -properties of the request, but this should not be enforced or validated by the runtime as it's outside of scope and -can be added later if there's appropriate demand (e.g. etag support). - -```cURL -PUT http://localhost/v1.0/actor-subscriptions/manage/ -``` - -Note that the subscription ID can only contain alphanumeric characters, underscores and dashes. The following details -an example of the expected body of this PUT request: - -```json -{ - "id": "[a subscription manager-scoped unique string]", - "source": "/", - "types": [ - "io.dapr.event.sent", - "io.dapr.workflow.activity.started" - ], - "filters": [ - {"exact": {"type": "io.dapr.event.sent"}}, - {"prefix": {"type": "io.dapr.event.", "subject": "//" -} -``` - -The following are the HTTP response codes that should be provided in response to this request: - -| Code | Description | -| 202 | Accepted and will take immediate effect | -| 400 | Request was malformed | Where possible, the runtime should seek to provide information in the body of the response as a string value indicating what about the request was malformed so the SDK can share this with the developer. | -| 404 | Unable to locate an existing subscription for the indicated actor subscription ID so no changes made. This should not register a new subscription as a fallback. | -| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | - - ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -565,58 +419,6 @@ subscriptions would be ideal and especially until we have more specialized state values, would simplify management of since-unnecessary subscriptions. This needn't include any detailed filtering in the initial release and instead should only filter by actor type and instance, if specified at all. -###### HTTP Specification -Query the existing Actor PubSub subscriptions by making a GET request to the following endpoint: -```cURL -// List all -GET http://localhost:/v1.0/actor-subscriptions/query - -// List by actor type -GET http://localhost:/v1.0/actor-subscriptions/query/actor/ - -// List by actor instance -GET http://localhost:/v1.0/actor-subscriptions/query/actor//instance/ -``` - -The following are the HTTP response codes that should be provided in response to this request: -| Code | Description | -| -- | -- | -| 200 | Returns the one or more subscription objects associated with the query in the response body as JSON. | -| 404 | Unable to locate any registered subscriptions for the specified endpoint filter. | -| 500 | The request appears to be formatted correctly, but there was an error in the Dapr runtime. | - -If the runtime returns a `200 OK`, the body should contain the following as a JSON array comprised of each of the -subscription objects: - -```json -[ - { - "id": "[a subscription manager-scoped unique string]", - "source": "/", - "types": [ - "io.dapr.event.sent" - ], - "filters": [ - {"exact": {"type": "io.dapr.event.sent"}}, - {"prefix": {"type": "io.dapr.event.", "subject": "//" - }, - { - "id": "[another subscription manager-scoped unique string]", - "source": "/", - "types": [ - "io.dapr.workflow.activity.started" - ], - "filters": [ - {"exact": {"type": "io.dapr.event.sent"}}, - {"prefix": {"type": "io.dapr.event.", "subject": "//" - } -] -``` - ##### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -721,4 +523,5 @@ No changes to SDKs will need to be made to support _sending_ PubSub messages to | -- | -- | | 3/31/2025 | Removed existing CEL filtering in favor of strictly using CloudEvent filtering and specification per the Subscriptions API ([0.1 working draft](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md)) | | 3/31/2025 | Added HTTP and updated gRPC specification for subscription management -| 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | \ No newline at end of file +| 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | +| 4/8/2025 | Removed HTTP API implementation in favor of making this proposal strictly gRPC-based | \ No newline at end of file From 588962c0051769943cff31d8a621fe7c5e6a089b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 8 Apr 2025 10:26:47 -0500 Subject: [PATCH 10/11] Added language clarifying event deduplication and why this doesn't allow multiple sinks per subscription. That said, this does support bulk subscription deletion. Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 3f45df4..8b58110 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -188,6 +188,20 @@ discussion to instead favor a gRPC-only implementation as it's a more natural fi ##### Creating a Subscription It is expected that at a minimum, an implementation of this proposal includes the capability to create a subscription. +More than one sink should be allowed to be specified as part of this message so that multiple actor types and/or actor +instance IDs can be subscribed to in a single request. + +While at first glance, it looks like there's an opportunity for deduplication of subscriptions on this front, I would +urge caution in doing so. For example, different subscriptions may specify a sink pointing to a broad actor type and +still others may specify individual actor IDs of that type, after consultation with @joshvanl, I would propose that +by default all deduplication should occur when a message is received by the runtime as part of the delivery mechanism. +This is discussed further in [this section](#event-message-routing). + +Moreover, while the presence of multiple sinks was considered, as these would be tied to the same `subscription_id` +going forward and the sinks themselves not individually managed, I would propose that this over-complicates deduplication, +sourcing and management operations. Requiring a one-to-one relationship of one sink to one `subscription_id` simplifies +this on all fronts. + ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -358,7 +372,8 @@ message GetSubscribeActorResponse { ``` ##### Deleting a Subscription -It is expected that at a minimum, an implementation of this proposal includes the capability to delete a subscription. +It is expected that at a minimum, an implementation of this proposal includes the capability to delete one or +more subscriptions at a time. ###### gRPC Specification The following defines the gRPC prototypes necessary to implement this functionality at a minimum: @@ -367,10 +382,10 @@ service Dapr { rpc DeleteSubscribeActorEventAlpha1(DeleteSubscribeActorEventRequestAlpha1) returns (google.protobuf.Empty) {} } -// This message defines the request to make to delete a registered Actor PubSub subscription +// This message defines the request to make to delete one or more registered Actor PubSub subscriptions message DeleteSubscribeActorEventRequestAlpha1 { - // Required: The identifier of the subscription to delete - string subscription_id = 1; + // Required: The identifier(s) of the subscription(s) to delete + repeated string subscription_id = 1; } ``` @@ -480,6 +495,9 @@ Upon the daprd runtime receiving an actor PubSub message, the runtime will: - Unwrap the CloudEvent envelope and evaluate which subscriptions are a match for the indicated `type` value - Evaluate the `filters` for each matching subscription and discard those subscriptions that don't evaluate as true for all provided expressions in the subscription filters property +- If there is more than one matching sink for successfully evaluated filters for any given actor ID (e.g. there exists +a subscription for both the actor ID and its actor type for the same method), the sinks should be deduplicated so the +message is sent to each actor ID only a single time per method. - Send the message to the sink specified in each filtered subscription The data payload should be serialized per the latest (v1.0.2) @@ -524,4 +542,6 @@ No changes to SDKs will need to be made to support _sending_ PubSub messages to | 3/31/2025 | Removed existing CEL filtering in favor of strictly using CloudEvent filtering and specification per the Subscriptions API ([0.1 working draft](https://github.com/cloudevents/spec/blob/main/subscriptions/spec.md)) | | 3/31/2025 | Added HTTP and updated gRPC specification for subscription management | 3/31/2025 | Removed programmatic and declarative subscription types in favor of only supporting streaming subscriptions in this initial release | -| 4/8/2025 | Removed HTTP API implementation in favor of making this proposal strictly gRPC-based | \ No newline at end of file +| 4/8/2025 | Removed HTTP API implementation in favor of making this proposal strictly gRPC-based | +| 4/8/2025 | Added language clarifying event deduplication and why multiple sinks per subscription isn't part of proposal | +| 4/8/2025 | Supports bulk subscription deletion | \ No newline at end of file From 1054ad256198ecb22b6c970d53de17baede8889a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 15 Apr 2025 16:13:16 -0500 Subject: [PATCH 11/11] Fixed typo Signed-off-by: Whit Waldo --- 20250322-actors-pubsub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/20250322-actors-pubsub.md b/20250322-actors-pubsub.md index 8b58110..8d09236 100644 --- a/20250322-actors-pubsub.md +++ b/20250322-actors-pubsub.md @@ -154,7 +154,7 @@ A subscription, per this specification and using each of the required filter dia // Required: The name of the PubSub topic/queue the event is sourced from "topic": "" }, - // Optional: If specified, an array of at least one ilter expression that evaluates to true or false. Delivery + // Optional: If specified, an array of at least one filter expression that evaluates to true or false. Delivery // should be performedonly if ALL of the expressions evaluate as true. // Must support the following dialects: // - exact: The value of the matching CloudEvent attribute MUST exactly match the specified value (case-sensitive)