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
2 changes: 1 addition & 1 deletion dapr/ext/workflow/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Workflow (orchestrator) functions must remain generators (`def` with `yield`). T

Client for workflow lifecycle management:

- `schedule_new_workflow(workflow, *, input, instance_id, start_at, reuse_id_policy)` → returns `instance_id`
- `schedule_new_workflow(workflow, *, input, instance_id, start_at, reuse_id_policy)` → returns `instance_id`. `reuse_id_policy` is deprecated (no effect, emits `DeprecationWarning`): an instance ID can always be reused once the existing instance is in a terminal state.
- `get_workflow_state(instance_id, *, fetch_payloads=True)` → `Optional[WorkflowState]`
- `wait_for_workflow_start(instance_id, *, fetch_payloads, timeout_in_seconds)`
- `wait_for_workflow_completion(instance_id, *, fetch_payloads, timeout_in_seconds)`
Expand Down
15 changes: 13 additions & 2 deletions dapr/ext/workflow/_durabletask/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,15 @@ def __str__(self):
return helpers.get_orchestration_status_str(self.value)


# TODO(v1.21): remove OrchestrationIdReuseAction, WorkflowIdReusePolicy, and the reuse_id_policy
# parameter on schedule_new_orchestration (sync and aio) — deprecated in 1.18, kept for the
# 3-version policy.
class OrchestrationIdReuseAction(Enum):
"""Action to take when scheduling an orchestration whose ID already exists."""
"""Action to take when scheduling an orchestration whose ID already exists.

Deprecated: has no effect and will be removed in a future release. An orchestration ID
can always be reused once the existing instance has reached a terminal state.
"""

ERROR = 0
IGNORE = 1
Expand All @@ -72,7 +79,11 @@ class OrchestrationIdReuseAction(Enum):

@dataclass
class WorkflowIdReusePolicy:
"""Policy controlling what happens when a new orchestration is scheduled with an ID that already exists."""
"""Policy controlling what happens when a new orchestration is scheduled with an ID that already exists.

Deprecated: has no effect and will be removed in a future release. An orchestration ID
can always be reused once the existing instance has reached a terminal state.
"""

action: OrchestrationIdReuseAction
operation_status: list[OrchestrationStatus]
Expand Down
15 changes: 13 additions & 2 deletions dapr/ext/workflow/aio/dapr_workflow_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from datetime import datetime
from typing import Any, Optional, TypeVar, Union
from warnings import warn

from grpc.aio import AioRpcError

