Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ to docs, or any other relevant information.

## [Unreleased]

### Added

- Added the experimental `TemporalWorkerOptions.PatchActivationCallback`, allowing workers to
decide whether a first non-replay `Workflow.Patched` call should activate a patch during rolling
deployments.

### Changed

- Hardened read-only workflow context enforcement so queries, update validators, and patch activation
callbacks cannot mutate handlers or workflow details, invoke patches, or schedule workflow work.
Patch activation callbacks also cannot use workflow randomness or issue workflow commands.

### [1.17.0] - 2026-07-13

### Added
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,9 @@ can be used from workflows including:
workflow calls elsewhere.
* `GetExternalWorkflowHandle` - Get a handle to an external workflow to issue cancellation requests and signals.
* `NewGuid` - Create a deterministically random UUIDv4 GUID.
* `Patched` and `DeprecatePatch` - Support for patch-based versioning inside the workflow.
* `Patched` and `DeprecatePatch` - Support for patch-based versioning inside the workflow. Workers
can set `PatchActivationCallback` to decide whether a newly introduced patch should activate
during rolling deployments.
* `UpsertMemo` - Update the memo values for the workflow.
* `UpsertTypedSearchAttributes` - Update the search attributes for the workflow.

Expand Down
31 changes: 24 additions & 7 deletions src/Temporalio/Worker/NotifyOnSetDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace Temporalio.Worker
{
/// <summary>
/// Specialized dictionary that invokes a callback on each value set (but not value delete).
/// Specialized dictionary that invokes callbacks before mutation and after each value set.
/// </summary>
/// <typeparam name="TKey">Dictionary key type.</typeparam>
/// <typeparam name="TValue">Dictionary value type.</typeparam>
Expand All @@ -18,18 +18,23 @@ internal class NotifyOnSetDictionary<TKey, TValue> : IDictionary<TKey, TValue>,
{
private readonly Dictionary<TKey, TValue> dict;
private readonly Action<TKey, TValue> callback;
private readonly Action beforeMutation;

/// <summary>
/// Initializes a new instance of the <see cref="NotifyOnSetDictionary{TKey, TValue}"/> class.
/// </summary>
/// <param name="copyFrom">Dictionary to copy existing values from.</param>
/// <param name="callback">Callback to invoke on value set.</param>
/// <param name="beforeMutation">Callback to invoke before any mutation.</param>
public NotifyOnSetDictionary(
IEnumerable<KeyValuePair<TKey, TValue>> copyFrom, Action<TKey, TValue> callback)
IEnumerable<KeyValuePair<TKey, TValue>> copyFrom,
Action<TKey, TValue> callback,
Action beforeMutation)
{
// Can't use kvp enumerable constructor on Dictionary, too new
dict = copyFrom.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
this.callback = callback;
this.beforeMutation = beforeMutation;
}

/// <inheritdoc />
Expand Down Expand Up @@ -71,7 +76,11 @@ public TValue this[TKey key]
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);

/// <inheritdoc />
public void Clear() => dict.Clear();
public void Clear()
{
beforeMutation();
dict.Clear();
}

/// <inheritdoc />
public bool Contains(KeyValuePair<TKey, TValue> item) =>
Expand All @@ -88,11 +97,18 @@ public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) =>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => dict.GetEnumerator();

/// <inheritdoc />
public bool Remove(TKey key) => dict.Remove(key);
public bool Remove(TKey key)
{
beforeMutation();
return dict.Remove(key);
}

/// <inheritdoc />
public bool Remove(KeyValuePair<TKey, TValue> item) =>
((ICollection<KeyValuePair<TKey, TValue>>)dict).Remove(item);
public bool Remove(KeyValuePair<TKey, TValue> item)
{
beforeMutation();
return ((ICollection<KeyValuePair<TKey, TValue>>)dict).Remove(item);
}

/// <inheritdoc />
public bool TryGetValue(TKey key, out TValue value) =>
Expand All @@ -105,6 +121,7 @@ public bool TryGetValue(TKey key, out TValue value) =>

private void Set(TKey key, TValue value, bool overwrite)
{
beforeMutation();
if (!overwrite && ContainsKey(key))
{
throw new ArgumentException("Key already exists");
Expand All @@ -113,4 +130,4 @@ private void Set(TKey key, TValue value, bool overwrite)
callback(key, value);
}
}
}
}
18 changes: 18 additions & 0 deletions src/Temporalio/Worker/PatchActivationInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Temporalio.Workflows;

namespace Temporalio.Worker
{
/// <summary>
/// Input for <see cref="TemporalWorkerOptions.PatchActivationCallback" />.
/// </summary>
/// <param name="WorkflowInfo">Information about the workflow execution calling
/// <see cref="Workflow.Patched" />.</param>
/// <param name="PatchId">Patch ID passed to <see cref="Workflow.Patched" />.</param>
/// <remarks>
/// WARNING: This constructor may have required properties added. Do not rely on the exact
/// constructor, only use "with" clauses.
/// </remarks>
public record PatchActivationInput(
WorkflowInfo WorkflowInfo,
string PatchId);
}
1 change: 1 addition & 0 deletions src/Temporalio/Worker/TemporalWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public TemporalWorker(IWorkerClient client, TemporalWorkerOptions options)
RuntimeMetricMeter: MetricMeter,
WorkerLevelFailureExceptionTypes: options.WorkflowFailureExceptionTypes,
DisableEagerActivityExecution: options.DisableEagerActivityExecution,
PatchActivationCallback: options.PatchActivationCallback,
AssertValidLocalActivity: (activityType) => (activityWorker ??
throw new InvalidOperationException(
$"Activity {activityType} is not registered on this worker," +
Expand Down
14 changes: 14 additions & 0 deletions src/Temporalio/Worker/TemporalWorkerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,20 @@ public TemporalWorkerOptions()
/// </remarks>
public IReadOnlyCollection<Type>? WorkflowFailureExceptionTypes { get; set; }

/// <summary>
/// Gets or sets the callback that decides whether the first non-replay call to
/// <see cref="Workflow.Patched" /> for a patch ID should activate that patch.
/// </summary>
/// <remarks>
/// The callback is only invoked for a newly encountered patch. Existing history markers,
/// replay, and <see cref="Workflow.DeprecatePatch" /> bypass the callback. Returning <c>false</c>
/// leaves the patch inactive and does not record a patch marker. The callback runs in a
/// read-only workflow context and therefore cannot use workflow randomness, wait, or
/// schedule workflow commands.
/// </remarks>
/// <remarks>WARNING: This property is experimental and may change in the future.</remarks>
public Func<PatchActivationInput, bool>? PatchActivationCallback { get; set; }

/// <summary>
/// Gets or sets a value indicating whether deadlock detection will be disabled for all
/// workflows. If unset, this value defaults to true only if
Expand Down
Loading
Loading