Repository navigation
fix: send the graceful abort flag in the request body #1197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,14 @@ | |
| from apify_client.types import Timeout | ||
|
|
||
|
|
||
| def _build_abort_body(*, gracefully: bool | None) -> dict[str, bool]: | ||
| """Build the JSON body of the abort request, leaving out the options the caller did not set.""" | ||
| body: dict[str, bool] = {} | ||
| if gracefully is not None: | ||
| body['gracefully'] = gracefully | ||
| return body | ||
|
|
||
|
|
||
| @docs_group('Resource clients') | ||
| class RunClient(ResourceClient): | ||
| """Sub-client for managing a specific Actor run. | ||
|
|
@@ -131,7 +139,8 @@ def abort(self, *, gracefully: bool | None = None, timeout: Timeout = 'medium') | |
| response = self._http_client.call( | ||
| url=self._build_url('abort'), | ||
| method='POST', | ||
| params=self._build_params(gracefully=gracefully), | ||
| headers={'content-type': 'application/json'}, | ||
| data=json.dumps(_build_abort_body(gracefully=gracefully)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: the abort handler reads
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: the abort endpoint only reads |
||
| timeout=timeout, | ||
| ) | ||
| result = response_to_dict(response) | ||
|
|
@@ -678,7 +687,8 @@ async def abort(self, *, gracefully: bool | None = None, timeout: Timeout = 'med | |
| response = await self._http_client.call( | ||
| url=self._build_url('abort'), | ||
| method='POST', | ||
| params=self._build_params(gracefully=gracefully), | ||
| headers={'content-type': 'application/json'}, | ||
| data=json.dumps(_build_abort_body(gracefully=gracefully)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: the async client has the same problem. The API never reads
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: the async client has the same regression. The API never reads |
||
| timeout=timeout, | ||
| ) | ||
| result = response_to_dict(response) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| import pytest | ||
| from werkzeug import Request, Response | ||
|
|
||
| from apify_client import ApifyClient, ApifyClientAsync | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pytest_httpserver import HTTPServer | ||
|
|
||
| pytestmark = pytest.mark.usefixtures('http_client_classes') | ||
|
|
||
| _MOCKED_RUN_ID = 'test_run_id' | ||
| _ABORT_PATH = f'/v2/actor-runs/{_MOCKED_RUN_ID}/abort' | ||
| _RUN_RESPONSE = { | ||
| 'data': { | ||
| 'id': _MOCKED_RUN_ID, | ||
| 'actId': 'test_actor_id', | ||
| 'userId': 'test_user_id', | ||
| 'startedAt': '2024-01-01T00:00:00.000Z', | ||
| 'status': 'ABORTING', | ||
| 'meta': {'origin': 'API'}, | ||
| 'stats': {}, | ||
| 'options': {'build': 'latest', 'timeoutSecs': 3600, 'memoryMbytes': 1024, 'diskMbytes': 2048}, | ||
| 'buildId': 'test_build_id', | ||
| 'defaultKeyValueStoreId': 'test_kvs_id', | ||
| 'defaultDatasetId': 'test_dataset_id', | ||
| 'defaultRequestQueueId': 'test_rq_id', | ||
| 'containerUrl': 'https://example.com', | ||
| } | ||
| } | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ('gracefully', 'expected_body'), | ||
| [ | ||
| pytest.param(None, {}, id='default'), | ||
| pytest.param(True, {'gracefully': True}, id='graceful'), | ||
| pytest.param(False, {'gracefully': False}, id='forced'), | ||
| ], | ||
| ) | ||
| def test_run_abort_sends_gracefully_in_body_sync( | ||
| httpserver: HTTPServer, | ||
| *, | ||
| gracefully: bool | None, | ||
| expected_body: dict, | ||
| ) -> None: | ||
| """The graceful flag travels in the JSON body and is left out when the caller did not set it.""" | ||
| captured_requests: list[Request] = [] | ||
|
|
||
| def capture_request(request: Request) -> Response: | ||
| captured_requests.append(request) | ||
| return Response(status=200, response=json.dumps(_RUN_RESPONSE), mimetype='application/json') | ||
|
|
||
| httpserver.expect_request(_ABORT_PATH, method='POST').respond_with_handler(capture_request) | ||
|
|
||
| api_url = httpserver.url_for('/').removesuffix('/') | ||
| client = ApifyClient(token='test_token', api_url=api_url) | ||
|
|
||
| run = client.run(_MOCKED_RUN_ID).abort(gracefully=gracefully) | ||
|
|
||
| assert run.id == _MOCKED_RUN_ID | ||
| assert len(captured_requests) == 1 | ||
| assert 'gracefully' not in captured_requests[0].args | ||
| assert json.loads(captured_requests[0].get_data()) == expected_body | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ('gracefully', 'expected_body'), | ||
| [ | ||
| pytest.param(None, {}, id='default'), | ||
| pytest.param(True, {'gracefully': True}, id='graceful'), | ||
| pytest.param(False, {'gracefully': False}, id='forced'), | ||
| ], | ||
| ) | ||
| async def test_run_abort_sends_gracefully_in_body_async( | ||
| httpserver: HTTPServer, | ||
| *, | ||
| gracefully: bool | None, | ||
| expected_body: dict, | ||
| ) -> None: | ||
| """Async variant of `test_run_abort_sends_gracefully_in_body_sync`.""" | ||
| captured_requests: list[Request] = [] | ||
|
|
||
| def capture_request(request: Request) -> Response: | ||
| captured_requests.append(request) | ||
| return Response(status=200, response=json.dumps(_RUN_RESPONSE), mimetype='application/json') | ||
|
|
||
| httpserver.expect_request(_ABORT_PATH, method='POST').respond_with_handler(capture_request) | ||
|
|
||
| api_url = httpserver.url_for('/').removesuffix('/') | ||
| client = ApifyClientAsync(token='test_token', api_url=api_url) | ||
|
|
||
| run = await client.run(_MOCKED_RUN_ID).abort(gracefully=gracefully) | ||
|
|
||
| assert run.id == _MOCKED_RUN_ID | ||
| assert len(captured_requests) == 1 | ||
| assert 'gracefully' not in captured_requests[0].args | ||
| assert json.loads(captured_requests[0].get_data()) == expected_body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important: the abort handler reads
gracefullyonly from the query string, so moving it into the JSON body means the API silently ignores it andabort(gracefully=True)always aborts immediately. (apify-core src/api/src/routes/actor_runs/run_abort.ts:28)