From f92968f33400c4d3ad047fa45bebafeed7f25059 Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Thu, 21 May 2026 16:43:18 +0100 Subject: [PATCH 1/4] actors: document app-initiated gRPC event streams (Alpha1) Add documentation for SubscribeActorEventsAlpha1, the new bidirectional gRPC stream that lets actor-hosting apps receive all callbacks (invoke, reminder, timer, deactivate) over an app-initiated connection to daprd, without exposing an inbound server port. Closes dapr/dapr#927. Introduced in Dapr v1.18 via dapr/dapr#9812. Files added/changed: - actors-app-initiated-streams.md: concept page explaining the protocol, the motivation, how it differs from traditional callbacks, and NetworkPolicy / firewall guidance for operators. - howto-actors-app-initiated-streams.md: how-to guide with step-by-step instructions and raw gRPC code examples in Go. - actors_api.md: new SubscribeActorEventsAlpha1 gRPC section with full proto reference tables for all request/response message types. - alpha-beta-apis.md: add row for the new alpha API. Signed-off-by: Nelson Parente --- .../actors/actors-app-initiated-streams.md | 129 +++++++ .../howto-actors-app-initiated-streams.md | 327 ++++++++++++++++++ .../en/operations/support/alpha-beta-apis.md | 1 + .../content/en/reference/api/actors_api.md | 130 +++++++ 4 files changed, 587 insertions(+) create mode 100644 daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md create mode 100644 daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md new file mode 100644 index 00000000000..4f391262f8b --- /dev/null +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md @@ -0,0 +1,129 @@ +--- +type: docs +title: "Actor app-initiated gRPC event streams (Alpha)" +linkTitle: "App-initiated streams (Alpha)" +weight: 55 +description: "Receive actor callbacks over an app-initiated gRPC stream without exposing a server port" +--- + +{{% alert title="Alpha" color="warning" %}} +The `SubscribeActorEventsAlpha1` API is in **alpha**. The API shape may change in a future release. Do not use it in production workloads without accepting that risk. +{{% /alert %}} + +## Overview + +By default, Dapr delivers actor callbacks — method invocations, reminders, timers, and deactivations — by calling **inbound** HTTP or gRPC endpoints on the application. This requires each actor-hosting pod to expose a server port that the Dapr sidecar can reach. + +Starting with Dapr v1.18, actor hosts can instead open a **single bidirectional gRPC stream from the app to the sidecar** (`SubscribeActorEventsAlpha1`) and receive all four callback types over that one connection. The app is the gRPC _client_; it dials daprd, opens the stream, and waits for callbacks to arrive. No inbound port is required. + +This pattern closes the long-standing feature request [dapr/dapr#927](https://github.com/dapr/dapr/issues/927) and aligns actor callback delivery with how Dapr already handles: + +- Pub/sub streaming subscriptions (`SubscribeTopicEventsAlpha1`) +- Configuration watch streams +- Scheduler job streams + +## Why app-initiated streams? + +| | Traditional callbacks | App-initiated stream | +|---|---|---| +| **Connection direction** | sidecar → app | app → sidecar | +| **App server port required** | Yes | No | +| **NetworkPolicy / firewall** | Must allow sidecar→app inbound | Only app→sidecar outbound needed | +| **Callback types** | Separate endpoints per type | All four types on one stream | +| **SDK support** | All SDKs | SDKs adding support (see below) | +| **Stability** | Stable | Alpha (v1.18+) | + +The app-initiated approach is especially useful in environments where: + +- NetworkPolicies restrict inbound traffic to application pods. +- Actors run in serverless or restricted-networking environments. +- You want a single connection-management surface instead of per-callback routes. + +## How it works + +The protocol follows a request–response pairing over a bidirectional gRPC stream: + +1. **App opens the stream.** The app calls `SubscribeActorEventsAlpha1` on the Dapr gRPC service and sends an initial registration message (`SubscribeActorEventsRequestInitialAlpha1`) listing the actor types it hosts, together with optional runtime configuration overrides (idle timeout, drain settings, reentrancy). + +2. **Dapr acknowledges registration.** daprd responds with a `SubscribeActorEventsResponseInitialAlpha1` on the stream. An empty message body signals success; errors surface as a gRPC stream error. + +3. **Dapr sends callbacks.** Whenever an actor method is invoked, a reminder or timer fires, or an actor is deactivated, daprd sends a `SubscribeActorEventsResponseAlpha1` message down the stream. Each message carries a unique correlation `id`. + +4. **App responds.** The app processes the callback and sends back a `SubscribeActorEventsRequestAlpha1` message containing the matching `id`. The response type determines the action: + + | Callback received | App sends back | + |---|---| + | `invoke_request` (method call) | `invoke_response` with response payload | + | `reminder_request` | `reminder_response` (optionally `cancel: true` to stop the reminder) | + | `timer_request` | `timer_response` (optionally `cancel: true` to stop the timer) | + | `deactivate_request` | `deactivate_response` (ack only, no payload) | + +5. **Error signaling.** If the app cannot handle a callback (for example, the actor method does not exist), it sends a `request_failed` message with the originating `id`, a gRPC status code, and an optional message. daprd maps `codes.NotFound` to a permanent non-retryable failure. + +### Reconnection and rolling restarts + +Dapr supports multiple concurrent streams from the same app process. This enables zero-downtime rolling restarts: + +- When a new pod opens a stream, daprd routes all **new** callbacks to the newest connection. +- The **older** connection continues to receive responses for callbacks it already sent; it drains naturally. +- Once all in-flight work on an older connection completes, that connection can be closed safely. + +Apps should reconnect with exponential back-off if the stream is interrupted. + +## NetworkPolicy and firewall considerations + +Because the app **initiates** the connection, the traffic direction is: + +``` +app pod → daprd sidecar (gRPC port, default 50001) +``` + +In environments with restrictive NetworkPolicies, this means you no longer need a rule that allows the sidecar to initiate inbound connections to the app pod. However, you do need egress from the app pod to the sidecar's gRPC port. + +{{% alert title="Operator note" color="primary" %}} +If you previously locked down actor-hosting pods by denying all inbound traffic from the sidecar, you can remove that inbound rule for pods that use `SubscribeActorEventsAlpha1`. Keep the rule in place if the same pods also use traditional HTTP/gRPC actor callbacks (the two modes can coexist during migration). + +The daprd gRPC port is configurable via the `dapr.io/grpc-port` annotation (default: `50001`). Ensure egress from app pods to that port on `localhost` / the sidecar is permitted. +{{% /alert %}} + +### Example NetworkPolicy (Kubernetes) + +The following policy allows app pods with the label `app: my-actor-service` to reach the sidecar gRPC port while denying all other inbound traffic to the app pod: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: actor-app-initiated-stream +spec: + podSelector: + matchLabels: + app: my-actor-service + policyTypes: + - Ingress + - Egress + ingress: [] # no inbound rules required for app-initiated streams + egress: + - ports: + - protocol: TCP + port: 50001 # daprd gRPC port (adjust if changed via annotation) +``` + +## SDK support + +SDK-level support for `SubscribeActorEventsAlpha1` is being added in the v1.18 SDK releases. Refer to the documentation for each SDK: + +- [.NET SDK actors]({{% ref "dotnet-actors" %}}) +- [Java SDK actors]({{% ref "java#actors" %}}) +- [Python SDK actors]({{% ref "python-actor" %}}) +- [Go SDK actors]({{% ref "go-sdk-service" %}}) + +Until SDK support is complete, the API is accessible directly via the generated gRPC client. See the [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}) for a raw gRPC example. + +## Related links + +- [How-to: Use actor app-initiated gRPC streams]({{% ref "howto-actors-app-initiated-streams" %}}) +- [Actor API reference — SubscribeActorEventsAlpha1]({{% ref "actors_api#subscribeactoreventsalpha1-grpc" %}}) +- [Actors overview]({{% ref "actors-overview" %}}) +- [Actor runtime configuration]({{% ref "actors-runtime-config" %}}) +- [Runtime PR dapr/dapr#9812](https://github.com/dapr/dapr/pull/9812) diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md new file mode 100644 index 00000000000..845453f259b --- /dev/null +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md @@ -0,0 +1,327 @@ +--- +type: docs +title: "How-to: Use actor app-initiated gRPC event streams" +linkTitle: "How-to: App-initiated streams" +weight: 56 +description: "Open a gRPC stream from your app to daprd to receive actor callbacks without exposing a server port" +--- + +{{% alert title="Alpha" color="warning" %}} +The `SubscribeActorEventsAlpha1` API is in **alpha**. The API shape may change in a future release. +{{% /alert %}} + +This guide shows how to use the `SubscribeActorEventsAlpha1` gRPC stream so your actor host application receives all four callback types — method invocations, reminders, timers, and deactivations — over a single app-initiated connection to the Dapr sidecar. + +Read the [concept doc]({{% ref "actors-app-initiated-streams" %}}) first to understand the protocol and when to use this approach. + +## Prerequisites + +- [Dapr v1.18 or later]({{% ref "getting-started" %}}) +- A state store configured for actors (see [actors overview]({{% ref "actors-overview" %}}) for requirements) +- The Dapr proto definitions for your language (Go example uses `github.com/dapr/dapr/pkg/proto/runtime/v1`) + +## Protocol summary + +``` +App daprd (Dapr sidecar) + | | + |-- SubscribeActorEventsAlpha1() ---------->| open stream + | | + |-- SubscribeActorEventsRequestInitialAlpha1 -->| register actor types + config + |<-- SubscribeActorEventsResponseInitialAlpha1 --| ack (empty = success) + | | + |<-- invoke_request (id="abc", method="x") --| callback + |-- invoke_response (id="abc", data=...) -->| response + | | + |<-- reminder_request (id="def") ----------| callback + |-- reminder_response (id="def") -------->| ack + | | + |<-- deactivate_request (id="ghi") --------| callback + |-- deactivate_response (id="ghi") ------->| ack +``` + +Every callback and response is correlated by a unique `id` generated by daprd. The app **must** echo the same `id` on the response. + +## Step 1: Open the stream and register actor types + +The first message sent on the stream must be an `initial_request` advertising the actor types the app hosts. All fields other than `entities` are optional and override Dapr's defaults. + +{{< tabpane text=true >}} + +{{% tab "Go (raw gRPC)" %}} + +```go +package main + +import ( + "context" + "log" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + runtimev1pb "github.com/dapr/dapr/pkg/proto/runtime/v1" + durationpb "google.golang.org/protobuf/types/known/durationpb" +) + +func main() { + // Connect to the Dapr sidecar gRPC port (default 50001). + conn, err := grpc.NewClient("localhost:50001", + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("connect: %v", err) + } + defer conn.Close() + + client := runtimev1pb.NewDaprClient(conn) + ctx := context.Background() + + // Open the bidirectional stream. + stream, err := client.SubscribeActorEventsAlpha1(ctx) + if err != nil { + log.Fatalf("open stream: %v", err) + } + + // First message: register the actor types this app hosts. + idleTimeout := durationpb.New(60 * time.Minute) + err = stream.Send(&runtimev1pb.SubscribeActorEventsRequestAlpha1{ + RequestType: &runtimev1pb.SubscribeActorEventsRequestAlpha1_InitialRequest{ + InitialRequest: &runtimev1pb.SubscribeActorEventsRequestInitialAlpha1{ + Entities: []string{"MyActor"}, + ActorIdleTimeout: idleTimeout, + DrainRebalancedActors: boolPtr(true), + }, + }, + }) + if err != nil { + log.Fatalf("send initial: %v", err) + } + + // Wait for the acknowledgement from daprd. + msg, err := stream.Recv() + if err != nil { + log.Fatalf("recv initial ack: %v", err) + } + if msg.GetInitialResponse() == nil { + log.Fatalf("unexpected first message type") + } + log.Println("registered with Dapr, waiting for callbacks") + + // Enter the callback loop. + handleCallbacks(stream) +} + +func boolPtr(b bool) *bool { return &b } +``` + +{{% /tab %}} + +{{< /tabpane >}} + +## Step 2: Handle callbacks + +After the initial handshake, the stream delivers callbacks from daprd. Each callback message uses a `oneof response_type`. The app must send a correlated response for every callback received. + +{{< tabpane text=true >}} + +{{% tab "Go (raw gRPC)" %}} + +```go +func handleCallbacks(stream runtimev1pb.Dapr_SubscribeActorEventsAlpha1Client) { + for { + msg, err := stream.Recv() + if err != nil { + log.Printf("stream closed: %v", err) + return + } + + switch v := msg.ResponseType.(type) { + + case *runtimev1pb.SubscribeActorEventsResponseAlpha1_InvokeRequest: + req := v.InvokeRequest + log.Printf("invoke %s/%s.%s (id=%s)", req.ActorType, req.ActorId, req.Method, req.Id) + + // Execute the actor method and build the response payload. + result, invokeErr := dispatchMethod(req.ActorType, req.ActorId, req.Method, req.Data) + + resp := &runtimev1pb.SubscribeActorEventsRequestAlpha1{} + if invokeErr != nil { + resp.RequestType = &runtimev1pb.SubscribeActorEventsRequestAlpha1_InvokeResponse{ + InvokeResponse: &runtimev1pb.SubscribeActorEventsRequestInvokeResponseAlpha1{ + Id: req.Id, + Data: []byte(invokeErr.Error()), + Error: true, + }, + } + } else { + resp.RequestType = &runtimev1pb.SubscribeActorEventsRequestAlpha1_InvokeResponse{ + InvokeResponse: &runtimev1pb.SubscribeActorEventsRequestInvokeResponseAlpha1{ + Id: req.Id, + Data: result, + }, + } + } + if err := stream.Send(resp); err != nil { + log.Printf("send invoke response: %v", err) + return + } + + case *runtimev1pb.SubscribeActorEventsResponseAlpha1_ReminderRequest: + req := v.ReminderRequest + log.Printf("reminder %s/%s name=%s (id=%s)", req.ActorType, req.ActorId, req.Name, req.Id) + + // Return cancel=true to cancel the reminder after this firing. + shouldCancel := handleReminder(req.ActorType, req.ActorId, req.Name, req.Data) + + if err := stream.Send(&runtimev1pb.SubscribeActorEventsRequestAlpha1{ + RequestType: &runtimev1pb.SubscribeActorEventsRequestAlpha1_ReminderResponse{ + ReminderResponse: &runtimev1pb.SubscribeActorEventsRequestReminderResponseAlpha1{ + Id: req.Id, + Cancel: shouldCancel, + }, + }, + }); err != nil { + return + } + + case *runtimev1pb.SubscribeActorEventsResponseAlpha1_TimerRequest: + req := v.TimerRequest + log.Printf("timer %s/%s name=%s (id=%s)", req.ActorType, req.ActorId, req.Name, req.Id) + + shouldCancel := handleTimer(req.ActorType, req.ActorId, req.Name, req.Data) + + if err := stream.Send(&runtimev1pb.SubscribeActorEventsRequestAlpha1{ + RequestType: &runtimev1pb.SubscribeActorEventsRequestAlpha1_TimerResponse{ + TimerResponse: &runtimev1pb.SubscribeActorEventsRequestReminderResponseAlpha1{ + Id: req.Id, + Cancel: shouldCancel, + }, + }, + }); err != nil { + return + } + + case *runtimev1pb.SubscribeActorEventsResponseAlpha1_DeactivateRequest: + req := v.DeactivateRequest + log.Printf("deactivate %s/%s (id=%s)", req.ActorType, req.ActorId, req.Id) + + releaseActorState(req.ActorType, req.ActorId) + + if err := stream.Send(&runtimev1pb.SubscribeActorEventsRequestAlpha1{ + RequestType: &runtimev1pb.SubscribeActorEventsRequestAlpha1_DeactivateResponse{ + DeactivateResponse: &runtimev1pb.SubscribeActorEventsRequestDeactivateResponseAlpha1{ + Id: req.Id, + }, + }, + }); err != nil { + return + } + } + } +} + +// Stubs — replace with your actor logic. +func dispatchMethod(actorType, actorID, method string, data []byte) ([]byte, error) { return nil, nil } +func handleReminder(actorType, actorID, name string, data interface{}) bool { return false } +func handleTimer(actorType, actorID, name string, data interface{}) bool { return false } +func releaseActorState(actorType, actorID string) {} +``` + +{{% /tab %}} + +{{< /tabpane >}} + +## Step 3: Signal an error + +If the app cannot process a callback (for example, the actor method is not registered), send a `request_failed` message instead of the typed response. Use gRPC status codes from [google.golang.org/grpc/codes](https://pkg.go.dev/google.golang.org/grpc/codes). + +```go +import "google.golang.org/grpc/codes" + +stream.Send(&runtimev1pb.SubscribeActorEventsRequestAlpha1{ + RequestType: &runtimev1pb.SubscribeActorEventsRequestAlpha1_RequestFailed{ + RequestFailed: &runtimev1pb.SubscribeActorEventsRequestFailedAlpha1{ + Id: req.Id, + Code: uint32(codes.NotFound), // marks the failure as permanent/non-retryable + Message: "actor method not found: " + req.Method, + }, + }, +}) +``` + +{{% alert title="Important" color="warning" %}} +daprd treats `codes.NotFound` as a permanent, non-retryable failure. Use other status codes (e.g. `codes.Internal`) for transient errors that Dapr should retry. +{{% /alert %}} + +## Step 4: Handle reconnection + +The stream is a long-lived connection. If daprd restarts or the network is interrupted, `stream.Recv()` returns an error. Reconnect with exponential back-off: + +```go +import ( + "time" + "math/rand" +) + +func runWithReconnect(ctx context.Context, client runtimev1pb.DaprClient) { + backoff := 1 * time.Second + for { + if ctx.Err() != nil { + return + } + stream, err := client.SubscribeActorEventsAlpha1(ctx) + if err != nil { + log.Printf("open stream error: %v; retrying in %s", err, backoff) + time.Sleep(backoff + time.Duration(rand.Intn(500))*time.Millisecond) + backoff = min(backoff*2, 30*time.Second) + continue + } + backoff = 1 * time.Second // reset on success + + if err := sendInitialRequest(stream); err != nil { + continue + } + if _, err := stream.Recv(); err != nil { // wait for initial ack + continue + } + handleCallbacks(stream) // blocks until stream closes + } +} + +func min(a, b time.Duration) time.Duration { + if a < b { return a } + return b +} +``` + +## Runtime configuration fields + +The initial registration message can include the following optional fields to override Dapr's defaults for all actor types on this stream: + +| Field | Type | Description | +|-------|------|-------------| +| `entities` | `[]string` | **Required.** Actor types this app hosts. | +| `actor_idle_timeout` | `Duration` | Deactivate an actor after this idle period. Default: 60 minutes. | +| `drain_ongoing_call_timeout` | `Duration` | How long to wait for in-flight calls during rebalancing. Default: 60 seconds. | +| `drain_rebalanced_actors` | `bool` | If true, wait for drain before deactivating rebalanced actors. Default: true. | +| `reentrancy` | `ActorReentrancyConfig` | Enable actor reentrancy and set max stack depth. Default: disabled. | +| `entities_config` | `[]ActorEntityConfig` | Per-actor-type overrides for any of the fields above. | + +See [actor runtime configuration]({{% ref "actors-runtime-config" %}}) for a description of each parameter. + +## Coexistence with traditional callbacks + +The app-initiated stream and traditional HTTP/gRPC actor callbacks are not mutually exclusive. During a migration you can run both: + +- Pods that have opened `SubscribeActorEventsAlpha1` receive callbacks via the stream. +- Pods that have not opened the stream continue to receive callbacks via the traditional inbound endpoints. + +Migrate all pods before removing the inbound ports and NetworkPolicy rules. + +## Related links + +- [Actor app-initiated streams concept]({{% ref "actors-app-initiated-streams" %}}) +- [Actor API reference — SubscribeActorEventsAlpha1]({{% ref "actors_api#subscribeactoreventsalpha1-grpc" %}}) +- [Actor runtime configuration]({{% ref "actors-runtime-config" %}}) +- [Actors overview]({{% ref "actors-overview" %}}) diff --git a/daprdocs/content/en/operations/support/alpha-beta-apis.md b/daprdocs/content/en/operations/support/alpha-beta-apis.md index 730c0bdb6bd..1b772b37579 100644 --- a/daprdocs/content/en/operations/support/alpha-beta-apis.md +++ b/daprdocs/content/en/operations/support/alpha-beta-apis.md @@ -15,6 +15,7 @@ description: "List of current alpha and beta APIs" | Cryptography | [Crypto proto](https://github.com/dapr/dapr/blob/5aba3c9aa4ea9b3f388df125f9c66495b43c5c9e/dapr/proto/runtime/v1/dapr.proto#L118) | `v1.0-alpha1/crypto` | The cryptography API enables you to perform **high level** cryptography operations for encrypting and decrypting messages. | [Cryptography API]({{% ref "cryptography-overview.md" %}}) | v1.11 | | Streaming Subscription | [Streaming Subscription proto](https://github.com/dapr/dapr/blob/310c83140b2f0c3cb7d2bef19624df88af3e8e0a/dapr/proto/runtime/v1/dapr.proto#L454) | N/A | Subscription is defined in the application code. Streaming subscriptions are dynamic, meaning they allow for adding or removing subscriptions at runtime. | [Streaming Subscription API]({{% ref "subscription-methods/#streaming-subscriptions" %}}) | v1.14 | | Conversation | [Conversation proto](https://github.com/dapr/dapr/blob/master/dapr/proto/runtime/v1/dapr.proto#L226) | `v1.0-alpha2/conversation` | Converse between different large language models using the conversation API. | [Conversation API]({{% ref "conversation-overview.md" %}}) | v1.15 | +| Actor App-Initiated Streams | [SubscribeActorEventsAlpha1 proto](https://github.com/dapr/dapr/blob/master/dapr/proto/runtime/v1/dapr.proto) | N/A | Actor hosts open a single bidirectional gRPC stream to daprd and receive all actor callbacks (invoke, reminder, timer, deactivate) over that connection. Apps do not need to expose a server port. | [Actor app-initiated streams]({{% ref "actors-app-initiated-streams" %}}) | v1.18 | ## Beta APIs diff --git a/daprdocs/content/en/reference/api/actors_api.md b/daprdocs/content/en/reference/api/actors_api.md index 288c4dcafb4..d74eae0a915 100644 --- a/daprdocs/content/en/reference/api/actors_api.md +++ b/daprdocs/content/en/reference/api/actors_api.md @@ -741,6 +741,136 @@ Example of getting a health check response from the app: curl -X GET http://localhost:3000/healthz \ ``` +## SubscribeActorEventsAlpha1 (gRPC) + +{{% alert title="Alpha" color="warning" %}} +`SubscribeActorEventsAlpha1` is in **alpha**. The API shape may change in future releases. +{{% /alert %}} + +`SubscribeActorEventsAlpha1` is a bidirectional gRPC streaming RPC on the **Dapr service** (`dapr.proto.runtime.v1.Dapr`). The application is the gRPC _client_: it dials daprd, opens the stream, and receives all actor callbacks — method invocations, reminders, timers, and deactivations — over that single connection. Apps using this RPC do not need to expose an HTTP or gRPC server port. + +See the [actor app-initiated streams concept doc]({{% ref "actors-app-initiated-streams" %}}) and [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}) for usage guidance and code examples. + +### gRPC service definition + +```protobuf +// On the Dapr service (app dials daprd): +rpc SubscribeActorEventsAlpha1(stream SubscribeActorEventsRequestAlpha1) + returns (stream SubscribeActorEventsResponseAlpha1) {} +``` + +### App → Dapr: SubscribeActorEventsRequestAlpha1 + +Messages sent **from the app to daprd**. The first message must be `initial_request`; all subsequent messages must be responses correlated by `id`. + +| Field (oneof `request_type`) | When to send | +|---|---| +| `initial_request` (`SubscribeActorEventsRequestInitialAlpha1`) | **First message only.** Registers actor types and runtime config. | +| `invoke_response` (`SubscribeActorEventsRequestInvokeResponseAlpha1`) | Response to an `invoke_request` callback. | +| `reminder_response` (`SubscribeActorEventsRequestReminderResponseAlpha1`) | Response to a `reminder_request` callback. | +| `timer_response` (`SubscribeActorEventsRequestReminderResponseAlpha1`) | Response to a `timer_request` callback. | +| `deactivate_response` (`SubscribeActorEventsRequestDeactivateResponseAlpha1`) | Response to a `deactivate_request` callback. | +| `request_failed` (`SubscribeActorEventsRequestFailedAlpha1`) | Sent instead of any typed response when the app cannot handle the callback. | + +#### SubscribeActorEventsRequestInitialAlpha1 + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `entities` | `[]string` | Yes | Actor types hosted by this app. | +| `actor_idle_timeout` | `Duration` | No | Idle timeout before deactivation. Unset = Dapr default (60 min). | +| `drain_ongoing_call_timeout` | `Duration` | No | How long to wait for in-flight calls during rebalancing. Unset = Dapr default (60 s). | +| `drain_rebalanced_actors` | `bool` | No | Drain in-flight calls before deactivating rebalanced actors. Unset = `true`. | +| `reentrancy` | `ActorReentrancyConfig` | No | Reentrancy configuration for all actor types on this stream. | +| `entities_config` | `[]ActorEntityConfig` | No | Per-actor-type overrides. Each entry must reference a type listed in `entities`. | + +#### SubscribeActorEventsRequestInvokeResponseAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Correlation ID from the originating `invoke_request`. | +| `data` | `bytes` | Response payload. | +| `metadata` | `map` | Response-level headers, including `content-type`. | +| `error` | `bool` | When `true`, `data` is an application-defined error payload passed verbatim to the caller. | + +#### SubscribeActorEventsRequestReminderResponseAlpha1 + +Used for both `reminder_response` and `timer_response`. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Correlation ID from the originating reminder or timer request. | +| `cancel` | `bool` | When `true`, instructs Dapr to cancel the reminder or timer after this firing. | + +#### SubscribeActorEventsRequestDeactivateResponseAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Correlation ID from the originating `deactivate_request`. | + +#### SubscribeActorEventsRequestFailedAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Correlation ID from the originating request. | +| `code` | `uint32` | gRPC status code. `codes.NotFound` (5) signals a permanent, non-retryable failure. | +| `message` | `string` | Human-readable error description. | + +### Dapr → App: SubscribeActorEventsResponseAlpha1 + +Messages sent **from daprd to the app**. The first message is `initial_response`; all subsequent messages are callback requests. + +| Field (oneof `response_type`) | Description | +|---|---| +| `initial_response` (`SubscribeActorEventsResponseInitialAlpha1`) | Empty ack confirming successful registration. Errors surface as a gRPC stream error. | +| `invoke_request` (`SubscribeActorEventsResponseInvokeRequestAlpha1`) | Actor method invocation. | +| `reminder_request` (`SubscribeActorEventsResponseReminderRequestAlpha1`) | Actor reminder fired. | +| `timer_request` (`SubscribeActorEventsResponseTimerRequestAlpha1`) | Actor timer fired. | +| `deactivate_request` (`SubscribeActorEventsResponseDeactivateRequestAlpha1`) | Actor instance being deactivated. | + +#### SubscribeActorEventsResponseInvokeRequestAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Unique correlation ID. Echo on `invoke_response`. | +| `actor_type` | `string` | Actor type. | +| `actor_id` | `string` | Actor ID. | +| `method` | `string` | Method name to invoke. | +| `data` | `bytes` | Request payload. | +| `metadata` | `map` | Request-level headers including `content-type` and `Dapr-Reentrancy-Id` (when reentrancy is enabled). | + +#### SubscribeActorEventsResponseReminderRequestAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Unique correlation ID. Echo on `reminder_response`. | +| `actor_type` | `string` | Actor type. | +| `actor_id` | `string` | Actor ID. | +| `name` | `string` | Reminder name. | +| `due_time` | `string` | Reminder due time (time.ParseDuration format). | +| `period` | `string` | Reminder period (time.ParseDuration format). | +| `data` | `google.protobuf.Any` | Reminder data payload. | + +#### SubscribeActorEventsResponseTimerRequestAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Unique correlation ID. Echo on `timer_response`. | +| `actor_type` | `string` | Actor type. | +| `actor_id` | `string` | Actor ID. | +| `name` | `string` | Timer name. | +| `due_time` | `string` | Timer due time. | +| `period` | `string` | Timer period. | +| `callback` | `string` | Callback method name registered with the timer. | +| `data` | `google.protobuf.Any` | Timer data payload. | + +#### SubscribeActorEventsResponseDeactivateRequestAlpha1 + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Unique correlation ID. Echo on `deactivate_response`. | +| `actor_type` | `string` | Actor type. | +| `actor_id` | `string` | Actor ID. | + ## Activating an Actor Conceptually, activating an actor means creating the actor's object and adding the actor to a tracking table. [Review an example from the .NET SDK](https://github.com/dapr/dotnet-sdk/blob/6c271262231c41b21f3ca866eb0d55f7ce8b7dbc/src/Dapr.Actors/Runtime/ActorManager.cs#L199). From f365a3cf44021421288dd1342a8715d325b54093 Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Thu, 21 May 2026 16:52:30 +0100 Subject: [PATCH 2/4] actors: tighten app-initiated streams docs (review pass) Signed-off-by: Nelson Parente --- .../actors/actors-app-initiated-streams.md | 32 ++++----------- .../howto-actors-app-initiated-streams.md | 39 +++++++------------ .../en/operations/support/alpha-beta-apis.md | 2 +- .../content/en/reference/api/actors_api.md | 4 +- 4 files changed, 25 insertions(+), 52 deletions(-) diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md index 4f391262f8b..bb8f1a1bc5b 100644 --- a/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md @@ -7,20 +7,14 @@ description: "Receive actor callbacks over an app-initiated gRPC stream without --- {{% alert title="Alpha" color="warning" %}} -The `SubscribeActorEventsAlpha1` API is in **alpha**. The API shape may change in a future release. Do not use it in production workloads without accepting that risk. +`SubscribeActorEventsAlpha1` is in alpha. The API shape may change in a future release. {{% /alert %}} -## Overview +By default, Dapr delivers actor callbacks — method invocations, reminders, timers, and deactivations — by calling **inbound** HTTP or gRPC endpoints on the application. Each actor-hosting pod must expose a server port that the Dapr sidecar can reach. -By default, Dapr delivers actor callbacks — method invocations, reminders, timers, and deactivations — by calling **inbound** HTTP or gRPC endpoints on the application. This requires each actor-hosting pod to expose a server port that the Dapr sidecar can reach. +Starting with Dapr v1.18, actor hosts can instead open a **single bidirectional gRPC stream from the app to the sidecar** (`SubscribeActorEventsAlpha1`) and receive all four callback types over that connection. The app is the gRPC _client_: it dials daprd, opens the stream, and waits for callbacks. No inbound port is required. -Starting with Dapr v1.18, actor hosts can instead open a **single bidirectional gRPC stream from the app to the sidecar** (`SubscribeActorEventsAlpha1`) and receive all four callback types over that one connection. The app is the gRPC _client_; it dials daprd, opens the stream, and waits for callbacks to arrive. No inbound port is required. - -This pattern closes the long-standing feature request [dapr/dapr#927](https://github.com/dapr/dapr/issues/927) and aligns actor callback delivery with how Dapr already handles: - -- Pub/sub streaming subscriptions (`SubscribeTopicEventsAlpha1`) -- Configuration watch streams -- Scheduler job streams +This aligns actor callback delivery with how Dapr handles pub/sub streaming subscriptions (`SubscribeTopicEventsAlpha1`), configuration watch streams, and scheduler job streams. ## Why app-initiated streams? @@ -33,11 +27,7 @@ This pattern closes the long-standing feature request [dapr/dapr#927](https://gi | **SDK support** | All SDKs | SDKs adding support (see below) | | **Stability** | Stable | Alpha (v1.18+) | -The app-initiated approach is especially useful in environments where: - -- NetworkPolicies restrict inbound traffic to application pods. -- Actors run in serverless or restricted-networking environments. -- You want a single connection-management surface instead of per-callback routes. +The app-initiated approach is useful when NetworkPolicies restrict inbound traffic to application pods, when actors run in restricted-networking environments, or when you want a single connection-management surface instead of per-callback HTTP/gRPC routes. ## How it works @@ -88,7 +78,7 @@ The daprd gRPC port is configurable via the `dapr.io/grpc-port` annotation (defa ### Example NetworkPolicy (Kubernetes) -The following policy allows app pods with the label `app: my-actor-service` to reach the sidecar gRPC port while denying all other inbound traffic to the app pod: +The following policy restricts ingress to actor-hosting pods and allows egress to the sidecar gRPC port. Adjust the port if you set `dapr.io/grpc-port` to a non-default value. ```yaml apiVersion: networking.k8s.io/v1 @@ -111,14 +101,7 @@ spec: ## SDK support -SDK-level support for `SubscribeActorEventsAlpha1` is being added in the v1.18 SDK releases. Refer to the documentation for each SDK: - -- [.NET SDK actors]({{% ref "dotnet-actors" %}}) -- [Java SDK actors]({{% ref "java#actors" %}}) -- [Python SDK actors]({{% ref "python-actor" %}}) -- [Go SDK actors]({{% ref "go-sdk-service" %}}) - -Until SDK support is complete, the API is accessible directly via the generated gRPC client. See the [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}) for a raw gRPC example. +SDK helpers for `SubscribeActorEventsAlpha1` are not yet available; use the generated gRPC client directly. See the [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}) for a raw gRPC example in Go. SDK support is being tracked in the v1.18 SDK releases. ## Related links @@ -127,3 +110,4 @@ Until SDK support is complete, the API is accessible directly via the generated - [Actors overview]({{% ref "actors-overview" %}}) - [Actor runtime configuration]({{% ref "actors-runtime-config" %}}) - [Runtime PR dapr/dapr#9812](https://github.com/dapr/dapr/pull/9812) + diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md index 845453f259b..cb2b367bb7f 100644 --- a/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md @@ -7,12 +7,10 @@ description: "Open a gRPC stream from your app to daprd to receive actor callbac --- {{% alert title="Alpha" color="warning" %}} -The `SubscribeActorEventsAlpha1` API is in **alpha**. The API shape may change in a future release. +`SubscribeActorEventsAlpha1` is in alpha. The API shape may change in a future release. {{% /alert %}} -This guide shows how to use the `SubscribeActorEventsAlpha1` gRPC stream so your actor host application receives all four callback types — method invocations, reminders, timers, and deactivations — over a single app-initiated connection to the Dapr sidecar. - -Read the [concept doc]({{% ref "actors-app-initiated-streams" %}}) first to understand the protocol and when to use this approach. +This guide shows how to implement `SubscribeActorEventsAlpha1` in Go using the generated gRPC client. Read the [concept doc]({{% ref "actors-app-initiated-streams" %}}) first for protocol details and when to prefer this approach over traditional callbacks. ## Prerequisites @@ -62,6 +60,7 @@ import ( "google.golang.org/grpc/credentials/insecure" runtimev1pb "github.com/dapr/dapr/pkg/proto/runtime/v1" + anypb "google.golang.org/protobuf/types/known/anypb" durationpb "google.golang.org/protobuf/types/known/durationpb" ) @@ -223,9 +222,9 @@ func handleCallbacks(stream runtimev1pb.Dapr_SubscribeActorEventsAlpha1Client) { // Stubs — replace with your actor logic. func dispatchMethod(actorType, actorID, method string, data []byte) ([]byte, error) { return nil, nil } -func handleReminder(actorType, actorID, name string, data interface{}) bool { return false } -func handleTimer(actorType, actorID, name string, data interface{}) bool { return false } -func releaseActorState(actorType, actorID string) {} +func handleReminder(actorType, actorID, name string, data *anypb.Any) bool { return false } +func handleTimer(actorType, actorID, name string, data *anypb.Any) bool { return false } +func releaseActorState(actorType, actorID string) {} ``` {{% /tab %}} @@ -260,8 +259,8 @@ The stream is a long-lived connection. If daprd restarts or the network is inter ```go import ( - "time" "math/rand" + "time" ) func runWithReconnect(ctx context.Context, client runtimev1pb.DaprClient) { @@ -277,22 +276,17 @@ func runWithReconnect(ctx context.Context, client runtimev1pb.DaprClient) { backoff = min(backoff*2, 30*time.Second) continue } - backoff = 1 * time.Second // reset on success + backoff = 1 * time.Second // reset on success if err := sendInitialRequest(stream); err != nil { continue } - if _, err := stream.Recv(); err != nil { // wait for initial ack + if _, err := stream.Recv(); err != nil { // wait for initial ack continue } - handleCallbacks(stream) // blocks until stream closes + handleCallbacks(stream) // blocks until stream closes } } - -func min(a, b time.Duration) time.Duration { - if a < b { return a } - return b -} ``` ## Runtime configuration fields @@ -302,9 +296,9 @@ The initial registration message can include the following optional fields to ov | Field | Type | Description | |-------|------|-------------| | `entities` | `[]string` | **Required.** Actor types this app hosts. | -| `actor_idle_timeout` | `Duration` | Deactivate an actor after this idle period. Default: 60 minutes. | -| `drain_ongoing_call_timeout` | `Duration` | How long to wait for in-flight calls during rebalancing. Default: 60 seconds. | -| `drain_rebalanced_actors` | `bool` | If true, wait for drain before deactivating rebalanced actors. Default: true. | +| `actor_idle_timeout` | `Duration` | Deactivate an actor after this idle period. Unset = Dapr default (60 minutes). | +| `drain_ongoing_call_timeout` | `Duration` | How long to wait for in-flight calls during rebalancing. Unset = Dapr default. | +| `drain_rebalanced_actors` | `bool` | If true, wait for drain before deactivating rebalanced actors. Unset = Dapr default. | | `reentrancy` | `ActorReentrancyConfig` | Enable actor reentrancy and set max stack depth. Default: disabled. | | `entities_config` | `[]ActorEntityConfig` | Per-actor-type overrides for any of the fields above. | @@ -312,12 +306,7 @@ See [actor runtime configuration]({{% ref "actors-runtime-config" %}}) for a des ## Coexistence with traditional callbacks -The app-initiated stream and traditional HTTP/gRPC actor callbacks are not mutually exclusive. During a migration you can run both: - -- Pods that have opened `SubscribeActorEventsAlpha1` receive callbacks via the stream. -- Pods that have not opened the stream continue to receive callbacks via the traditional inbound endpoints. - -Migrate all pods before removing the inbound ports and NetworkPolicy rules. +The app-initiated stream and traditional HTTP/gRPC callbacks are not mutually exclusive. Pods that have opened `SubscribeActorEventsAlpha1` receive callbacks via the stream; pods that have not opened the stream continue to use traditional inbound endpoints. Migrate all pods before removing inbound ports and NetworkPolicy rules. ## Related links diff --git a/daprdocs/content/en/operations/support/alpha-beta-apis.md b/daprdocs/content/en/operations/support/alpha-beta-apis.md index 1b772b37579..3fe98762ee6 100644 --- a/daprdocs/content/en/operations/support/alpha-beta-apis.md +++ b/daprdocs/content/en/operations/support/alpha-beta-apis.md @@ -15,7 +15,7 @@ description: "List of current alpha and beta APIs" | Cryptography | [Crypto proto](https://github.com/dapr/dapr/blob/5aba3c9aa4ea9b3f388df125f9c66495b43c5c9e/dapr/proto/runtime/v1/dapr.proto#L118) | `v1.0-alpha1/crypto` | The cryptography API enables you to perform **high level** cryptography operations for encrypting and decrypting messages. | [Cryptography API]({{% ref "cryptography-overview.md" %}}) | v1.11 | | Streaming Subscription | [Streaming Subscription proto](https://github.com/dapr/dapr/blob/310c83140b2f0c3cb7d2bef19624df88af3e8e0a/dapr/proto/runtime/v1/dapr.proto#L454) | N/A | Subscription is defined in the application code. Streaming subscriptions are dynamic, meaning they allow for adding or removing subscriptions at runtime. | [Streaming Subscription API]({{% ref "subscription-methods/#streaming-subscriptions" %}}) | v1.14 | | Conversation | [Conversation proto](https://github.com/dapr/dapr/blob/master/dapr/proto/runtime/v1/dapr.proto#L226) | `v1.0-alpha2/conversation` | Converse between different large language models using the conversation API. | [Conversation API]({{% ref "conversation-overview.md" %}}) | v1.15 | -| Actor App-Initiated Streams | [SubscribeActorEventsAlpha1 proto](https://github.com/dapr/dapr/blob/master/dapr/proto/runtime/v1/dapr.proto) | N/A | Actor hosts open a single bidirectional gRPC stream to daprd and receive all actor callbacks (invoke, reminder, timer, deactivate) over that connection. Apps do not need to expose a server port. | [Actor app-initiated streams]({{% ref "actors-app-initiated-streams" %}}) | v1.18 | +| Actor App-Initiated Streams | [SubscribeActorEventsAlpha1 proto](https://github.com/dapr/dapr/blob/86440a8a624c8ceb6d8cf882682bcd34f6025772/dapr/proto/runtime/v1/dapr.proto#L125) | N/A | Actor hosts open a single bidirectional gRPC stream to daprd and receive all actor callbacks (invoke, reminder, timer, deactivate) over that connection. Apps do not need to expose a server port. | [Actor app-initiated streams]({{% ref "actors-app-initiated-streams" %}}) | v1.18 | ## Beta APIs diff --git a/daprdocs/content/en/reference/api/actors_api.md b/daprdocs/content/en/reference/api/actors_api.md index d74eae0a915..ac3ef46d1db 100644 --- a/daprdocs/content/en/reference/api/actors_api.md +++ b/daprdocs/content/en/reference/api/actors_api.md @@ -778,8 +778,8 @@ Messages sent **from the app to daprd**. The first message must be `initial_requ |-------|------|----------|-------------| | `entities` | `[]string` | Yes | Actor types hosted by this app. | | `actor_idle_timeout` | `Duration` | No | Idle timeout before deactivation. Unset = Dapr default (60 min). | -| `drain_ongoing_call_timeout` | `Duration` | No | How long to wait for in-flight calls during rebalancing. Unset = Dapr default (60 s). | -| `drain_rebalanced_actors` | `bool` | No | Drain in-flight calls before deactivating rebalanced actors. Unset = `true`. | +| `drain_ongoing_call_timeout` | `Duration` | No | How long to wait for in-flight calls during rebalancing. Unset = Dapr default. | +| `drain_rebalanced_actors` | `bool` | No | Drain in-flight calls before deactivating rebalanced actors. Unset = Dapr default. | | `reentrancy` | `ActorReentrancyConfig` | No | Reentrancy configuration for all actor types on this stream. | | `entities_config` | `[]ActorEntityConfig` | No | Per-actor-type overrides. Each entry must reference a type listed in `entities`. | From dddfed1a0dfd0445a5e51dd72a3426acd40467de Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Thu, 11 Jun 2026 10:56:12 +0100 Subject: [PATCH 3/4] docs: address review feedback on actor app-initiated streams - Convert the how-to protocol summary to a mermaid sequence diagram - Surface SDK support near the concept intro and in the comparison table - Move the why-summary sentence above the comparison table - Correct the SDK support section: the Go SDK helper (client.SubscribeActorEvents) has landed, so the no-SDK-support note was inaccurate; point to it and note other SDKs use the generated gRPC client. Drop the release-tracking sentence. Signed-off-by: Nelson Parente --- .../actors/actors-app-initiated-streams.md | 10 +++--- .../howto-actors-app-initiated-streams.md | 33 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md index bb8f1a1bc5b..d77265aa046 100644 --- a/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/actors-app-initiated-streams.md @@ -16,19 +16,21 @@ Starting with Dapr v1.18, actor hosts can instead open a **single bidirectional This aligns actor callback delivery with how Dapr handles pub/sub streaming subscriptions (`SubscribeTopicEventsAlpha1`), configuration watch streams, and scheduler job streams. +The [Go SDK](https://github.com/dapr/go-sdk) provides a high-level helper for this API ([`client.SubscribeActorEvents`](https://github.com/dapr/go-sdk/tree/main/examples/actor-grpc)). Other Dapr SDKs currently expose `SubscribeActorEventsAlpha1` only through their generated gRPC client. See [SDK support](#sdk-support) for details. + ## Why app-initiated streams? +The app-initiated approach is useful when NetworkPolicies restrict inbound traffic to application pods, when actors run in restricted-networking environments, or when you want a single connection-management surface instead of per-callback HTTP/gRPC routes. + | | Traditional callbacks | App-initiated stream | |---|---|---| | **Connection direction** | sidecar → app | app → sidecar | | **App server port required** | Yes | No | | **NetworkPolicy / firewall** | Must allow sidecar→app inbound | Only app→sidecar outbound needed | | **Callback types** | Separate endpoints per type | All four types on one stream | -| **SDK support** | All SDKs | SDKs adding support (see below) | +| **SDK support** | All SDKs | Go SDK helper; other SDKs via generated gRPC client | | **Stability** | Stable | Alpha (v1.18+) | -The app-initiated approach is useful when NetworkPolicies restrict inbound traffic to application pods, when actors run in restricted-networking environments, or when you want a single connection-management surface instead of per-callback HTTP/gRPC routes. - ## How it works The protocol follows a request–response pairing over a bidirectional gRPC stream: @@ -101,7 +103,7 @@ spec: ## SDK support -SDK helpers for `SubscribeActorEventsAlpha1` are not yet available; use the generated gRPC client directly. See the [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}) for a raw gRPC example in Go. SDK support is being tracked in the v1.18 SDK releases. +The [Go SDK](https://github.com/dapr/go-sdk) provides a high-level helper, [`client.SubscribeActorEvents`](https://github.com/dapr/go-sdk/tree/main/examples/actor-grpc), that manages the stream lifecycle, callback dispatch, and reconnection for you. Other Dapr SDKs currently expose `SubscribeActorEventsAlpha1` only through their generated gRPC client; call it directly as shown in the [how-to guide]({{% ref "howto-actors-app-initiated-streams" %}}). ## Related links diff --git a/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md index cb2b367bb7f..e3703c1efdc 100644 --- a/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md +++ b/daprdocs/content/en/developing-applications/building-blocks/actors/howto-actors-app-initiated-streams.md @@ -12,6 +12,10 @@ description: "Open a gRPC stream from your app to daprd to receive actor callbac This guide shows how to implement `SubscribeActorEventsAlpha1` in Go using the generated gRPC client. Read the [concept doc]({{% ref "actors-app-initiated-streams" %}}) first for protocol details and when to prefer this approach over traditional callbacks. +{{% alert title="Using the Go SDK?" color="primary" %}} +The [Dapr Go SDK](https://github.com/dapr/go-sdk) wraps this protocol in a high-level helper — [`client.SubscribeActorEvents`](https://github.com/dapr/go-sdk/tree/main/examples/actor-grpc) — that manages the stream, callback dispatch, and reconnection for you. This guide documents the raw gRPC protocol directly, which is useful for understanding the wire format or for SDKs that do not yet provide a helper. +{{% /alert %}} + ## Prerequisites - [Dapr v1.18 or later]({{% ref "getting-started" %}}) @@ -20,22 +24,19 @@ This guide shows how to implement `SubscribeActorEventsAlpha1` in Go using the g ## Protocol summary -``` -App daprd (Dapr sidecar) - | | - |-- SubscribeActorEventsAlpha1() ---------->| open stream - | | - |-- SubscribeActorEventsRequestInitialAlpha1 -->| register actor types + config - |<-- SubscribeActorEventsResponseInitialAlpha1 --| ack (empty = success) - | | - |<-- invoke_request (id="abc", method="x") --| callback - |-- invoke_response (id="abc", data=...) -->| response - | | - |<-- reminder_request (id="def") ----------| callback - |-- reminder_response (id="def") -------->| ack - | | - |<-- deactivate_request (id="ghi") --------| callback - |-- deactivate_response (id="ghi") ------->| ack +```mermaid +sequenceDiagram + participant App + participant daprd as daprd (Dapr sidecar) + App->>daprd: SubscribeActorEventsAlpha1() — open stream + App->>daprd: SubscribeActorEventsRequestInitialAlpha1
(register actor types + config) + daprd-->>App: SubscribeActorEventsResponseInitialAlpha1
(ack — empty = success) + daprd-->>App: invoke_request (id="abc", method="x") + App->>daprd: invoke_response (id="abc", data=...) + daprd-->>App: reminder_request (id="def") + App->>daprd: reminder_response (id="def") + daprd-->>App: deactivate_request (id="ghi") + App->>daprd: deactivate_response (id="ghi") ``` Every callback and response is correlated by a unique `id` generated by daprd. The app **must** echo the same `id` on the response. From 756cab1c855388e8898ff2d9fdff47dd666ed8a0 Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Tue, 23 Jun 2026 11:45:35 +0100 Subject: [PATCH 4/4] docs: pin actor streams proto link to the v1.18 backport commit The alpha-beta-apis row linked to an unrelated version-skew CI commit. Repoint to the v1.18 backport merge commit (dapr/dapr#9964), where the SubscribeActorEventsAlpha1 RPC is defined in dapr.proto at the same line (L125), so the permalink is stable and points at v1.18 content. Signed-off-by: Nelson Parente --- daprdocs/content/en/operations/support/alpha-beta-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/operations/support/alpha-beta-apis.md b/daprdocs/content/en/operations/support/alpha-beta-apis.md index 3fe98762ee6..54e2e0843fc 100644 --- a/daprdocs/content/en/operations/support/alpha-beta-apis.md +++ b/daprdocs/content/en/operations/support/alpha-beta-apis.md @@ -15,7 +15,7 @@ description: "List of current alpha and beta APIs" | Cryptography | [Crypto proto](https://github.com/dapr/dapr/blob/5aba3c9aa4ea9b3f388df125f9c66495b43c5c9e/dapr/proto/runtime/v1/dapr.proto#L118) | `v1.0-alpha1/crypto` | The cryptography API enables you to perform **high level** cryptography operations for encrypting and decrypting messages. | [Cryptography API]({{% ref "cryptography-overview.md" %}}) | v1.11 | | Streaming Subscription | [Streaming Subscription proto](https://github.com/dapr/dapr/blob/310c83140b2f0c3cb7d2bef19624df88af3e8e0a/dapr/proto/runtime/v1/dapr.proto#L454) | N/A | Subscription is defined in the application code. Streaming subscriptions are dynamic, meaning they allow for adding or removing subscriptions at runtime. | [Streaming Subscription API]({{% ref "subscription-methods/#streaming-subscriptions" %}}) | v1.14 | | Conversation | [Conversation proto](https://github.com/dapr/dapr/blob/master/dapr/proto/runtime/v1/dapr.proto#L226) | `v1.0-alpha2/conversation` | Converse between different large language models using the conversation API. | [Conversation API]({{% ref "conversation-overview.md" %}}) | v1.15 | -| Actor App-Initiated Streams | [SubscribeActorEventsAlpha1 proto](https://github.com/dapr/dapr/blob/86440a8a624c8ceb6d8cf882682bcd34f6025772/dapr/proto/runtime/v1/dapr.proto#L125) | N/A | Actor hosts open a single bidirectional gRPC stream to daprd and receive all actor callbacks (invoke, reminder, timer, deactivate) over that connection. Apps do not need to expose a server port. | [Actor app-initiated streams]({{% ref "actors-app-initiated-streams" %}}) | v1.18 | +| Actor App-Initiated Streams | [SubscribeActorEventsAlpha1 proto](https://github.com/dapr/dapr/blob/2fe035dfd8d248a3272dd29574ce2e315b8cf570/dapr/proto/runtime/v1/dapr.proto#L125) | N/A | Actor hosts open a single bidirectional gRPC stream to daprd and receive all actor callbacks (invoke, reminder, timer, deactivate) over that connection. Apps do not need to expose a server port. | [Actor app-initiated streams]({{% ref "actors-app-initiated-streams" %}}) | v1.18 | ## Beta APIs