From 237ac2117076f3a8dc0486f5b9632f59f029d979 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 11 Sep 2025 18:39:27 -0400 Subject: [PATCH 1/5] Investigation: minimal repro --- pyproject.toml | 2 +- tests/nexus/test_timeout.py | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/nexus/test_timeout.py diff --git a/pyproject.toml b/pyproject.toml index 8f4044e2b..9597be2f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ lint-types = [ { cmd = "uv run mypy --namespace-packages --check-untyped-defs ."}, ] run-bench = "uv run python scripts/run_bench.py" -test = "uv run pytest" +test = "uv run pytest tests/nexus/test_timeout.py" [tool.pytest.ini_options] diff --git a/tests/nexus/test_timeout.py b/tests/nexus/test_timeout.py new file mode 100644 index 000000000..4b95240f4 --- /dev/null +++ b/tests/nexus/test_timeout.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import asyncio +import uuid +from datetime import timedelta + +import nexusrpc.handler +import pytest + +from temporalio import workflow +from temporalio.testing import WorkflowEnvironment +from tests.helpers import new_worker +from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name + + +@workflow.defn +class NexusCallerWorkflow: + """Workflow that calls a Nexus operation.""" + + @workflow.run + async def run(self) -> None: + nexus_client = workflow.create_nexus_client( + endpoint=make_nexus_endpoint_name(workflow.info().task_queue), + service="MaxConcurrentTestService", + ) + + await nexus_client.execute_operation( + "op", None, schedule_to_close_timeout=timedelta(seconds=60) + ) + + +async def test_nexus_timeout(env: WorkflowEnvironment): + if env.supports_time_skipping: + pytest.skip("Nexus tests don't work with Javas test server") + + @nexusrpc.handler.service_handler + class MaxConcurrentTestService: + @nexusrpc.handler.sync_operation + async def op( + self, _ctx: nexusrpc.handler.StartOperationContext, id: None + ) -> None: + await asyncio.Event().wait() + + async with new_worker( + env.client, + NexusCallerWorkflow, + nexus_service_handlers=[MaxConcurrentTestService()], + ) as worker: + await create_nexus_endpoint(worker.task_queue, env.client) + + execute_workflow = env.client.execute_workflow( + NexusCallerWorkflow.run, + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + try: + await asyncio.wait_for(execute_workflow, timeout=3) + except TimeoutError: + print("Saw expected timeout") + pass + else: + pytest.fail("Expected timeout") From 52bcf2a74e9acdf82f3628aa01e562639956b685 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 11 Sep 2025 18:49:36 -0400 Subject: [PATCH 2/5] Cleanup --- tests/nexus/test_timeout.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/nexus/test_timeout.py b/tests/nexus/test_timeout.py index 4b95240f4..b436172f1 100644 --- a/tests/nexus/test_timeout.py +++ b/tests/nexus/test_timeout.py @@ -13,19 +13,27 @@ from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +@nexusrpc.handler.service_handler +class NexusService: + @nexusrpc.handler.sync_operation + async def op_that_never_returns( + self, _ctx: nexusrpc.handler.StartOperationContext, id: None + ) -> None: + await asyncio.Event().wait() + + @workflow.defn class NexusCallerWorkflow: - """Workflow that calls a Nexus operation.""" - @workflow.run async def run(self) -> None: nexus_client = workflow.create_nexus_client( endpoint=make_nexus_endpoint_name(workflow.info().task_queue), - service="MaxConcurrentTestService", + service=NexusService, ) - await nexus_client.execute_operation( - "op", None, schedule_to_close_timeout=timedelta(seconds=60) + NexusService.op_that_never_returns, + None, + schedule_to_close_timeout=timedelta(seconds=10), ) @@ -33,18 +41,10 @@ async def test_nexus_timeout(env: WorkflowEnvironment): if env.supports_time_skipping: pytest.skip("Nexus tests don't work with Javas test server") - @nexusrpc.handler.service_handler - class MaxConcurrentTestService: - @nexusrpc.handler.sync_operation - async def op( - self, _ctx: nexusrpc.handler.StartOperationContext, id: None - ) -> None: - await asyncio.Event().wait() - async with new_worker( env.client, NexusCallerWorkflow, - nexus_service_handlers=[MaxConcurrentTestService()], + nexus_service_handlers=[NexusService()], ) as worker: await create_nexus_endpoint(worker.task_queue, env.client) From 9344069295405610f4a53f8b0370859db4c25e42 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 11 Sep 2025 18:56:04 -0400 Subject: [PATCH 3/5] Cleanup --- tests/nexus/test_timeout.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/nexus/test_timeout.py b/tests/nexus/test_timeout.py index b436172f1..995bdbd46 100644 --- a/tests/nexus/test_timeout.py +++ b/tests/nexus/test_timeout.py @@ -13,8 +13,13 @@ from tests.helpers.nexus import create_nexus_endpoint, make_nexus_endpoint_name +@nexusrpc.service +class Service: + op_that_never_returns: nexusrpc.Operation[None, None] + + @nexusrpc.handler.service_handler -class NexusService: +class ServiceHandler: @nexusrpc.handler.sync_operation async def op_that_never_returns( self, _ctx: nexusrpc.handler.StartOperationContext, id: None @@ -28,10 +33,10 @@ class NexusCallerWorkflow: async def run(self) -> None: nexus_client = workflow.create_nexus_client( endpoint=make_nexus_endpoint_name(workflow.info().task_queue), - service=NexusService, + service=ServiceHandler, ) await nexus_client.execute_operation( - NexusService.op_that_never_returns, + Service.op_that_never_returns, None, schedule_to_close_timeout=timedelta(seconds=10), ) @@ -44,7 +49,7 @@ async def test_nexus_timeout(env: WorkflowEnvironment): async with new_worker( env.client, NexusCallerWorkflow, - nexus_service_handlers=[NexusService()], + nexus_service_handlers=[ServiceHandler()], ) as worker: await create_nexus_endpoint(worker.task_queue, env.client) From 325de018bb8285e422ec57e83353a2537248923d Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 11 Sep 2025 19:05:20 -0400 Subject: [PATCH 4/5] Cleanup --- tests/nexus/test_timeout.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/nexus/test_timeout.py b/tests/nexus/test_timeout.py index 995bdbd46..74b093e9d 100644 --- a/tests/nexus/test_timeout.py +++ b/tests/nexus/test_timeout.py @@ -7,6 +7,7 @@ import nexusrpc.handler import pytest +import temporalio.exceptions from temporalio import workflow from temporalio.testing import WorkflowEnvironment from tests.helpers import new_worker @@ -38,13 +39,13 @@ async def run(self) -> None: await nexus_client.execute_operation( Service.op_that_never_returns, None, - schedule_to_close_timeout=timedelta(seconds=10), + schedule_to_close_timeout=timedelta(seconds=1), ) async def test_nexus_timeout(env: WorkflowEnvironment): if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with Javas test server") + pytest.skip("Nexus tests don't work with Java test server") async with new_worker( env.client, @@ -53,15 +54,11 @@ async def test_nexus_timeout(env: WorkflowEnvironment): ) as worker: await create_nexus_endpoint(worker.task_queue, env.client) - execute_workflow = env.client.execute_workflow( - NexusCallerWorkflow.run, - id=str(uuid.uuid4()), - task_queue=worker.task_queue, - ) try: - await asyncio.wait_for(execute_workflow, timeout=3) - except TimeoutError: - print("Saw expected timeout") - pass - else: - pytest.fail("Expected timeout") + await env.client.execute_workflow( + NexusCallerWorkflow.run, + id=str(uuid.uuid4()), + task_queue=worker.task_queue, + ) + except Exception as err: + assert err.cause.cause.__class__ == temporalio.exceptions.TimeoutError # type: ignore From 03ddd52d30533dd60f82cad5e769ea65353a59b5 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Mon, 29 Sep 2025 05:17:44 -0400 Subject: [PATCH 5/5] Hack GitHub ci.yml: run multiple times --- .github/workflows/ci.yml | 59 ++++++++++------------------------------ 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6289dbcd0..158f9d3be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,20 @@ jobs: - run: poe lint - run: poe build-develop - run: mkdir junit-xml + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 + - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml + timeout-minutes: 15 - run: poe test ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml timeout-minutes: 15 # Time skipping doesn't yet support ARM @@ -103,43 +117,6 @@ jobs: path: junit-xml retention-days: 14 - # Confirm protos are already generated properly with older protobuf - # library and run test with that older version. We must downgrade protobuf - # so we can generate 3.x and 4.x compatible API. We have to use older - # Python to run this check because the grpcio-tools version we use - # is <= 3.10. - - name: Check generated protos and test protobuf 3.x - if: ${{ matrix.protoCheckTarget }} - env: - TEMPORAL_TEST_PROTO3: 1 - run: | - uv add --python 3.9 "protobuf<4" - uv sync --all-extras - poe build-develop - poe gen-protos - poe format - [[ -z $(git status --porcelain temporalio) ]] || (git diff temporalio; echo "Protos changed"; exit 1) - poe test -s - timeout-minutes: 10 - - # Do docs stuff (only on one host) - - name: Build API docs - if: ${{ matrix.docsTarget }} - run: poe gen-docs - - name: Deploy prod API docs - if: ${{ github.ref == 'refs/heads/main' && matrix.docsTarget }} - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - run: npx vercel deploy build/apidocs -t ${{ secrets.VERCEL_TOKEN }} --prod --yes - - # Confirm README ToC is generated properly - - uses: actions/setup-node@v4 - - name: Check generated README ToC - if: ${{ matrix.docsTarget }} - run: | - npx doctoc README.md - [[ -z $(git status --porcelain README.md) ]] || (git diff README.md; echo "README changed"; exit 1) test-latest-deps: timeout-minutes: 30 runs-on: ubuntu-latest @@ -175,11 +152,3 @@ jobs: name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--latest-deps--time-skipping path: junit-xml retention-days: 14 - - # Runs the sdk features repo tests with this repo's current SDK code - features-tests: - uses: temporalio/features/.github/workflows/python.yaml@main - with: - python-repo-path: ${{github.event.pull_request.head.repo.full_name}} - version: ${{github.event.pull_request.head.ref}} - version-is-repo-ref: true