Skip to content

Commit 459845f

Browse files
author
Ayushi Ahjolia
committed
feat: add should_complete to CompletionConfig
1 parent aff242b commit 459845f

13 files changed

Lines changed: 2044 additions & 20 deletions

File tree

packages/aws-durable-execution-sdk-python-examples/examples-catalog.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,17 @@
398398
},
399399
"path": "./src/map/map_with_failure_tolerance.py"
400400
},
401+
{
402+
"name": "Map with Should Complete",
403+
"description": "Map operation with custom should_complete predicate for early completion",
404+
"handler": "map_with_should_complete.handler",
405+
"integration": true,
406+
"durableConfig": {
407+
"RetentionPeriodInDays": 7,
408+
"ExecutionTimeout": 300
409+
},
410+
"path": "./src/map/map_with_should_complete.py"
411+
},
401412
{
402413
"name": "Map Completion Config",
403414
"description": "Reproduces issue where map with minSuccessful loses failure count",
@@ -442,6 +453,17 @@
442453
},
443454
"path": "./src/parallel/parallel_with_failure_tolerance.py"
444455
},
456+
{
457+
"name": "Parallel with Should Complete",
458+
"description": "Parallel operation with custom quorum-based should_complete predicate",
459+
"handler": "parallel_with_should_complete.handler",
460+
"integration": true,
461+
"durableConfig": {
462+
"RetentionPeriodInDays": 7,
463+
"ExecutionTimeout": 300
464+
},
465+
"path": "./src/parallel/parallel_with_should_complete.py"
466+
},
445467
{
446468
"name": "Map with Custom SerDes",
447469
"description": "Map operation with custom item-level serialization",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Example demonstrating map with a custom should_complete predicate.
2+
3+
The predicate gives full control over when a batch completes early,
4+
beyond the threshold-based fields (min_successful, tolerated_failure_count,
5+
tolerated_failure_percentage). Here we stop processing as soon as we
6+
accumulate 3 successful results, regardless of how many items remain.
7+
"""
8+
9+
from typing import Any
10+
11+
from aws_durable_execution_sdk_python.config import (
12+
CompletionConfig,
13+
CompletionDecision,
14+
CompletionStatus,
15+
MapConfig,
16+
complete_batch,
17+
continue_batch,
18+
)
19+
from aws_durable_execution_sdk_python.context import DurableContext
20+
from aws_durable_execution_sdk_python.execution import durable_execution
21+
22+
23+
def _should_complete(status: CompletionStatus) -> CompletionDecision:
24+
"""Complete once 3 items have succeeded."""
25+
return complete_batch() if status.success_count >= 3 else continue_batch()
26+
27+
28+
@durable_execution
29+
def handler(_event: Any, context: DurableContext) -> dict[str, Any]:
30+
"""Process items with a custom completion predicate."""
31+
items: list[int] = list(range(1, 11)) # [1, 2, ..., 10]
32+
33+
config: MapConfig = MapConfig(
34+
max_concurrency=2,
35+
completion_config=CompletionConfig(should_complete=_should_complete),
36+
)
37+
38+
results = context.map(
39+
inputs=items,
40+
func=lambda ctx, item, index, _: ctx.step(
41+
lambda _: item * 10, name=f"process-{index}"
42+
),
43+
name="map_should_complete",
44+
config=config,
45+
)
46+
47+
return {
48+
"success_count": results.success_count,
49+
"failure_count": results.failure_count,
50+
"started_count": results.started_count,
51+
"completion_reason": results.completion_reason.value,
52+
"results": results.get_results(),
53+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Example demonstrating parallel with a custom should_complete predicate.
2+
3+
Uses an index-based quorum rule: the batch completes when branch A (index 0)
4+
succeeds OR both branches B (index 1) and C (index 2) succeed. This shows
5+
how the items snapshot enables dependency-style completion logic.
6+
"""
7+
8+
from typing import Any
9+
10+
from aws_durable_execution_sdk_python.config import (
11+
CompletionConfig,
12+
CompletionDecision,
13+
CompletionStatus,
14+
ParallelConfig,
15+
complete_batch,
16+
continue_batch,
17+
)
18+
from aws_durable_execution_sdk_python.context import DurableContext
19+
from aws_durable_execution_sdk_python.execution import durable_execution
20+
21+
22+
def _quorum_predicate(status: CompletionStatus) -> CompletionDecision:
23+
"""Complete when branch A succeeds OR both B and C succeed."""
24+
if not status.items:
25+
return continue_batch()
26+
branch_a_ok: bool = status.items[0].is_succeeded
27+
branch_b_ok: bool = len(status.items) > 1 and status.items[1].is_succeeded
28+
branch_c_ok: bool = len(status.items) > 2 and status.items[2].is_succeeded
29+
return (
30+
complete_batch()
31+
if branch_a_ok or (branch_b_ok and branch_c_ok)
32+
else continue_batch()
33+
)
34+
35+
36+
@durable_execution
37+
def handler(_event: Any, context: DurableContext) -> dict[str, Any]:
38+
"""Run parallel branches with a quorum-based completion predicate."""
39+
config: ParallelConfig = ParallelConfig(
40+
max_concurrency=3,
41+
completion_config=CompletionConfig(should_complete=_quorum_predicate),
42+
)
43+
44+
functions = [
45+
# Branch A - slow task
46+
lambda ctx: ctx.step(lambda _: "Branch A done", name="branch-a"),
47+
# Branch B - fast task
48+
lambda ctx: ctx.step(lambda _: "Branch B done", name="branch-b"),
49+
# Branch C - fast task
50+
lambda ctx: ctx.step(lambda _: "Branch C done", name="branch-c"),
51+
# Branch D - never needed if quorum met
52+
lambda ctx: ctx.step(lambda _: "Branch D done", name="branch-d"),
53+
]
54+
55+
results = context.parallel(
56+
functions=functions,
57+
name="quorum-branches",
58+
config=config,
59+
)
60+
61+
return {
62+
"success_count": results.success_count,
63+
"completion_reason": results.completion_reason.value,
64+
"results": results.get_results(),
65+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Tests for map with should_complete predicate."""
2+
3+
import pytest
4+
from aws_durable_execution_sdk_python.execution import InvocationStatus
5+
from aws_durable_execution_sdk_python.lambda_service import OperationStatus
6+
from src.map import map_with_should_complete
7+
from test.conftest import deserialize_operation_payload
8+
9+
10+
@pytest.mark.example
11+
@pytest.mark.durable_execution(
12+
handler=map_with_should_complete.handler,
13+
lambda_function_name="Map with Should Complete",
14+
)
15+
def test_map_with_should_complete(durable_runner):
16+
"""Test map with custom should_complete predicate that stops at 3 successes."""
17+
with durable_runner:
18+
result = durable_runner.run(input="test", timeout=10)
19+
20+
assert result.status is InvocationStatus.SUCCEEDED
21+
22+
result_data = deserialize_operation_payload(result.result)
23+
24+
# Predicate completes after 3 successes; with max_concurrency=2,
25+
# a concurrent sibling may finish before the check fires, so up to
26+
# max_concurrency extra items can complete.
27+
assert result_data["success_count"] >= 3
28+
assert result_data["success_count"] <= 4 # at most 1 extra from concurrency
29+
assert result_data["failure_count"] == 0
30+
assert result_data["completion_reason"] == "CUSTOM_COMPLETION_SUCCEEDED"
31+
32+
# Results are item * 10 for the items processed
33+
assert len(result_data["results"]) == result_data["success_count"]
34+
35+
# Get the map operation
36+
map_op = result.get_context("map_should_complete")
37+
assert map_op is not None
38+
assert map_op.status is OperationStatus.SUCCEEDED
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Tests for parallel with should_complete quorum predicate."""
2+
3+
import pytest
4+
from aws_durable_execution_sdk_python.execution import InvocationStatus
5+
from aws_durable_execution_sdk_python.lambda_service import OperationStatus
6+
from src.parallel import parallel_with_should_complete
7+
from test.conftest import deserialize_operation_payload
8+
9+
10+
@pytest.mark.example
11+
@pytest.mark.durable_execution(
12+
handler=parallel_with_should_complete.handler,
13+
lambda_function_name="Parallel with Should Complete",
14+
)
15+
def test_parallel_with_should_complete(durable_runner):
16+
"""Test parallel with quorum predicate: branch A OR (B AND C)."""
17+
with durable_runner:
18+
result = durable_runner.run(input="test", timeout=10)
19+
20+
assert result.status is InvocationStatus.SUCCEEDED
21+
22+
result_data = deserialize_operation_payload(result.result)
23+
24+
# Quorum met: either branch A succeeded (1 success), or B and C both
25+
# succeeded (2 successes). With concurrency a raced sibling may also
26+
# complete before the batch stops.
27+
assert result_data["success_count"] >= 1
28+
assert result_data["completion_reason"] == "CUSTOM_COMPLETION_SUCCEEDED"
29+
assert len(result_data["results"]) >= 1
30+
31+
# Get the parallel operation
32+
parallel_op = result.get_context("quorum-branches")
33+
assert parallel_op is not None
34+
assert parallel_op.status is OperationStatus.SUCCEEDED

packages/aws-durable-execution-sdk-python/src/aws_durable_execution_sdk_python/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
# Helper decorators - commonly used for step functions
88
# Concurrency
99
from aws_durable_execution_sdk_python.concurrency.models import BatchResult
10-
from aws_durable_execution_sdk_python.config import ParallelBranch
10+
from aws_durable_execution_sdk_python.config import (
11+
BatchItemStatus,
12+
CompletionDecision,
13+
CompletionItemStatus,
14+
CompletionOutcome,
15+
CompletionStatus,
16+
ParallelBranch,
17+
complete_batch,
18+
continue_batch,
19+
)
1120
from aws_durable_execution_sdk_python.context import (
1221
DurableContext,
1322
durable_parallel_branch,
@@ -45,12 +54,17 @@
4554

4655

4756
__all__ = [
57+
"BatchItemStatus",
4858
"BatchResult",
4959
"CallbackError",
5060
"CallbackExternalError",
5161
"CallbackSubmitterError",
5262
"CallbackTimeoutError",
5363
"ChildContextError",
64+
"CompletionDecision",
65+
"CompletionItemStatus",
66+
"CompletionOutcome",
67+
"CompletionStatus",
5468
"DurableContext",
5569
"DurableExecutionsError",
5670
"DurableOperationError",
@@ -67,6 +81,8 @@
6781
"WaitForConditionError",
6882
"WithRetryConfig",
6983
"__version__",
84+
"complete_batch",
85+
"continue_batch",
7086
"durable_execution",
7187
"durable_parallel_branch",
7288
"durable_step",

0 commit comments

Comments
 (0)