diff --git a/.changes/unreleased/Fixes-20260329-190000.yaml b/.changes/unreleased/Fixes-20260329-190000.yaml new file mode 100644 index 00000000000..fde5c36eeed --- /dev/null +++ b/.changes/unreleased/Fixes-20260329-190000.yaml @@ -0,0 +1,6 @@ +kind: Fixes +body: Reduce snowplow telemetry HTTP timeout from 5s to 1s so an unreachable collector does not stall dbt at the end of every invocation +time: 2026-03-29T19:00:00.000000-04:00 +custom: + Author: claygeo + Issue: "9989" diff --git a/core/dbt/tracking.py b/core/dbt/tracking.py index 7032f873541..f3e70fa14d8 100644 --- a/core/dbt/tracking.py +++ b/core/dbt/tracking.py @@ -100,7 +100,10 @@ def http_post(self, payload): self.endpoint, data=payload, headers={"content-type": "application/json; charset=utf-8"}, - timeout=5.0, + # Keep the timeout short so that a missing or unreachable collector + # does not noticeably delay the end of every dbt invocation. + # See https://github.com/dbt-labs/dbt-core/issues/9989 + timeout=1.0, ) self._log_result("GET", r.status_code) @@ -109,7 +112,14 @@ def http_post(self, payload): def http_get(self, payload): self._log_request("GET", payload) - r = requests.get(self.endpoint, params=payload, timeout=5.0) + r = requests.get( + self.endpoint, + params=payload, + # Keep the timeout short so that a missing or unreachable collector + # does not noticeably delay the end of every dbt invocation. + # See https://github.com/dbt-labs/dbt-core/issues/9989 + timeout=1.0, + ) self._log_result("GET", r.status_code) return r diff --git a/tests/unit/test_tracking.py b/tests/unit/test_tracking.py index 998c33851c8..e9a9d18e02a 100644 --- a/tests/unit/test_tracking.py +++ b/tests/unit/test_tracking.py @@ -1,6 +1,7 @@ import datetime import tempfile from unittest import mock +from unittest.mock import MagicMock, patch import pytest @@ -228,3 +229,38 @@ def test_forwards_catalog_type_from_adapter_integration( opts = mock_track.call_args[0][0] assert opts["catalog_type"] == "ICEBERG_REST" adapter.get_catalog_integration.assert_called_once_with("test_catalog") + + +class TestTimeoutEmitter: + """Verify that the TimeoutEmitter uses short timeouts so an unreachable + collector doesn't stall dbt at the end of every invocation. + See https://github.com/dbt-labs/dbt-core/issues/9989 + """ + + def test_http_post_uses_short_timeout(self): + emitter = dbt.tracking.TimeoutEmitter() + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch("dbt.tracking.requests.post", return_value=mock_response) as mock_post: + emitter.http_post('{"test": "payload"}') + _, kwargs = mock_post.call_args + assert "timeout" in kwargs + assert kwargs["timeout"] <= 2.0, ( + f"POST timeout {kwargs['timeout']}s is too long; keep it ≤ 2s so an unreachable " + "collector doesn't stall dbt at the end of every invocation." + ) + + def test_http_get_uses_short_timeout(self): + emitter = dbt.tracking.TimeoutEmitter() + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch("dbt.tracking.requests.get", return_value=mock_response) as mock_get: + emitter.http_get({"test": "payload"}) + _, kwargs = mock_get.call_args + assert "timeout" in kwargs + assert kwargs["timeout"] <= 2.0, ( + f"GET timeout {kwargs['timeout']}s is too long; keep it ≤ 2s so an unreachable " + "collector doesn't stall dbt at the end of every invocation." + )