mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-03 03:56:04 +00:00
## 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>
99 lines
2.4 KiB
Go
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
|
|
}
|