Skip to content

Commit 54aa936

Browse files
authored
Merge branch 'main' into dependabot/go_modules/sample-apps/go/cloud/google.golang.org/grpc-1.79.3
2 parents f09fbc1 + cc8c769 commit 54aa936

32 files changed

Lines changed: 1116 additions & 350 deletions

.github/workflows/snipsync.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Snipsync
2+
3+
on:
4+
schedule:
5+
- cron: '0 6 * * *' # Daily at 6:00 UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
snipsync:
10+
name: Sync code snippets
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
steps:
16+
- name: Generate token
17+
id: generate_token
18+
uses: actions/create-github-app-token@v1
19+
with:
20+
app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }}
21+
private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }}
22+
23+
- name: Checkout
24+
uses: actions/checkout@v6
25+
with:
26+
token: ${{ steps.generate_token.outputs.token }}
27+
ref: main
28+
29+
- name: Setup Node
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: 20
33+
cache: yarn
34+
35+
- name: Install dependencies
36+
run: yarn install --frozen-lockfile
37+
38+
- name: Run snipsync
39+
run: yarn snipsync
40+
41+
- name: Check for changes
42+
id: changes
43+
run: |
44+
if git diff --quiet; then
45+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
46+
else
47+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
48+
fi
49+
50+
- name: Commit and push changes
51+
if: steps.changes.outputs.has_changes == 'true'
52+
run: |
53+
git config user.name "github-actions[bot]"
54+
git config user.email "github-actions[bot]@users.noreply.github.com"
55+
56+
branch_name="snipsync/daily-update"
57+
git checkout -B "$branch_name"
58+
git add docs/
59+
git commit -m "chore: sync code snippets via snipsync"
60+
git push --force-with-lease origin "$branch_name"
61+
62+
- name: Create or update PR
63+
if: steps.changes.outputs.has_changes == 'true'
64+
env:
65+
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
66+
run: |
67+
branch_name="snipsync/daily-update"
68+
existing_pr=$(gh pr list --head "$branch_name" --state open --json number --jq '.[0].number')
69+
70+
if [ -n "$existing_pr" ]; then
71+
echo "PR #$existing_pr already exists — updated with latest push."
72+
else
73+
gh pr create \
74+
--title "chore: sync code snippets" \
75+
--body "$(cat <<'EOF'
76+
Automated daily sync of code snippets from source repositories via snipsync.
77+
78+
This PR was generated by the [Snipsync workflow](https://github.com/${{ github.repository }}/actions/workflows/snipsync.yml).
79+
EOF
80+
)" \
81+
--head "$branch_name" \
82+
--base "main"
83+
fi

