|
9 | 9 | import threading |
10 | 10 | import time |
11 | 11 | import uuid |
| 12 | +from concurrent.futures import ThreadPoolExecutor |
12 | 13 | from concurrent.futures.process import BrokenProcessPool |
13 | 14 | from contextvars import ContextVar |
14 | 15 | from dataclasses import dataclass |
15 | 16 | from datetime import datetime, timedelta, timezone |
| 17 | +from time import sleep |
16 | 18 | from typing import Any, Callable, List, NoReturn, Optional, Sequence, Type |
17 | 19 |
|
| 20 | +import temporalio.api.workflowservice.v1 |
18 | 21 | from temporalio import activity, workflow |
19 | 22 | from temporalio.client import ( |
20 | 23 | AsyncActivityHandle, |
@@ -1486,3 +1489,105 @@ async def h(): |
1486 | 1489 | client, worker, heartbeat, retry_max_attempts=2 |
1487 | 1490 | ) |
1488 | 1491 | assert result.result == "details: Some detail" |
| 1492 | + |
| 1493 | + |
| 1494 | +async def test_activity_reset_catch( |
| 1495 | + client: Client, worker: ExternalWorker, env: WorkflowEnvironment |
| 1496 | +): |
| 1497 | + if env.supports_time_skipping: |
| 1498 | + pytest.skip("Time skipping server doesn't support activity reset") |
| 1499 | + |
| 1500 | + @activity.defn |
| 1501 | + async def wait_cancel() -> str: |
| 1502 | + req = temporalio.api.workflowservice.v1.ResetActivityRequest( |
| 1503 | + namespace=client.namespace, |
| 1504 | + execution=temporalio.api.common.v1.WorkflowExecution( |
| 1505 | + workflow_id=activity.info().workflow_id, |
| 1506 | + run_id=activity.info().workflow_run_id, |
| 1507 | + ), |
| 1508 | + id=activity.info().activity_id, |
| 1509 | + ) |
| 1510 | + await client.workflow_service.reset_activity(req) |
| 1511 | + try: |
| 1512 | + while True: |
| 1513 | + await asyncio.sleep(0.3) |
| 1514 | + activity.heartbeat() |
| 1515 | + except asyncio.CancelledError: |
| 1516 | + details = activity.cancellation_details() |
| 1517 | + assert details is not None |
| 1518 | + return "Got cancelled error, reset? " + str(details.reset) |
| 1519 | + |
| 1520 | + @activity.defn |
| 1521 | + def sync_wait_cancel() -> str: |
| 1522 | + req = temporalio.api.workflowservice.v1.ResetActivityRequest( |
| 1523 | + namespace=client.namespace, |
| 1524 | + execution=temporalio.api.common.v1.WorkflowExecution( |
| 1525 | + workflow_id=activity.info().workflow_id, |
| 1526 | + run_id=activity.info().workflow_run_id, |
| 1527 | + ), |
| 1528 | + id=activity.info().activity_id, |
| 1529 | + ) |
| 1530 | + asyncio.run(client.workflow_service.reset_activity(req)) |
| 1531 | + try: |
| 1532 | + while True: |
| 1533 | + sleep(0.3) |
| 1534 | + activity.heartbeat() |
| 1535 | + except temporalio.exceptions.CancelledError: |
| 1536 | + details = activity.cancellation_details() |
| 1537 | + assert details is not None |
| 1538 | + return "Got cancelled error, reset? " + str(details.reset) |
| 1539 | + except Exception as e: |
| 1540 | + return str(type(e)) + str(e) |
| 1541 | + |
| 1542 | + result = await _execute_workflow_with_activity( |
| 1543 | + client, |
| 1544 | + worker, |
| 1545 | + wait_cancel, |
| 1546 | + ) |
| 1547 | + assert result.result == "Got cancelled error, reset? True" |
| 1548 | + |
| 1549 | + config = WorkerConfig( |
| 1550 | + activity_executor=ThreadPoolExecutor(max_workers=1), |
| 1551 | + ) |
| 1552 | + result = await _execute_workflow_with_activity( |
| 1553 | + client, |
| 1554 | + worker, |
| 1555 | + sync_wait_cancel, |
| 1556 | + worker_config=config, |
| 1557 | + ) |
| 1558 | + assert result.result == "Got cancelled error, reset? True" |
| 1559 | + |
| 1560 | + |
| 1561 | +async def test_activity_reset_history( |
| 1562 | + client: Client, worker: ExternalWorker, env: WorkflowEnvironment |
| 1563 | +): |
| 1564 | + if env.supports_time_skipping: |
| 1565 | + pytest.skip("Time skipping server doesn't support activity reset") |
| 1566 | + |
| 1567 | + @activity.defn |
| 1568 | + async def wait_cancel() -> str: |
| 1569 | + req = temporalio.api.workflowservice.v1.ResetActivityRequest( |
| 1570 | + namespace=client.namespace, |
| 1571 | + execution=temporalio.api.common.v1.WorkflowExecution( |
| 1572 | + workflow_id=activity.info().workflow_id, |
| 1573 | + run_id=activity.info().workflow_run_id, |
| 1574 | + ), |
| 1575 | + id=activity.info().activity_id, |
| 1576 | + ) |
| 1577 | + await client.workflow_service.reset_activity(req) |
| 1578 | + while True: |
| 1579 | + await asyncio.sleep(0.3) |
| 1580 | + activity.heartbeat() |
| 1581 | + |
| 1582 | + with pytest.raises(WorkflowFailureError) as e: |
| 1583 | + result = await _execute_workflow_with_activity( |
| 1584 | + client, |
| 1585 | + worker, |
| 1586 | + wait_cancel, |
| 1587 | + ) |
| 1588 | + assert isinstance(e.value.cause, ActivityError) |
| 1589 | + assert isinstance(e.value.cause.cause, ApplicationError) |
| 1590 | + assert ( |
| 1591 | + e.value.cause.cause.message |
| 1592 | + == "Unhandled activity cancel error produced by activity reset" |
| 1593 | + ) |
0 commit comments