From 71ad4861731e0f4fcd654598844ccc075979457d Mon Sep 17 00:00:00 2001 From: omby8888 <160610297+omby8888@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:29:29 +0300 Subject: [PATCH] [Framework] Handle ReadTimeout errors (#983) # Description What - Handle ReadTimeout Errors Why - Requests to port or 3party apps may take a while, on the other hand we want to do out best effort to complete the resync How - Increased the httpx client timeout and not raise exceptions during batch upsert entities ## Type of change Please leave one option from the following and delete the rest: - [X] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] New Integration (non-breaking change which adds a new integration) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Non-breaking change (fix of existing functionality that will not change current behavior) - [ ] Documentation (added/updated documentation)

All tests should be run against the port production environment(using a testing org).

### Core testing checklist - [x] Integration able to create all default resources from scratch - [x] Resync finishes successfully - [x] Resync able to create entities - [x] Resync able to update entities - [x] Resync able to detect and delete entities - [x] Scheduled resync able to abort existing resync and start a new one - [x] Tested with at least 2 integrations from scratch - [x] Tested with Kafka and Polling event listeners - [x] Tested deletion of entities that don't pass the selector ### Integration testing checklist - [ ] Integration able to create all default resources from scratch - [ ] Resync able to create entities - [ ] Resync able to update entities - [ ] Resync able to detect and delete entities - [ ] Resync finishes successfully - [ ] If new resource kind is added or updated in the integration, add example raw data, mapping and expected result to the `examples` folder in the integration directory. - [ ] If resource kind is updated, run the integration with the example data and check if the expected result is achieved - [ ] If new resource kind is added or updated, validate that live-events for that resource are working as expected - [ ] Docs PR link [here](#) ### Preflight checklist - [ ] Handled rate limiting - [ ] Handled pagination - [ ] Implemented the code in async - [ ] Support Multi account ## Screenshots Include screenshots from your environment showing how the resources of the integration will look. ## API Documentation Provide links to the API documentation used for this integration. --- CHANGELOG.md | 8 +++ port_ocean/clients/port/mixins/entities.py | 2 +- port_ocean/clients/port/utils.py | 2 +- port_ocean/config/settings.py | 2 +- port_ocean/helpers/retry.py | 5 ++ .../clients/port/mixins/test_entities.py | 53 +++++++++++++++++++ pyproject.toml | 2 +- 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 port_ocean/tests/clients/port/mixins/test_entities.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f960f840e6..8873f23374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm +## 0.10.8 (2024-09-04) + +### Bug Fixes + +- Avoid raising exception when receiving ReadTimeout on batch upsert entities +- Increased both internal port client and third party client timeout to handle long requests + + ## 0.10.7 (2024-08-28) ### Improvements diff --git a/port_ocean/clients/port/mixins/entities.py b/port_ocean/clients/port/mixins/entities.py index d34d5ce317..f8d5f63fa3 100644 --- a/port_ocean/clients/port/mixins/entities.py +++ b/port_ocean/clients/port/mixins/entities.py @@ -101,7 +101,7 @@ async def batch_upsert_entities( ) for entity in entities ), - return_exceptions=should_raise, + return_exceptions=True, ) entity_results = [ entity for entity in modified_entities_results if isinstance(entity, Entity) diff --git a/port_ocean/clients/port/utils.py b/port_ocean/clients/port/utils.py index 2bbc3359dc..a2c3782c84 100644 --- a/port_ocean/clients/port/utils.py +++ b/port_ocean/clients/port/utils.py @@ -18,7 +18,7 @@ # The max_keepalive_connections can't be too high, as it will cause the application to run out of available connections. PORT_HTTP_MAX_CONNECTIONS_LIMIT = 200 PORT_HTTP_MAX_KEEP_ALIVE_CONNECTIONS = 50 -PORT_HTTP_TIMEOUT = 10.0 +PORT_HTTP_TIMEOUT = 60.0 PORT_HTTPX_TIMEOUT = httpx.Timeout(PORT_HTTP_TIMEOUT) PORT_HTTPX_LIMITS = httpx.Limits( diff --git a/port_ocean/config/settings.py b/port_ocean/config/settings.py index 6a3fd3e7d4..5b9a8e7abb 100644 --- a/port_ocean/config/settings.py +++ b/port_ocean/config/settings.py @@ -67,7 +67,7 @@ class IntegrationConfiguration(BaseOceanSettings, extra=Extra.allow): allow_environment_variables_jq_access: bool = True initialize_port_resources: bool = True scheduled_resync_interval: int | None = None - client_timeout: int = 30 + client_timeout: int = 60 send_raw_data_examples: bool = True port: PortSettings event_listener: EventListenerSettingsType = Field( diff --git a/port_ocean/helpers/retry.py b/port_ocean/helpers/retry.py index 98ecffff34..077c6e0ffb 100644 --- a/port_ocean/helpers/retry.py +++ b/port_ocean/helpers/retry.py @@ -290,6 +290,11 @@ async def _retry_operation_async( if remaining_attempts < 1: self._log_error(request, error) raise + except httpx.ReadTimeout as e: + error = e + if remaining_attempts < 1: + self._log_error(request, error) + raise except httpx.TimeoutException as e: error = e if remaining_attempts < 1: diff --git a/port_ocean/tests/clients/port/mixins/test_entities.py b/port_ocean/tests/clients/port/mixins/test_entities.py new file mode 100644 index 0000000000..0a2d641d1b --- /dev/null +++ b/port_ocean/tests/clients/port/mixins/test_entities.py @@ -0,0 +1,53 @@ +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from port_ocean.clients.port.mixins.entities import EntityClientMixin +from port_ocean.core.models import Entity +from httpx import ReadTimeout + + +errored_entity_identifier: str = "a" +expected_result_entities = [ + Entity(identifier="b", blueprint="b"), + Entity(identifier="c", blueprint="c"), +] +all_entities = [ + Entity(identifier=errored_entity_identifier, blueprint="a") +] + expected_result_entities + + +async def mock_upsert_entity(entity: Entity, *args: Any, **kwargs: Any) -> Entity: + if entity.identifier == errored_entity_identifier: + raise ReadTimeout("") + else: + return entity + + +@pytest.fixture +async def entity_client(monkeypatch: Any) -> EntityClientMixin: + # Arrange + entity_client = EntityClientMixin(auth=MagicMock(), client=MagicMock()) + monkeypatch.setattr(entity_client, "upsert_entity", mock_upsert_entity) + + return entity_client + + +async def test_batch_upsert_entities_read_timeout_should_raise_false( + entity_client: EntityClientMixin, +) -> None: + result_entities = await entity_client.batch_upsert_entities( + entities=all_entities, request_options=MagicMock(), should_raise=False + ) + + assert result_entities == expected_result_entities + + +async def test_batch_upsert_entities_read_timeout_should_raise_true( + entity_client: EntityClientMixin, +) -> None: + with pytest.raises(ReadTimeout): + await entity_client.batch_upsert_entities( + entities=all_entities, request_options=MagicMock(), should_raise=True + ) diff --git a/pyproject.toml b/pyproject.toml index d7060f9561..4ef1caabb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "port-ocean" -version = "0.10.7" +version = "0.10.8" description = "Port Ocean is a CLI tool for managing your Port projects." readme = "README.md" homepage = "https://app.getport.io"