diff --git a/api/integrations/common/constants.py b/api/integrations/common/constants.py new file mode 100644 index 000000000000..d17252d73f14 --- /dev/null +++ b/api/integrations/common/constants.py @@ -0,0 +1,5 @@ +# Default timeout (in seconds) for outbound HTTP requests made by integration +# wrappers. Without an explicit timeout, ``requests`` waits indefinitely, so an +# unresponsive third-party endpoint can hang the worker thread that dispatches +# the event, leading to resource exhaustion. +INTEGRATION_REQUEST_TIMEOUT_SECONDS = 10 diff --git a/api/integrations/datadog/datadog.py b/api/integrations/datadog/datadog.py index 7fbd759b0dd9..b62d2b78679d 100644 --- a/api/integrations/datadog/datadog.py +++ b/api/integrations/datadog/datadog.py @@ -4,6 +4,9 @@ import requests from audit.models import AuditLog +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper logger = logging.getLogger(__name__) @@ -46,7 +49,9 @@ def _track_event(self, event: dict) -> None: # type: ignore[type-arg] event["source_type_name"] = FLAGSMITH_SOURCE_TYPE_NAME response = self.session.post( - f"{self.events_url}?api_key={self.api_key}", data=json.dumps(event) + f"{self.events_url}?api_key={self.api_key}", + data=json.dumps(event), + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, ) logger.debug( "Sent event to DataDog. Response code was %s" % response.status_code diff --git a/api/integrations/dynatrace/dynatrace.py b/api/integrations/dynatrace/dynatrace.py index 75bc41df566e..41b20c994c49 100644 --- a/api/integrations/dynatrace/dynatrace.py +++ b/api/integrations/dynatrace/dynatrace.py @@ -7,6 +7,9 @@ from audit.services import get_audited_instance_from_audit_log_record from features.models import Feature, FeatureState from features.versioning.models import EnvironmentFeatureVersion +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper from segments.models import Segment @@ -29,7 +32,10 @@ def __init__(self, base_url: str, api_key: str, entity_selector: str): def _track_event(self, event: dict) -> None: # type: ignore[type-arg] event["entitySelector"] = self.entity_selector response = requests.post( - self.url, headers=self._headers(), data=json.dumps(event) + self.url, + headers=self._headers(), + data=json.dumps(event), + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, ) logger.debug( "Sent event to Dynatrace. Response code was %s" % response.status_code diff --git a/api/integrations/grafana/grafana.py b/api/integrations/grafana/grafana.py index 21289d13aa51..8da5e68973f7 100644 --- a/api/integrations/grafana/grafana.py +++ b/api/integrations/grafana/grafana.py @@ -5,6 +5,9 @@ import requests from audit.models import AuditLog +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper from integrations.grafana.mappers import ( map_audit_log_record_to_grafana_annotation, @@ -36,6 +39,7 @@ def _track_event(self, event: dict[str, Any]) -> None: url=self.url, headers=self._headers(), data=json.dumps(event), + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, ) logger.debug( diff --git a/api/integrations/heap/heap.py b/api/integrations/heap/heap.py index 5b15d4ca1c17..bf419182baed 100644 --- a/api/integrations/heap/heap.py +++ b/api/integrations/heap/heap.py @@ -6,6 +6,9 @@ from environments.identities.models import Identity from environments.identities.traits.models import Trait from features.models import FeatureState +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseIdentityIntegrationWrapper from .constants import DEFAULT_HEAP_API_URL @@ -21,7 +24,11 @@ def __init__(self, config: HeapConfiguration): self.url = f"{base_url}/api/track" def _identify_user(self, user_data: dict) -> None: # type: ignore[type-arg] - response = requests.post(self.url, json=user_data) + response = requests.post( + self.url, + json=user_data, + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, + ) logger.debug("Sent event to Heap. Response code was: %s" % response.status_code) def generate_user_data( diff --git a/api/integrations/mixpanel/mixpanel.py b/api/integrations/mixpanel/mixpanel.py index cc2820aaa53c..ea9333eefa4d 100644 --- a/api/integrations/mixpanel/mixpanel.py +++ b/api/integrations/mixpanel/mixpanel.py @@ -6,6 +6,9 @@ from environments.identities.models import Identity from environments.identities.traits.models import Trait from features.models import FeatureState +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseIdentityIntegrationWrapper from .constants import DEFAULT_MIXPANEL_API_URL @@ -30,7 +33,12 @@ def __init__(self, config: MixpanelConfiguration): } def _identify_user(self, user_data: MixpanelUserData) -> None: - response = requests.post(self.url, headers=self.headers, json=user_data) + response = requests.post( + self.url, + headers=self.headers, + json=user_data, + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, + ) logger.debug( "Sent event to Mixpanel. Response code was: %s" % response.status_code ) diff --git a/api/integrations/new_relic/new_relic.py b/api/integrations/new_relic/new_relic.py index 622756f45d90..eb4b8513ae74 100644 --- a/api/integrations/new_relic/new_relic.py +++ b/api/integrations/new_relic/new_relic.py @@ -4,6 +4,9 @@ import requests from audit.models import AuditLog +from integrations.common.constants import ( + INTEGRATION_REQUEST_TIMEOUT_SECONDS, +) from integrations.common.wrapper import AbstractBaseEventIntegrationWrapper logger = logging.getLogger(__name__) @@ -20,7 +23,10 @@ def __init__(self, base_url: str, api_key: str, app_id: str): def _track_event(self, event: dict) -> None: # type: ignore[type-arg] response = requests.post( - self.url, headers=self._headers(), data=json.dumps(event) + self.url, + headers=self._headers(), + data=json.dumps(event), + timeout=INTEGRATION_REQUEST_TIMEOUT_SECONDS, ) logger.debug( "Sent event to NewRelic. Response code was %s" % response.status_code