Skip to content

Commit 700cd8b

Browse files
authored
Merge branch 'main' into andystaples-add-purge-timeout
2 parents 6a4e68a + 9a660a1 commit 700cd8b

11 files changed

Lines changed: 120 additions & 31 deletions

File tree

‎.github/copilot-instructions.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,17 @@ works correctly:
224224
- If a new public API is added to the core SDK (e.g. a method on
225225
`OrchestrationContext`), confirm it is accessible through the
226226
azuremanaged package and add a test or example if appropriate.
227+
228+
## Release Coordination
229+
230+
- Package versions and inter-package minimum dependencies record the latest
231+
released compatibility contract. They are not an instruction to bump versions
232+
in a feature PR.
233+
- When a core `durabletask` API is consumed by `durabletask.azuremanaged` or
234+
`azure-functions-durable`, do not update package versions or dependency
235+
minimums in the feature PR. Document the affected package changelogs and
236+
release coordination instead.
237+
- Create a dedicated release PR after the core package has been released. The
238+
pipeline will publish `durabletask` first, so bump all dependency minimums,
239+
package versions, and release notes together so resolvers cannot select a
240+
provider release with an incompatible older core package.

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ ADDED
1212
- Added an optional timeout to filtered orchestration purges. Callers can now
1313
limit a purge operation's duration and inspect `PurgeInstancesResult.is_complete`
1414
for completed, partial, or backend-unknown completion status.
15+
- Added optional `reason` parameters to `TaskHubGrpcClient` and
16+
`AsyncTaskHubGrpcClient` suspend and resume operations. The reason is now sent
17+
to the backend with the lifecycle request.
1518
- Added the optional `new_version` argument to
1619
`OrchestrationContext.continue_as_new()` so continued orchestrations can
1720
switch to a new version.

‎azure-functions-durable/CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ ADDED
1111

1212
- Added an optional timeout to filtered orchestration purges through
1313
`DurableFunctionsClient` and `SyncDurableFunctionsClient`.
14+
> [!NOTE]
15+
> Release this change only in coordination with a new `durabletask` release
16+
> that contains the suspend/resume reason APIs. Publish `durabletask` first,
17+
> then update this package's minimum dependency in its dedicated release PR.
18+
19+
FIXED
20+
21+
- Fixed deprecated `DurableFunctionsClient.suspend()` and `resume()` methods
22+
discarding their `reason` arguments. Reasons are now forwarded to the Durable
23+
Task backend.
1424

1525
## v2.0.0b2
1626

‎azure-functions-durable/azure/durable_functions/client.py‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -431,21 +431,13 @@ async def purge_instance_history(self, instance_id: str) -> PurgeHistoryResult:
431431

432432
@deprecated("suspend is deprecated; use suspend_orchestration instead.")
433433
async def suspend(self, instance_id: str, reason: Optional[str] = None) -> None:
434-
"""Deprecated alias for :meth:`suspend_orchestration`.
435-
436-
The v1 ``reason`` argument has no equivalent in durabletask and is
437-
ignored.
438-
"""
439-
await self.suspend_orchestration(instance_id)
434+
"""Deprecated alias for :meth:`suspend_orchestration`."""
435+
await self.suspend_orchestration(instance_id, reason=reason)
440436

441437
@deprecated("resume is deprecated; use resume_orchestration instead.")
442438
async def resume(self, instance_id: str, reason: Optional[str] = None) -> None:
443-
"""Deprecated alias for :meth:`resume_orchestration`.
444-
445-
The v1 ``reason`` argument has no equivalent in durabletask and is
446-
ignored.
447-
"""
448-
await self.resume_orchestration(instance_id)
439+
"""Deprecated alias for :meth:`resume_orchestration`."""
440+
await self.resume_orchestration(instance_id, reason=reason)
449441

