Skip to content

Commit 4140c53

Browse files
authored
Handle metrics_with_related read timeouts (#770)
## Summary The `metrics_with_related` GQL query is known to be a bit slow. We are seeing some timeouts in our backend that seem to be related to this. Since this path optionally adds extra context, it isn't necessary for `list_metrics` to behave correctly. Getting dimensions and entities can still be achieved through individual tool calls. ## Checklist - [x] I have performed a self-review of my code - [x] I have made corresponding changes to the documentation (in https://github.com/dbt-labs/docs.getdbt.com) if required -- Mention it here - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
1 parent 20c603c commit 4140c53

4 files changed

Lines changed: 35 additions & 22 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Under the Hood
2+
body: Handle metrics_with_related read timeouts
3+
time: 2026-05-11T13:37:01.154417-05:00

src/dbt_mcp/semantic_layer/client.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import base64
33
import json
4+
import logging
45
from collections.abc import Callable
56
from contextlib import AbstractContextManager
67
from datetime import date, datetime, time, timedelta
@@ -29,15 +30,17 @@
2930
GetMetricsCompiledSqlError,
3031
GetMetricsCompiledSqlResult,
3132
GetMetricsCompiledSqlSuccess,
33+
ListMetricsResponse,
3234
MetricToolResponse,
3335
OrderByParam,
3436
QueryMetricsError,
3537
QueryMetricsResult,
3638
QueryMetricsSuccess,
3739
SavedQueryToolResponse,
38-
ListMetricsResponse,
3940
)
4041

42+
logger = logging.getLogger(__name__)
43+
4144

4245
def DEFAULT_RESULT_FORMATTER(table: pa.Table) -> str:
4346
"""Convert PyArrow Table to JSON string with ISO date formatting.
@@ -130,18 +133,35 @@ async def list_metrics(
130133
{"query": GRAPHQL_QUERIES["metrics"], "variables": {"search": search}},
131134
)
132135
metrics_count = len(metrics_result["data"]["metricsPaginated"]["items"])
136+
dimensionless_response = ListMetricsResponse(
137+
metrics=[
138+
MetricToolResponse(
139+
name=m.get("name"),
140+
type=m.get("type"),
141+
label=m.get("label"),
142+
description=m.get("description"),
143+
metadata=(m.get("config") or {}).get("meta"),
144+
)
145+
for m in metrics_result["data"]["metricsPaginated"]["items"]
146+
]
147+
)
133148
if metrics_count and metrics_count <= config.metrics_related_max:
134149
# Re-fetch with the same search filter using a single query that includes
135150
# per-metric dimensions and entities. This avoids the N×2 parallel calls
136151
# approach: the nested GQL fields return per-metric data accurately (not
137152
# an intersection like dimensionsPaginated with multiple metrics would).
138-
full_result = await submit_request(
139-
config,
140-
{
141-
"query": GRAPHQL_QUERIES["metrics_with_related"],
142-
"variables": {"search": search},
143-
},
144-
)
153+
try:
154+
full_result = await submit_request(
155+
config,
156+
{
157+
"query": GRAPHQL_QUERIES["metrics_with_related"],
158+
"variables": {"search": search},
159+
},
160+
timeout=5.0,
161+
)
162+
except Exception as e:
163+
logger.warning(f"Error fetching metrics with related: {e}")
164+
return dimensionless_response
145165
return ListMetricsResponse(
146166
metrics=[
147167
MetricToolResponse(
@@ -156,18 +176,7 @@ async def list_metrics(
156176
for m in full_result["data"]["metricsPaginated"]["items"]
157177
]
158178
)
159-
return ListMetricsResponse(
160-
metrics=[
161-
MetricToolResponse(
162-
name=m.get("name"),
163-
type=m.get("type"),
164-
label=m.get("label"),
165-
description=m.get("description"),
166-
metadata=(m.get("config") or {}).get("meta"),
167-
)
168-
for m in metrics_result["data"]["metricsPaginated"]["items"]
169-
]
170-
)
179+
return dimensionless_response
171180

172181
async def list_saved_queries(
173182
self,

src/dbt_mcp/semantic_layer/gql/gql_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
async def submit_request(
88
sl_config: SemanticLayerConfig,
99
payload: dict,
10+
timeout: float = 30.0,
1011
) -> dict:
1112
if "variables" not in payload:
1213
payload["variables"] = {}
1314
payload["variables"]["environmentId"] = sl_config.prod_environment_id
1415

15-
async with httpx.AsyncClient(timeout=30.0) as client:
16+
async with httpx.AsyncClient(timeout=timeout) as client:
1617
response = await client.post(
1718
sl_config.url,
1819
json=payload,

tests/unit/semantic_layer/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def _make_query_dispatcher():
343343
query by checking for the presence of nested 'dimensions {' in the query string.
344344
"""
345345

346-
def dispatch(_, payload):
346+
def dispatch(_, payload, **kwargs):
347347
query = payload.get("query", "")
348348
if "metricsPaginated" in query:
349349
if "dimensions {" in query:

0 commit comments

Comments
 (0)