docs/develop/dotnet/core-application.mdx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,60 @@ This means there are several things Workflows cannot do such as:
8484
Some calls in .NET do unsuspecting non-deterministic things and are easy to accidentally use.
8585
This is especially true with `Task`s.
8686
Temporal requires that the deterministic `TaskScheduler.Current` is used, but many .NET async calls will use `TaskScheduler.Default` implicitly (and some analyzers even encourage this).
87+
88+
The following sections cover replay-safe APIs, followed by .NET-specific `Task` gotchas.
89+
90+
#### Logging
91+
92+
Use [`Workflow.Logger`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Logger), which is an instance of .NET's `ILogger`. The SDK logger automatically suppresses log messages during replay to avoid duplicates:
93+
94+
```csharp
95+
Workflow.Logger.LogInformation("Starting workflow for {Name}", name);
96+
```
97+
98+
For logger configuration, see [Observability: Logging](/develop/dotnet/observability#logging).
99+
100+
#### Random numbers and UUIDs
101+
102+
Use [`Workflow.Random`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Random) to get a deterministic `Random` instance. For UUIDs, use [`Workflow.NewGuid()`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_NewGuid). Never use `System.Random` or `Guid.NewGuid()` directly:
103+
104+
```csharp
105+
// Good - deterministic across replays
106+
var value = Workflow.Random.Next(1, 100);
107+
var uniqueId = Workflow.NewGuid();
108+
109+
// Bad - different result on every replay
110+
var value = new Random().Next(1, 100);
111+
```
112+
113+
#### Current time
114+
115+
Use [`Workflow.UtcNow`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_UtcNow) instead of `DateTime.UtcNow`. The SDK returns the time of the last Workflow Task, which is consistent across replays:
116+
117+
```csharp
118+
var currentTime = Workflow.UtcNow;
119+
```
120+
121+
#### Detecting replay (advanced)
122+
123+
Use [`Workflow.Unsafe.IsReplaying`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.Unsafe.html#Temporalio_Workflows_Workflow_Unsafe_IsReplaying) to guard code that should only run on the first execution, such as emitting metrics or sending external notifications from an Interceptor.
124+
:::caution
125+
126+
Never use this to affect Workflow business logic — branching on replay status breaks determinism.
127+
128+
:::
129+
130+
```csharp
131+
if (!Workflow.Unsafe.IsReplaying)
132+
{
133+
EmitMetric("workflow_started", 1);
134+
}
135+
```
136+
137+
If your goal is to always take action when something new is happening, check that `Workflow.Unsafe.IsReplayingHistoryEvents` is false instead. This will be false during read-only operations like queries and update validators. This is what the SDK's built-in logger and metric meter use internally.
138+
139+
#### .NET Task gotchas
140+
87141
Here are some known gotchas to avoid with .NET tasks inside of Workflows:
88142

89143
- Do not use `Task.Run` - this uses the default scheduler and puts work on the thread pool.

docs/develop/go/cancellation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func YourWorkflow(ctx workflow.Context) error {
5252
WaitForCancellation: true,
5353
}
5454
defer func() {
55-
// This logic ensures cleanup only happens if there is a Cancellation error
55+
// This logic ensures cleanup only happens if there is a Cancelation error
5656
if !errors.Is(ctx.Err(), workflow.ErrCanceled) {
5757
return
5858
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
---
2+
id: context-propagation
3+
title: Context Propagation - Go SDK
4+
sidebar_label: Context Propagation
5+
toc_max_heading_level: 4
6+
keywords:
7+
- context propagation
8+
- context propagators
9+
- go sdk
10+
- headers
11+
- tracing
12+
tags:
13+
- Context Propagation
14+
- Go SDK
15+
- Temporal SDKs
16+
description: How to propagate custom key-value data across Workflow, Activity, and Child Workflow boundaries using the Temporal Go SDK.
17+
---
18+
19+
Context propagation lets you pass custom key-value data from a Client to Workflows, and from Workflows to Activities and Child Workflows, without threading it through every function signature. Common use cases include propagating tracing IDs, tenant IDs, auth tokens, or other request-scoped metadata.
20+
21+
{/* TODO: Link to /encyclopedia/context-propagation once that page lands */}
22+
23+
:::tip
24+
25+
If you want to propagate tracing context, check if there is a [built-in tracing interceptor](/develop/go/observability#tracing) for your library before building a custom context propagator.
26+
27+
:::
28+
29+
## How it works
30+
31+
1. **Register** a context propagator on the Client via `ContextPropagators` in [ClientOptions](https://pkg.go.dev/go.temporal.io/sdk/internal#ClientOptions)
32+
2. **Inject** - On outbound calls, the SDK calls `Inject` (from `context.Context`) or `InjectFromWorkflow` (from `workflow.Context`) to serialize values into Temporal headers
33+
3. **Extract** - On inbound calls, the SDK calls `Extract` (into `context.Context`) or `ExtractToWorkflow` (into `workflow.Context`) to deserialize headers back into the context
34+
4. **Access** - Your Workflow and Activity code reads values from the context as usual
35+
36+
## Implement a context propagator
37+
38+
A context propagator implements the [`ContextPropagator`](https://pkg.go.dev/go.temporal.io/sdk/workflow#ContextPropagator) interface:
39+
40+
```go
41+
type ContextPropagator interface {
42+
// Inject writes values from a Go context.Context into headers (Client/Activity side)
43+
Inject(context.Context, HeaderWriter) error
44+
// Extract reads headers into a Go context.Context (Client/Activity side)
45+
Extract(context.Context, HeaderReader) (context.Context, error)
46+
// InjectFromWorkflow writes values from a workflow.Context into headers
47+
InjectFromWorkflow(Context, HeaderWriter) error
48+
// ExtractToWorkflow reads headers into a workflow.Context
49+
ExtractToWorkflow(Context, HeaderReader) (Context, error)
50+
}
51+
```
52+
53+
There are two pairs of methods because Go uses `context.Context` in non-Workflow code (Client, Activities) and `workflow.Context` inside Workflows. You must implement all four methods for values to propagate across every boundary (Client → Workflow → Activity/Child Workflow).
54+
55+
Here is a propagator that carries a custom key-value pair from the Client to Workflows and Activities (from the [context propagation sample](https://github.com/temporalio/samples-go/tree/main/ctxpropagation)):
56+
57+
58+
<!--SNIPSTART samples-go-ctx-propagation-propagator-->
59+
[ctxpropagation/propagator.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/propagator.go)
60+
```go
61+
type (
62+
// contextKey is an unexported type used as key for items stored in the
63+
// Context object
64+
contextKey struct{}
65+
66+
// propagator implements the custom context propagator
67+
propagator struct{}
68+
69+
// Values is a struct holding values
70+
Values struct {
71+
Key string `json:"key"`
72+
Value string `json:"value"`
73+
}
74+
)
75+
76+
// PropagateKey is the key used to store the value in the Context object
77+
var PropagateKey = contextKey{}
78+
79+
// HeaderKey is the key used by the propagator to pass values through the
80+
// Temporal server headers
81+
const HeaderKey = "custom-header"
82+
83+
// NewContextPropagator returns a context propagator that propagates a set of
84+
// string key-value pairs across a workflow
85+
func NewContextPropagator() workflow.ContextPropagator {
86+
return &propagator{}
87+
}
88+
89+
// Inject injects values from context into headers for propagation
90+
func (s *propagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error {
91+
value := ctx.Value(PropagateKey)
92+
payload, err := converter.GetDefaultDataConverter().ToPayload(value)
93+
if err != nil {
94+
return err
95+
}
96+
writer.Set(HeaderKey, payload)
97+
return nil
98+
}
99+
100+
// InjectFromWorkflow injects values from context into headers for propagation
101+
func (s *propagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error {
102+
value := ctx.Value(PropagateKey)
103+
payload, err := converter.GetDefaultDataConverter().ToPayload(value)
104+
if err != nil {
105+
return err
106+
}
107+
writer.Set(HeaderKey, payload)
108+
return nil
109+
}
110+
111+
// Extract extracts values from headers and puts them into context
112+
func (s *propagator) Extract(ctx context.Context, reader workflow.HeaderReader) (context.Context, error) {
113+
if value, ok := reader.Get(HeaderKey); ok {
114+
var values Values
115+
if err := converter.GetDefaultDataConverter().FromPayload(value, &values); err != nil {
116+
return ctx, nil
117+
}
118+
ctx = context.WithValue(ctx, PropagateKey, values)
119+
}
120+
121+
return ctx, nil
122+
}
123+
```
124+
<!--SNIPEND-->
125+
126+
## Register the propagator and set context values
127+
128+
Register the propagator on the Client. Then set context values before starting a Workflow:
129+
130+
<!--SNIPSTART samples-go-ctx-propagation-starter-->
131+
[ctxpropagation/starter/main.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/starter/main.go)
132+
```go
133+
// The client is a heavyweight object that should be created once per process.
134+
c, err := client.Dial(client.Options{
135+
HostPort: client.DefaultHostPort,
136+
Interceptors: []interceptor.ClientInterceptor{tracingInterceptor},
137+
ContextPropagators: []workflow.ContextPropagator{ctxpropagation.NewContextPropagator()},
138+
})
139+
if err != nil {
140+
log.Fatalln("Unable to create client", err)
141+
}
142+
defer c.Close()
143+
144+
workflowID := "ctx-propagation_" + uuid.New()
145+
workflowOptions := client.StartWorkflowOptions{
146+
ID: workflowID,
147+
TaskQueue: "ctx-propagation",
148+
}
149+
150+
ctx := context.Background()
151+
ctx = context.WithValue(ctx, ctxpropagation.PropagateKey, &ctxpropagation.Values{Key: "test", Value: "tested"})
152+
153+
we, err := c.ExecuteWorkflow(ctx, workflowOptions, ctxpropagation.CtxPropWorkflow)
154+
```
155+
<!--SNIPEND-->
156+
157+
You can also register context propagators through a [Plugin](/develop/plugins-guide) if you are building a reusable library.
158+
159+
## Access propagated values
160+
161+
In your Workflow, the propagated values are available on the `workflow.Context`. When the Workflow starts an Activity, the SDK automatically propagates the same values:
162+
163+
<!--SNIPSTART samples-go-ctx-propagation-workflow-->
164+
[ctxpropagation/workflow.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/workflow.go)
165+
```go
166+
// CtxPropWorkflow workflow definition
167+
func CtxPropWorkflow(ctx workflow.Context) (err error) {
168+
ao := workflow.ActivityOptions{
169+
StartToCloseTimeout: 2 * time.Second, // such a short timeout to make sample fail over very fast
170+
}
171+
ctx = workflow.WithActivityOptions(ctx, ao)
172+
173+
if val := ctx.Value(PropagateKey); val != nil {
174+
vals := val.(Values)
175+
workflow.GetLogger(ctx).Info("custom context propagated to workflow", vals.Key, vals.Value)
176+
}
177+
178+
var values Values
179+
if err = workflow.ExecuteActivity(ctx, SampleActivity).Get(ctx, &values); err != nil {
180+
workflow.GetLogger(ctx).Error("Workflow failed.", "Error", err)
181+
return err
182+
}
183+
workflow.GetLogger(ctx).Info("context propagated to activity", values.Key, values.Value)
184+
workflow.GetLogger(ctx).Info("Workflow completed.")
185+
return nil
186+
}
187+
```
188+
<!--SNIPEND-->
189+
190+
<!--SNIPSTART samples-go-ctx-propagation-activity-->
191+
[ctxpropagation/activities.go](https://github.com/temporalio/samples-go/blob/main/ctxpropagation/activities.go)
192+
```go
193+
func SampleActivity(ctx context.Context) (*Values, error) {
194+
if val := ctx.Value(PropagateKey); val != nil {
195+
vals := val.(Values)
196+
return &vals, nil
197+
}
198+
return nil, nil
199+
}
200+
```
201+
<!--SNIPEND-->
202+
203+
You can configure multiple context propagators on a single Client, each responsible for its own set of keys.
204+
205+
## Context propagation over Nexus
206+
207+
Nexus does not use the `ContextPropagator` interface. It relies on a Temporal-agnostic protocol with its own header format (`nexus.Header`, a wrapper around `map[string]string`).
208+
209+
To propagate context over Nexus Operation calls, use interceptors to explicitly serialize and deserialize context into the Nexus header. See the [Nexus Context Propagation sample](https://github.com/temporalio/samples-go/tree/main/nexus-context-propagation).
210+
211+
## Further reading
212+
213+
- [Passing Context with Temporal](https://spiralscout.com/blog/passing-context-with-temporal) - A conceptual guide to middleware and walkthrough of building a context propagator in Go

0 commit comments

Comments
 (0)