Expand Down Expand Up @@ -109,12 +110,22 @@ async def schedule_new_workflow(
start_at: The time when the workflow instance should start executing.
If not specified or if a date-time in the past is specified, the workflow instance will
be scheduled immediately.
reuse_id_policy: Optional policy to reuse the workflow id when there is a conflict with
an existing workflow instance.
reuse_id_policy: Deprecated and has no effect; it will be removed in a future
release. A workflow instance ID can always be reused once the existing instance
with that ID has reached a terminal state (e.g. COMPLETED, FAILED, or TERMINATED).

Returns:
The ID of the scheduled workflow instance.
"""
# TODO(v1.21): remove reuse_id_policy — deprecated in 1.18, kept for the 3-version policy.
if reuse_id_policy is not None:
warn(
'reuse_id_policy is deprecated and has no effect; it will be removed in a '
'future release. A workflow instance ID can always be reused once the existing '
'instance with that ID has reached a terminal state.',
DeprecationWarning,
stacklevel=2,
)
if isinstance(workflow, str):
workflow_name = workflow
elif hasattr(workflow, '_dapr_alternate_name'):
Expand Down
15 changes: 13 additions & 2 deletions dapr/ext/workflow/dapr_workflow_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from datetime import datetime
from typing import Any, Optional, TypeVar, Union
from warnings import warn

from grpc import RpcError

Expand Down Expand Up @@ -111,12 +112,22 @@ def schedule_new_workflow(
start_at: The time when the workflow instance should start executing.
If not specified or if a date-time in the past is specified, the workflow instance will
be scheduled immediately.
reuse_id_policy: Optional policy to reuse the workflow id when there is a conflict with
an existing workflow instance.
reuse_id_policy: Deprecated and has no effect; it will be removed in a future
release. A workflow instance ID can always be reused once the existing instance
with that ID has reached a terminal state (e.g. COMPLETED, FAILED, or TERMINATED).

Returns:
The ID of the scheduled workflow instance.
"""
# TODO(v1.21): remove reuse_id_policy — deprecated in 1.18, kept for the 3-version policy.
if reuse_id_policy is not None:
warn(
'reuse_id_policy is deprecated and has no effect; it will be removed in a '
'future release. A workflow instance ID can always be reused once the existing '
'instance with that ID has reached a terminal state.',
DeprecationWarning,
stacklevel=2,
)
if isinstance(workflow, str):
workflow_name = workflow
elif hasattr(workflow, '_dapr_alternate_name'):
Expand Down
3 changes: 2 additions & 1 deletion examples/workflow/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def send_alert(ctx, message: str):
pass

if not status or status.runtime_status.name != 'RUNNING':
# TODO update to use reuse_id_policy
# Scheduling over a RUNNING instance fails; reuse of the ID is only allowed once the
# existing instance has reached a terminal state, hence the pre-check above.
instance_id = wf_client.schedule_new_workflow(
workflow=status_monitor_workflow,
input=JobStatus(job_id=job_id, is_healthy=True),
Expand Down
52 changes: 0 additions & 52 deletions tests/clients/grpc_private.key

This file was deleted.

28 changes: 0 additions & 28 deletions tests/clients/grpc_selfsigned.pem

This file was deleted.

28 changes: 28 additions & 0 deletions tests/ext/workflow/test_workflow_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

import unittest
import warnings
from datetime import datetime
from typing import Any, Union
from unittest import mock
Expand Down Expand Up @@ -198,6 +199,33 @@ def test_schedule_workflow_by_name_string(self):
assert result == mock_schedule_result
assert fake_client.last_scheduled_workflow_name == 'my_registered_workflow'

def test_schedule_workflow_reuse_id_policy_is_deprecated(self):
fake_client = FakeTaskHubGrpcClient()
with mock.patch(
'dapr.ext.workflow._durabletask.client.TaskHubGrpcClient', return_value=fake_client
):
wfClient = DaprWorkflowClient()
reuse_id_policy = client.WorkflowIdReusePolicy(
action=client.OrchestrationIdReuseAction.TERMINATE,
operation_status=[client.OrchestrationStatus.RUNNING],
)
with self.assertWarns(DeprecationWarning):
result = wfClient.schedule_new_workflow(
workflow='my_registered_workflow', reuse_id_policy=reuse_id_policy
)
assert result == mock_schedule_result

def test_schedule_workflow_without_reuse_id_policy_does_not_warn(self):
fake_client = FakeTaskHubGrpcClient()
with mock.patch(
'dapr.ext.workflow._durabletask.client.TaskHubGrpcClient', return_value=fake_client
):
wfClient = DaprWorkflowClient()
with warnings.catch_warnings():
warnings.simplefilter('error', DeprecationWarning)
result = wfClient.schedule_new_workflow(workflow='my_registered_workflow')
assert result == mock_schedule_result

def test_client_functions(self):
with mock.patch(
'dapr.ext.workflow._durabletask.client.TaskHubGrpcClient',
Expand Down
30 changes: 30 additions & 0 deletions tests/ext/workflow/test_workflow_client_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

import unittest
import warnings
from datetime import datetime
from typing import Any, Union
from unittest import mock
Expand Down Expand Up @@ -202,6 +203,35 @@ async def test_schedule_workflow_by_name_string(self):
assert result == mock_schedule_result
assert fake_client.last_scheduled_workflow_name == 'my_registered_workflow'

async def test_schedule_workflow_reuse_id_policy_is_deprecated(self):
fake_client = FakeAsyncTaskHubGrpcClient()
with mock.patch(
'dapr.ext.workflow._durabletask.aio.client.AsyncTaskHubGrpcClient',
return_value=fake_client,
):
wfClient = DaprWorkflowClient()
reuse_id_policy = client.WorkflowIdReusePolicy(
action=client.OrchestrationIdReuseAction.TERMINATE,
operation_status=[client.OrchestrationStatus.RUNNING],
)
with self.assertWarns(DeprecationWarning):
result = await wfClient.schedule_new_workflow(
workflow='my_registered_workflow', reuse_id_policy=reuse_id_policy
)
assert result == mock_schedule_result

async def test_schedule_workflow_without_reuse_id_policy_does_not_warn(self):
fake_client = FakeAsyncTaskHubGrpcClient()
with mock.patch(
'dapr.ext.workflow._durabletask.aio.client.AsyncTaskHubGrpcClient',
return_value=fake_client,
):
wfClient = DaprWorkflowClient()
with warnings.catch_warnings():
warnings.simplefilter('error', DeprecationWarning)
result = await wfClient.schedule_new_workflow(workflow='my_registered_workflow')
assert result == mock_schedule_result

async def test_client_functions(self):
with mock.patch(
'dapr.ext.workflow._durabletask.aio.client.AsyncTaskHubGrpcClient',
Expand Down
Loading