Visitar URL original
perf: Replace MessageToDict with optimized custom dict builder by abhijeet-dhumal · Pull Request #6015 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
perf: Replace MessageToDict with optimized custom dict builder
Signed-off-by: abhijeet-dhumal <abhijeetdhumal652@gmail.com>
  • Loading branch information
abhijeet-dhumal committed May 27, 2026
commit b28ddf9bd362a6636621ac0244b28cd7d71a2cd2
22 changes: 6 additions & 16 deletions sdk/python/feast/feature_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@
)
from fastapi.concurrency import run_in_threadpool
from fastapi.logger import logger
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, ORJSONResponse

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from fastapi.staticfiles import StaticFiles
from google.protobuf.json_format import MessageToDict
from pydantic import BaseModel, field_validator

import feast
Expand All @@ -52,6 +51,7 @@
FeastError,
)
from feast.feast_object import FeastObject
from feast.feature_server_utils import response_to_dict_fast
from feast.feature_view_utils import get_feature_view_from_feature_store
from feast.permissions.action import WRITE, AuthzedAction
from feast.permissions.security_manager import assert_permissions
Expand Down Expand Up @@ -391,13 +391,8 @@ async def get_online_features(request: GetOnlineFeaturesRequest) -> Any:
lambda: store.get_online_features(**read_params) # type: ignore
)

response_dict = await run_in_threadpool(
MessageToDict,
response.proto,
preserving_proto_field_name=True,
float_precision=18,
)
return response_dict
response_dict = await run_in_threadpool(response_to_dict_fast, response.proto)
return ORJSONResponse(content=response_dict)
Comment thread
abhijeet-dhumal marked this conversation as resolved.
Outdated

@app.post(
"/retrieve-online-documents",
Expand Down Expand Up @@ -433,13 +428,8 @@ async def retrieve_online_documents(
lambda: store.retrieve_online_documents(**read_params) # type: ignore
)

response_dict = await run_in_threadpool(
MessageToDict,
response.proto,
preserving_proto_field_name=True,
float_precision=18,
)
return response_dict
response_dict = await run_in_threadpool(response_to_dict_fast, response.proto)
return ORJSONResponse(content=response_dict)

@app.post("/push", dependencies=[Depends(inject_user_details)])
async def push(request: PushFeaturesRequest) -> Response:
Expand Down
79 changes: 79 additions & 0 deletions sdk/python/feast/feature_server_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Fast serialization utilities for Feature Server responses.

Matches the output format of MessageToDict with proto_json.patch() applied.
Values are serialized as native Python types (not wrapped dicts).
"""

from datetime import datetime, timezone
from typing import Any, Dict, Optional

from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse
from feast.protos.feast.types.Value_pb2 import Value

# FieldStatus enum mapping (protos/feast/serving/ServingService.proto)
_STATUS_NAMES: Dict[int, str] = {
0: "INVALID",
1: "PRESENT",
2: "NULL_VALUE",
3: "NOT_FOUND",
4: "OUTSIDE_MAX_AGE",
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.


def response_to_dict_fast(response: GetOnlineFeaturesResponse) -> Dict[str, Any]:

@franciscojavierarceo franciscojavierarceo Mar 6, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not response_to_dict? better yet, convert_response_to_dict is a better description

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - renamed to convert_response_to_dict. The _fast suffix was leaking implementation details ✅

"""Convert GetOnlineFeaturesResponse to dict (matches proto_json.patch() format)."""
result: Dict[str, Any] = {
"results": [
{
"values": [_value_to_native(v) for v in feature_vector.values],
"statuses": [
_STATUS_NAMES.get(s, "INVALID") for s in feature_vector.statuses
],
**(
{
"event_timestamps": [
_timestamp_to_str(ts)
for ts in feature_vector.event_timestamps
]
}
if feature_vector.event_timestamps
else {}
),
}
for feature_vector in response.results
]
}

if response.HasField("metadata"):
result["metadata"] = _metadata_to_dict(response.metadata)

return result
Comment thread
abhijeet-dhumal marked this conversation as resolved.


def _value_to_native(v: Value) -> Optional[Any]:
"""Convert a Value proto to native Python type (matches proto_json.patch() format)."""
which = v.WhichOneof("val")
if which is None or which == "null_val":
return None
elif "_list_" in which:
return list(getattr(v, which).val)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
else:
return getattr(v, which)
Comment on lines +77 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bytes_val values cause ORJSONResponse to raise TypeError

_value_to_native at feature_server_utils.py:68 returns raw bytes for bytes_val fields. Even if orjson were installed, orjson.dumps() cannot serialize Python bytes objects and raises TypeError: Type is not JSON serializable: bytes. The old code path returned the dict to FastAPI which processed it through jsonable_encoder (capable of handling bytes). The switch to ORJSONResponse bypasses that encoder, causing a regression for any response containing bytes-typed feature values.

Prompt for agents
In _value_to_native (feature_server_utils.py), when the value type is bytes_val, the raw bytes object is returned. orjson cannot serialize bytes. You need to either: (1) base64-encode bytes values before returning them (e.g. using base64.b64encode(val).decode('ascii')), or (2) not use ORJSONResponse and instead use a serializer that handles bytes. Note that bytes_list_val (handled by the _list_ branch) would also produce a list of bytes objects with the same issue.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhijeet-dhumal bytes_val is still problem since you are using JSONResponse

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this, Nikhil! Now bytes_val and bytes_list_val now base64-encode the output before returning, so JSONResponse / json.dumps can serialize them without TypeError.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review!

Comment thread
abhijeet-dhumal marked this conversation as resolved.
Outdated


def _timestamp_to_str(ts) -> str:
"""Convert protobuf Timestamp to RFC 3339 format with Z suffix."""
if ts.seconds == 0 and ts.nanos == 0:
return "1970-01-01T00:00:00Z"
dt = datetime.fromtimestamp(ts.seconds + ts.nanos / 1e9, tz=timezone.utc)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
return dt.strftime("%Y-%m-%dT%H:%M:%S") + (
".%06dZ" % (ts.nanos // 1000) if ts.nanos else "Z"
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated


def _metadata_to_dict(metadata) -> Dict[str, Any]:
"""Convert FeatureResponseMeta to dict (matches proto_json.patch() format)."""
result: Dict[str, Any] = {}
if metadata.HasField("feature_names"):
result["feature_names"] = list(metadata.feature_names.val)
return result
Loading