Files
tasks/common/contextutil/metadata.go
nikki-dagandGitHub 139ad62c9d History: Expose and call SetContextMetadata in readonly operations (#9240)
## What changed?
1. Exposed metadata functionality:
    - Made `SetContextMetadata()` public in MutableState interface
- Added constants for metadata keys (`MetadataKeyWorkflowType`,
`MetadataKeyWorkflowTaskQueue`)
- Added `ContextHasMetadata()` helper function to check if context has
metadata support
  2. Fixed readonly operations:
- `QueryWorkflow`: Now explicitly calls `SetContextMetadata()` since
queries never close transactions (readonly operation)
- `RespondWorkflowTaskCompleted`: Calls `SetContextMetadata()` when
there are only messages (e.g., update rejections) and no commands, since
these also don't close transactions
3. Added tests for the new `ContextHasMetadata()` helper and
`SetContextMetadata()` functionality

## Why?
Context metadata (workflow type, task queue) is automatically populated
when mutable state transactions close. However, readonly operations
never close transactions, so the metadata was missing from their
contexts.
This change ensures that successful readonly operations (queries, update
rejections) also have workflow metadata populated in their contexts

## How did you test it?
- [ ] built
- [ ] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)



---

> [!NOTE]
> **Medium Risk**
> Touches core history API request paths and mutable state interfaces;
while behavior is additive (context tagging), incorrect invocation or
key usage could affect observability/metrics and tests across multiple
workflows.
> 
> **Overview**
> Ensures workflow context metadata (workflow type/task queue) is
consistently populated even for readonly paths that don’t close
mutable-state transactions.
> 
> This exposes `MutableState.SetContextMetadata(ctx)` (renaming the
internal helper), standardizes metadata keys via
`contextutil.MetadataKeyWorkflowType`/`MetadataKeyWorkflowTaskQueue`,
adds `ContextHasMetadata()` for debugging, and explicitly calls
`SetContextMetadata` in `QueryWorkflow` and in
`RespondWorkflowTaskCompleted` when `Commands` is empty (e.g., update
rejections / heartbeats). Tests are expanded to assert metadata presence
across success and several error/edge cases.
> 
> <sup>Written by [Cursor
Bugbot]() for commit
69c3140233241c2072e3bacff7580debe91ed884. This will update automatically
on new commits. Configure
[here]().</sup>
2026-02-10 20:22:43 -06:00

99 lines
2.4 KiB
Go

package contextutil
import (
"context"
"sync"
)
type (
metadataContextKey struct{}
// metadataContext is used to store workflow and activity metadata
metadataContext struct {
sync.Mutex
Metadata map[string]any
}
)
var metadataCtxKey = metadataContextKey{}
const (
// MetadataKeyWorkflowType is the context metadata key for workflow type
MetadataKeyWorkflowType = "workflow-type"
// MetadataKeyWorkflowTaskQueue is the context metadata key for workflow task queue
MetadataKeyWorkflowTaskQueue = "workflow-task-queue"
)
// getMetadataContext extracts metadata context from golang context.
func getMetadataContext(ctx context.Context) *metadataContext {
metadataCtx := ctx.Value(metadataCtxKey)
if metadataCtx == nil {
return nil
}
mc, ok := metadataCtx.(*metadataContext)
if !ok {
return nil
}
return mc
}
// WithMetadataContext adds a metadata context to the given context.
func WithMetadataContext(ctx context.Context) context.Context {
metadataCtx := &metadataContext{
Metadata: make(map[string]any),
}
return context.WithValue(ctx, metadataCtxKey, metadataCtx)
}
// ContextHasMetadata returns true if the context has metadata support.
// This can be used to debug whether a context has been properly initialized with metadata.
func ContextHasMetadata(ctx context.Context) bool {
return getMetadataContext(ctx) != nil
}
// ContextMetadataSet sets a metadata key-value pair in the context.
func ContextMetadataSet(ctx context.Context, key string, value any) bool {
metadataCtx := getMetadataContext(ctx)
if metadataCtx == nil {
return false
}
metadataCtx.Lock()
defer metadataCtx.Unlock()
metadataCtx.Metadata[key] = value
return true
}
// ContextMetadataGet retrieves a metadata value from the context.
func ContextMetadataGet(ctx context.Context, key string) (any, bool) {
metadataCtx := getMetadataContext(ctx)
if metadataCtx == nil {
return nil, false
}
metadataCtx.Lock()
defer metadataCtx.Unlock()
value, ok := metadataCtx.Metadata[key]
return value, ok
}
// ContextMetadataGetAll retrieves all metadata from the context as a map copy.
func ContextMetadataGetAll(ctx context.Context) map[string]any {
metadataCtx := getMetadataContext(ctx)
if metadataCtx == nil {
return nil
}
metadataCtx.Lock()
defer metadataCtx.Unlock()
// Return a copy to prevent external modifications
result := make(map[string]any, len(metadataCtx.Metadata))
for k, v := range metadataCtx.Metadata {
result[k] = v
}
return result
}