450442
@deprecated("restart is deprecated; use restart_orchestration instead.")
451443
async def restart(

‎durabletask-azuremanaged/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ ADDED
1111

1212
- Added an optional timeout to filtered orchestration purges through
1313
`DurableTaskSchedulerClient` and `AsyncDurableTaskSchedulerClient`.
14+
- Added optional `reason` parameters to suspend and resume operations inherited
15+
from `TaskHubGrpcClient` and `AsyncTaskHubGrpcClient`.
1416

1517
## v1.9.0
1618

‎durabletask/client.py‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -799,13 +799,21 @@ def terminate_orchestration(self, instance_id: str, *,
799799
)
800800
self._stub.TerminateInstance(req)
801801

802-
def suspend_orchestration(self, instance_id: str) -> None:
803-
req = pb.SuspendRequest(instanceId=instance_id)
802+
def suspend_orchestration(self, instance_id: str, *,
803+
reason: str | None = None) -> None:
804+
req = pb.SuspendRequest(
805+
instanceId=instance_id,
806+
reason=helpers.get_string_value(reason),
807+
)
804808
self._logger.info(f"Suspending instance '{instance_id}'.")
805809
self._stub.SuspendInstance(req)
806810

807-
def resume_orchestration(self, instance_id: str) -> None:
808-
req = pb.ResumeRequest(instanceId=instance_id)
811+
def resume_orchestration(self, instance_id: str, *,
812+
reason: str | None = None) -> None:
813+
req = pb.ResumeRequest(
814+
instanceId=instance_id,
815+
reason=helpers.get_string_value(reason),
816+
)
809817
self._logger.info(f"Resuming instance '{instance_id}'.")
810818
self._stub.ResumeInstance(req)
811819

@@ -1335,13 +1343,21 @@ async def terminate_orchestration(self, instance_id: str, *,
13351343
)
13361344
await self._get_stub().TerminateInstance(req)
13371345

1338-
async def suspend_orchestration(self, instance_id: str) -> None:
1339-
req = pb.SuspendRequest(instanceId=instance_id)
1346+
async def suspend_orchestration(self, instance_id: str, *,
1347+
reason: str | None = None) -> None:
1348+
req = pb.SuspendRequest(
1349+
instanceId=instance_id,
1350+
reason=helpers.get_string_value(reason),
1351+
)
13401352
self._logger.info(f"Suspending instance '{instance_id}'.")
13411353
await self._get_stub().SuspendInstance(req)
13421354

1343-
async def resume_orchestration(self, instance_id: str) -> None:
1344-
req = pb.ResumeRequest(instanceId=instance_id)
1355+
async def resume_orchestration(self, instance_id: str, *,
1356+
reason: str | None = None) -> None:
1357+
req = pb.ResumeRequest(
1358+
instanceId=instance_id,
1359+
reason=helpers.get_string_value(reason),
1360+
)
13451361
self._logger.info(f"Resuming instance '{instance_id}'.")
13461362
await self._get_stub().ResumeInstance(req)
13471363

‎durabletask/internal/helpers.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,19 +234,23 @@ def new_event_raised_event(name: str, encoded_input: str | None = None) -> pb.Hi
234234
)
235235

236236

237-
def new_suspend_event() -> pb.HistoryEvent:
237+
def new_suspend_event(*, encoded_input: str | None = None) -> pb.HistoryEvent:
238238
return pb.HistoryEvent(
239239
eventId=-1,
240240
timestamp=timestamp_pb2.Timestamp(),
241-
executionSuspended=pb.ExecutionSuspendedEvent()
241+
executionSuspended=pb.ExecutionSuspendedEvent(
242+
input=get_string_value(encoded_input)
243+
)
242244
)
243245

244246

245-
def new_resume_event() -> pb.HistoryEvent:
247+
def new_resume_event(*, encoded_input: str | None = None) -> pb.HistoryEvent:
246248
return pb.HistoryEvent(
247249
eventId=-1,
248250
timestamp=timestamp_pb2.Timestamp(),
249-
executionResumed=pb.ExecutionResumedEvent()
251+
executionResumed=pb.ExecutionResumedEvent(
252+
input=get_string_value(encoded_input)
253+
)
250254
)
251255

252256

