Skip to content

Commit 661ecc7

Browse files
committed
fix: Fixed the intermittent FeatureViewNotFoundException
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent c335ec7 commit 661ecc7

File tree

4 files changed

+103
-13
lines changed

4 files changed

+103
-13
lines changed

sdk/python/feast/arrow_error_handler.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import time
34
from functools import wraps
@@ -67,15 +68,27 @@ def wrapper(*args, **kwargs):
6768

6869

6970
def _get_exception_data(except_str) -> str:
70-
substring = "Flight error: "
71+
if not isinstance(except_str, str):
72+
return ""
7173

72-
# Find the starting index of the substring
74+
substring = "Flight error: "
7375
position = except_str.find(substring)
74-
end_json_index = except_str.find("}")
75-
76-
if position != -1 and end_json_index != -1:
77-
# Extract the part of the string after the substring
78-
result = except_str[position + len(substring) : end_json_index + 1]
79-
return result
80-
81-
return ""
76+
if position == -1:
77+
return ""
78+
79+
search_start = position + len(substring)
80+
json_start = except_str.find("{", search_start)
81+
if json_start == -1:
82+
return ""
83+
84+
search_pos = json_start
85+
while True:
86+
json_end = except_str.find("}", search_pos + 1)
87+
if json_end == -1:
88+
return ""
89+
candidate = except_str[json_start : json_end + 1]
90+
try:
91+
json.loads(candidate)
92+
return candidate
93+
except (json.JSONDecodeError, ValueError):
94+
search_pos = json_end

sdk/python/feast/offline_server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from feast import FeatureStore, FeatureView, utils
1616
from feast.arrow_error_handler import arrow_server_error_handling_decorator
1717
from feast.data_source import DataSource
18+
from feast.errors import FeatureViewNotFoundException
1819
from feast.feature_logging import FeatureServiceLoggingSource
1920
from feast.feature_view import DUMMY_ENTITY_NAME
2021
from feast.infra.offline_stores.offline_utils import get_offline_store_from_config
@@ -199,7 +200,7 @@ def get_feature_view_by_name(
199200
)
200201
fv = fv.with_projection(p)
201202
return fv
202-
except Exception:
203+
except FeatureViewNotFoundException:
203204
try:
204205
return self.store.registry.get_stream_feature_view(
205206
name=fv_name, project=project

sdk/python/tests/unit/test_arrow_error_decorator.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
import pytest
55
from pydantic import ValidationError
66

7-
from feast.arrow_error_handler import arrow_client_error_handling_decorator
8-
from feast.errors import PermissionNotFoundException
7+
from feast.arrow_error_handler import (
8+
_get_exception_data,
9+
arrow_client_error_handling_decorator,
10+
)
11+
from feast.errors import FeatureViewNotFoundException, PermissionNotFoundException
912
from feast.infra.offline_stores.remote import RemoteOfflineStoreConfig
1013

1114
permissionError = PermissionNotFoundException("dummy_name", "dummy_project")
@@ -179,3 +182,50 @@ def method_on_client(self_arg):
179182
def test_config_rejects_negative_connection_retries(self):
180183
with pytest.raises(ValidationError):
181184
RemoteOfflineStoreConfig(host="localhost", connection_retries=-1)
185+
186+
187+
class TestGetExceptionData:
188+
def test_non_string_input_returns_empty(self):
189+
assert _get_exception_data(12345) == ""
190+
assert _get_exception_data(None) == ""
191+
assert _get_exception_data(b"bytes") == ""
192+
193+
def test_no_flight_error_prefix_returns_empty(self):
194+
assert _get_exception_data("some random error") == ""
195+
196+
def test_flight_error_prefix_without_json_returns_empty(self):
197+
assert _get_exception_data("Flight error: no json here") == ""
198+
199+
def test_extracts_json_from_flight_error(self):
200+
fv_error = FeatureViewNotFoundException("my_view", "my_project")
201+
error_str = f"Flight error: {fv_error.to_error_detail()}"
202+
result = _get_exception_data(error_str)
203+
assert '"class": "FeatureViewNotFoundException"' in result
204+
assert '"module": "feast.errors"' in result
205+
206+
def test_extracts_json_with_trailing_text(self):
207+
fv_error = FeatureViewNotFoundException("my_view", "my_project")
208+
error_str = (
209+
f"Flight error: {fv_error.to_error_detail()}. "
210+
"gRPC client debug context: some extra info"
211+
)
212+
result = _get_exception_data(error_str)
213+
assert '"class": "FeatureViewNotFoundException"' in result
214+
assert '"module": "feast.errors"' in result
215+
216+
def test_extracts_json_with_grpc_debug_context_containing_braces(self):
217+
fv_error = FeatureViewNotFoundException("my_view", "my_project")
218+
error_str = (
219+
f"Flight error: {fv_error.to_error_detail()}. "
220+
"gRPC client debug context: UNKNOWN:Error received from peer "
221+
'ipv4:127.0.0.1:59930 {grpc_message:"Flight error: ...", '
222+
'grpc_status:2, created_time:"2026-03-17T17:32:07"}'
223+
)
224+
result = _get_exception_data(error_str)
225+
assert '"class": "FeatureViewNotFoundException"' in result
226+
assert '"module": "feast.errors"' in result
227+
from feast.errors import FeastError
228+
229+
reconstructed = FeastError.from_error_detail(result)
230+
assert reconstructed is not None
231+
assert "my_view" in str(reconstructed)

sdk/python/tests/unit/test_offline_server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import tempfile
33
from datetime import datetime, timedelta
4+
from unittest.mock import patch
45

56
import assertpy
67
import pandas as pd
@@ -345,3 +346,28 @@ def _test_pull_all_from_table_or_query(temp_dir, fs: FeatureStore):
345346
start_date=start_date,
346347
end_date=end_date,
347348
).to_df()
349+
350+
351+
def test_get_feature_view_by_name_propagates_transient_errors():
352+
"""Transient registry errors must not be swallowed and misreported as
353+
FeatureViewNotFoundException."""
354+
with tempfile.TemporaryDirectory() as temp_dir:
355+
store = default_store(str(temp_dir))
356+
location = "grpc+tcp://localhost:0"
357+
358+
_init_auth_manager(store=store)
359+
server = OfflineServer(store=store, location=location)
360+
361+
transient_error = ConnectionError("registry temporarily unavailable")
362+
363+
with patch.object(
364+
server.store.registry,
365+
"get_feature_view",
366+
side_effect=transient_error,
367+
):
368+
with pytest.raises(ConnectionError, match="registry temporarily"):
369+
server.get_feature_view_by_name(
370+
fv_name="driver_hourly_stats",
371+
name_alias=None,
372+
project=PROJECT_NAME,
373+
)

0 commit comments

Comments
 (0)