From f8405b6c6b03b29b5d4f7a8e59e6d9f9a945df6d Mon Sep 17 00:00:00 2001 From: vikramlc Date: Wed, 28 Oct 2026 10:14:50 +0530 Subject: [PATCH 1/4] feat(unstable): Raise the available actions limit to cover two actions per task Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +++++ cognite/extractorutils/unstable/core/_dto.py | 4 +++- .../test_unstable/test_action_registration.py | 20 ++++++++++++++++++- tests/test_unstable/test_dto.py | 14 +++++++++---- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b19edb1a..de79e390 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 + +### Changed +* In the `unstable` package: The maximum number of available actions sent at startup is raised from 100 to 250, so an extractor with up to 100 actionable tasks (a start and a stop action each) plus custom actions can register. Requires an Odin version with the same limit (EDG-1049). + ## 7.13.2 ### Fixed diff --git a/cognite/extractorutils/unstable/core/_dto.py b/cognite/extractorutils/unstable/core/_dto.py index 6ee183c1..1420c010 100644 --- a/cognite/extractorutils/unstable/core/_dto.py +++ b/cognite/extractorutils/unstable/core/_dto.py @@ -195,7 +195,9 @@ def validate_status(cls, v: ActionStatus) -> ActionStatus: return v -AvailableActionList = Annotated[list[AvailableActionWrite], Len(min_length=0, max_length=100)] +# Each actionable task registers a start and a stop action; keep in sync with the Odin limit. +MAX_AVAILABLE_ACTIONS = 250 +AvailableActionList = Annotated[list[AvailableActionWrite], Len(min_length=0, max_length=MAX_AVAILABLE_ACTIONS)] 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..9b28de1c 100644 --- a/tests/test_unstable/test_action_registration.py +++ b/tests/test_unstable/test_action_registration.py @@ -4,8 +4,9 @@ from unittest.mock import MagicMock import pytest +from pydantic import ValidationError -from cognite.extractorutils.unstable.core._dto import ActionType, StartupRequest +from cognite.extractorutils.unstable.core._dto import MAX_AVAILABLE_ACTIONS, ActionType, StartupRequest from cognite.extractorutils.unstable.core.actions import CustomAction from cognite.extractorutils.unstable.core.base import Extractor, FullConfig from cognite.extractorutils.unstable.core.tasks import ContinuousTask, ScheduledTask, StartupTask, TaskContext @@ -251,3 +252,20 @@ def target(_: TaskContext) -> None: time.sleep(0.005) assert extractor._running_task_tokens.get("the-task") is replacement + + +def test_startup_request_accepts_two_actions_per_task_at_odin_task_limit() -> None: + extractor = _make_extractor() + for i in range(100): + 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 + assert len(actions) >= 200 + + +def test_startup_request_rejects_more_than_max_available_actions() -> None: + extractor = _make_extractor() + for i in range(MAX_AVAILABLE_ACTIONS): + extractor.add_task(ScheduledTask.from_interval(interval="1h", name=f"task-{i}", target=lambda _: None)) + with pytest.raises(ValidationError): + _startup_request(extractor) diff --git a/tests/test_unstable/test_dto.py b/tests/test_unstable/test_dto.py index 1206ac9f..64cc23db 100644 --- a/tests/test_unstable/test_dto.py +++ b/tests/test_unstable/test_dto.py @@ -2,6 +2,7 @@ from pydantic import ValidationError from cognite.extractorutils.unstable.core._dto import ( + MAX_AVAILABLE_ACTIONS, MAX_MESSAGE_LENGTH, MAX_METADATA_VALUE_BYTES, Action, @@ -99,11 +100,13 @@ def test_startup_request_available_actions_none_excluded() -> None: def test_startup_request_available_actions_max_length_enforced() -> None: - with pytest.raises(ValidationError, match="at most 100"): + with pytest.raises(ValidationError, match=f"at most {MAX_AVAILABLE_ACTIONS}"): StartupRequest( external_id="x", extractor=ExtractorInfo(external_id="x"), - available_actions=[AvailableActionWrite(name=f"a{i}", type=ActionType.custom) for i in range(101)], + available_actions=[ + AvailableActionWrite(name=f"a{i}", type=ActionType.custom) for i in range(MAX_AVAILABLE_ACTIONS + 1) + ], ) @@ -157,10 +160,13 @@ def test_checkin_request_action_updates_none_excluded() -> None: def test_checkin_request_action_updates_max_length_enforced() -> None: - with pytest.raises(ValidationError, match="at most 100"): + with pytest.raises(ValidationError, match=f"at most {MAX_AVAILABLE_ACTIONS}"): CheckinRequest( external_id="x", - action_updates=[ActionUpdate(external_id=f"act-{i}", status=ActionStatus.running) for i in range(101)], + action_updates=[ + ActionUpdate(external_id=f"act-{i}", status=ActionStatus.running) + for i in range(MAX_AVAILABLE_ACTIONS + 1) + ], ) From 7403df3ebd737ff5061a5ca2016a1f74855ecad6 Mon Sep 17 00:00:00 2001 From: vikramlc Date: Wed, 28 Oct 2026 10:15:36 +0530 Subject: [PATCH 2/4] test(unstable): Keep the action updates cap test at 100 Co-Authored-By: Claude Sonnet 5.5 --- tests/test_unstable/test_dto.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_unstable/test_dto.py b/tests/test_unstable/test_dto.py index 64cc23db..390e909a 100644 --- a/tests/test_unstable/test_dto.py +++ b/tests/test_unstable/test_dto.py @@ -160,13 +160,10 @@ def test_checkin_request_action_updates_none_excluded() -> None: def test_checkin_request_action_updates_max_length_enforced() -> None: - with pytest.raises(ValidationError, match=f"at most {MAX_AVAILABLE_ACTIONS}"): + with pytest.raises(ValidationError, match="at most 100"): CheckinRequest( external_id="x", - action_updates=[ - ActionUpdate(external_id=f"act-{i}", status=ActionStatus.running) - for i in range(MAX_AVAILABLE_ACTIONS + 1) - ], + action_updates=[ActionUpdate(external_id=f"act-{i}", status=ActionStatus.running) for i in range(101)], ) From fdc6c6ac7796f50ddff5ecf4cf1297363672a52d Mon Sep 17 00:00:00 2001 From: vikramlc Date: Wed, 28 Oct 2026 10:15:36 +0530 Subject: [PATCH 3/4] feat(unstable): Let Integrations API own the available actions limit Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- cognite/extractorutils/unstable/core/_dto.py | 5 ++--- .../test_unstable/test_action_registration.py | 17 ++++------------- tests/test_unstable/test_dto.py | 18 ++++++++---------- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de79e390..007deb87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Changes are grouped as follows ## Unreleased ### Changed -* In the `unstable` package: The maximum number of available actions sent at startup is raised from 100 to 250, so an extractor with up to 100 actionable tasks (a start and a stop action each) plus custom actions can register. Requires an Odin version with the same limit (EDG-1049). +* In the `unstable` package: Removed the client-side limit of 100 available actions in the startup request. Integrations API now decides the maximum, so it can be raised without a new extractor-utils release; a request over the limit is rejected by the API and fails the extractor at startup with the API's error message (EDG-1049). ## 7.13.2 diff --git a/cognite/extractorutils/unstable/core/_dto.py b/cognite/extractorutils/unstable/core/_dto.py index 1420c010..ca934251 100644 --- a/cognite/extractorutils/unstable/core/_dto.py +++ b/cognite/extractorutils/unstable/core/_dto.py @@ -195,9 +195,8 @@ def validate_status(cls, v: ActionStatus) -> ActionStatus: return v -# Each actionable task registers a start and a stop action; keep in sync with the Odin limit. -MAX_AVAILABLE_ACTIONS = 250 -AvailableActionList = Annotated[list[AvailableActionWrite], Len(min_length=0, max_length=MAX_AVAILABLE_ACTIONS)] +# 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 9b28de1c..fe4750ad 100644 --- a/tests/test_unstable/test_action_registration.py +++ b/tests/test_unstable/test_action_registration.py @@ -4,9 +4,8 @@ from unittest.mock import MagicMock import pytest -from pydantic import ValidationError -from cognite.extractorutils.unstable.core._dto import MAX_AVAILABLE_ACTIONS, ActionType, StartupRequest +from cognite.extractorutils.unstable.core._dto import ActionType, StartupRequest from cognite.extractorutils.unstable.core.actions import CustomAction from cognite.extractorutils.unstable.core.base import Extractor, FullConfig from cognite.extractorutils.unstable.core.tasks import ContinuousTask, ScheduledTask, StartupTask, TaskContext @@ -254,18 +253,10 @@ def target(_: TaskContext) -> None: assert extractor._running_task_tokens.get("the-task") is replacement -def test_startup_request_accepts_two_actions_per_task_at_odin_task_limit() -> None: +def test_startup_request_has_no_client_side_limit_on_available_actions() -> None: extractor = _make_extractor() - for i in range(100): + 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 - assert len(actions) >= 200 - - -def test_startup_request_rejects_more_than_max_available_actions() -> None: - extractor = _make_extractor() - for i in range(MAX_AVAILABLE_ACTIONS): - extractor.add_task(ScheduledTask.from_interval(interval="1h", name=f"task-{i}", target=lambda _: None)) - with pytest.raises(ValidationError): - _startup_request(extractor) + assert len(actions) >= 600 diff --git a/tests/test_unstable/test_dto.py b/tests/test_unstable/test_dto.py index 390e909a..e49dfa91 100644 --- a/tests/test_unstable/test_dto.py +++ b/tests/test_unstable/test_dto.py @@ -2,7 +2,6 @@ from pydantic import ValidationError from cognite.extractorutils.unstable.core._dto import ( - MAX_AVAILABLE_ACTIONS, MAX_MESSAGE_LENGTH, MAX_METADATA_VALUE_BYTES, Action, @@ -99,15 +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=f"at most {MAX_AVAILABLE_ACTIONS}"): - StartupRequest( - external_id="x", - extractor=ExtractorInfo(external_id="x"), - available_actions=[ - AvailableActionWrite(name=f"a{i}", type=ActionType.custom) for i in range(MAX_AVAILABLE_ACTIONS + 1) - ], - ) +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: From ddb0057e8613683def3b67de6b681798fe8998d5 Mon Sep 17 00:00:00 2001 From: vikramlc Date: Wed, 28 Oct 2026 20:25:31 +0530 Subject: [PATCH 4/4] docs(unstable): Address review comments on the available actions limit Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++-- tests/test_unstable/test_action_registration.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 007deb87..8eea5351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ Changes are grouped as follows ## Unreleased -### Changed -* In the `unstable` package: Removed the client-side limit of 100 available actions in the startup request. Integrations API now decides the maximum, so it can be raised without a new extractor-utils release; a request over the limit is rejected by the API and fails the extractor at startup with the API's error message (EDG-1049). +### 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 diff --git a/tests/test_unstable/test_action_registration.py b/tests/test_unstable/test_action_registration.py index fe4750ad..3fd36b45 100644 --- a/tests/test_unstable/test_action_registration.py +++ b/tests/test_unstable/test_action_registration.py @@ -259,4 +259,5 @@ def test_startup_request_has_no_client_side_limit_on_available_actions() -> None 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 - assert len(actions) >= 600 + # 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)