‎durabletask/testing/in_memory_backend.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,9 @@ def SuspendInstance(self, request: pb.SuspendRequest, context: grpc.ServicerCont
386386
if instance.status == pb.ORCHESTRATION_STATUS_SUSPENDED:
387387
return pb.SuspendResponse()
388388

389-
event = helpers.new_suspend_event()
389+
event = helpers.new_suspend_event(
390+
encoded_input=request.reason.value if request.HasField("reason") else None
391+
)
390392
instance.pending_events.append(event)
391393
instance.last_updated_at = datetime.now(timezone.utc)
392394
self._enqueue_orchestration(instance.instance_id)
@@ -403,7 +405,9 @@ def ResumeInstance(self, request: pb.ResumeRequest, context: grpc.ServicerContex
403405
f"Orchestration instance '{request.instanceId}' not found")
404406
return pb.ResumeResponse()
405407

406-
event = helpers.new_resume_event()
408+
event = helpers.new_resume_event(
409+
encoded_input=request.reason.value if request.HasField("reason") else None
410+
)
407411
instance.pending_events.append(event)
408412
instance.last_updated_at = datetime.now(timezone.utc)
409413
self._enqueue_orchestration(instance.instance_id)

‎tests/azure-functions-durable/test_client_compat.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,13 @@ async def test_suspend_resume_delegate():
472472
new=AsyncMock()) as suspend_mock:
473473
with pytest.warns(DeprecationWarning):
474474
await client.suspend("abc", "reason")
475-
suspend_mock.assert_awaited_once_with("abc")
475+
suspend_mock.assert_awaited_once_with("abc", reason="reason")
476476

477477
with patch.object(client, "resume_orchestration",
478478
new=AsyncMock()) as resume_mock:
479479
with pytest.warns(DeprecationWarning):
480480
await client.resume("abc", "reason")
481-
resume_mock.assert_awaited_once_with("abc")
481+
resume_mock.assert_awaited_once_with("abc", reason="reason")
482482
finally:
483483
await client.close()
484484

‎tests/durabletask/test_client.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,43 @@ def test_worker_stores_resiliency_options():
12581258
assert worker._resiliency_options is resiliency
12591259

12601260

1261+
def test_suspend_resume_orchestration_includes_optional_reason():
1262+
stub = MagicMock()
1263+
1264+
with patch('durabletask.client.shared.get_grpc_channel', return_value=MagicMock()), patch(
1265+
'durabletask.client.stubs.TaskHubSidecarServiceStub', return_value=stub):
1266+
client = TaskHubGrpcClient()
1267+
client.suspend_orchestration('suspended', reason='maintenance')
1268+
client.resume_orchestration('resumed')
1269+
1270+
suspend_request = stub.SuspendInstance.call_args.args[0]
1271+
assert suspend_request.instanceId == 'suspended'
1272+
assert suspend_request.reason.value == 'maintenance'
1273+
resume_request = stub.ResumeInstance.call_args.args[0]
1274+
assert resume_request.instanceId == 'resumed'
1275+
assert not resume_request.HasField('reason')
1276+
1277+
1278+
@pytest.mark.asyncio
1279+
async def test_async_suspend_resume_orchestration_includes_optional_reason():
1280+
stub = MagicMock()
1281+
stub.SuspendInstance = AsyncMock()
1282+
stub.ResumeInstance = AsyncMock()
1283+
1284+
with patch('durabletask.client.shared.get_async_grpc_channel', return_value=MagicMock()), patch(
1285+
'durabletask.client.stubs.TaskHubSidecarServiceStub', return_value=stub):
1286+
client = AsyncTaskHubGrpcClient()
1287+
await client.suspend_orchestration('suspended')
1288+
await client.resume_orchestration('resumed', reason='maintenance complete')
1289+
1290+
suspend_request = stub.SuspendInstance.call_args.args[0]
1291+
assert suspend_request.instanceId == 'suspended'
1292+
assert not suspend_request.HasField('reason')
1293+
resume_request = stub.ResumeInstance.call_args.args[0]
1294+
assert resume_request.instanceId == 'resumed'
1295+
assert resume_request.reason.value == 'maintenance complete'
1296+
1297+
12611298
def test_get_orchestration_history_aggregates_chunks_and_deexternalizes_payloads():
12621299
store = FakePayloadStore()
12631300
token = store.upload(b'history payload')

0 commit comments

Comments
 (0)