diff --git a/CHANGELOG.md b/CHANGELOG.md index b19edb1a..8eea5351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Changes are grouped as follows - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## Unreleased + +### Fixed +* In the `unstable` package: Fixed extractors with more than 49 actionable tasks (more than 100 available actions) being unable to start: the startup request was rejected locally by a hard-coded limit of 100 available actions. The client-side limit is removed and Integrations API decides the maximum, so it can be raised without a new extractor-utils release. A request over the API's limit is rejected with a 400 whose message names the limit; the message is logged at critical level, and with `RETRY_STARTUP` (the default) the startup is retried until the limit is raised or the extractor is stopped (EDG-1049). + ## 7.13.2 ### Fixed diff --git a/cognite/extractorutils/unstable/core/_dto.py b/cognite/extractorutils/unstable/core/_dto.py index 6ee183c1..ca934251 100644 --- a/cognite/extractorutils/unstable/core/_dto.py +++ b/cognite/extractorutils/unstable/core/_dto.py @@ -195,7 +195,8 @@ def validate_status(cls, v: ActionStatus) -> ActionStatus: return v -AvailableActionList = Annotated[list[AvailableActionWrite], Len(min_length=0, max_length=100)] +# No client-side limit: Integrations API owns the maximum number of available actions and rejects a larger request. +AvailableActionList = list[AvailableActionWrite] ActionUpdateList = Annotated[list[ActionUpdate], Len(min_length=0, max_length=100)] diff --git a/tests/test_unstable/test_action_registration.py b/tests/test_unstable/test_action_registration.py index 19f53dd0..3fd36b45 100644 --- a/tests/test_unstable/test_action_registration.py +++ b/tests/test_unstable/test_action_registration.py @@ -251,3 +251,13 @@ def target(_: TaskContext) -> None: time.sleep(0.005) assert extractor._running_task_tokens.get("the-task") is replacement + + +def test_startup_request_has_no_client_side_limit_on_available_actions() -> None: + extractor = _make_extractor() + for i in range(300): + extractor.add_task(ScheduledTask.from_interval(interval="1h", name=f"task-{i}", target=lambda _: None)) + actions = _startup_request(extractor).available_actions + assert actions is not None + # A start and a stop action per task, plus the built-in and user-registered custom actions + assert len(actions) == 2 * 300 + len(extractor._custom_actions) diff --git a/tests/test_unstable/test_dto.py b/tests/test_unstable/test_dto.py index 1206ac9f..e49dfa91 100644 --- a/tests/test_unstable/test_dto.py +++ b/tests/test_unstable/test_dto.py @@ -98,13 +98,14 @@ def test_startup_request_available_actions_none_excluded() -> None: assert "availableActions" not in body -def test_startup_request_available_actions_max_length_enforced() -> None: - with pytest.raises(ValidationError, match="at most 100"): - StartupRequest( - external_id="x", - extractor=ExtractorInfo(external_id="x"), - available_actions=[AvailableActionWrite(name=f"a{i}", type=ActionType.custom) for i in range(101)], - ) +def test_startup_request_available_actions_have_no_client_side_limit() -> None: + request = StartupRequest( + external_id="x", + extractor=ExtractorInfo(external_id="x"), + available_actions=[AvailableActionWrite(name=f"a{i}", type=ActionType.custom) for i in range(500)], + ) + assert request.available_actions is not None + assert len(request.available_actions) == 500 def test_action_camel_case_serialization() -> None: