Visitar URL original
feat(server): Remote Materialization by aniketpalu · Pull Request #6649 · 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
Prev Previous commit
Next Next commit
fix: Default run_async to False and cover version forwarding
Preserve sync semantics for remote users; opt into fire-and-forget
with run_async=True. Add unit tests that version is threaded through
authorize and both sync/async materialize server paths.

Signed-off-by: Aniket Paluskar <apaluska@redhat.com>
  • Loading branch information
aniketpalu committed Aug 5, 2026
commit 7909fc4a71371049a3a68b8aa36f533b42d9422d
24 changes: 12 additions & 12 deletions sdk/python/feast/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2516,13 +2516,13 @@ def _delegate_remote_materialize(
endpoint: str,
payload: Dict[str, Any],
force: bool = False,
run_async: bool = True,
run_async: bool = False,
) -> None:
"""POST materialize to the feature server.

When run_async=True (default), sends ?async=true and returns after 202.
When run_async=False, omits async and blocks until the server finishes
synchronous materialization (HTTP response).
When run_async=False (default), omits async and blocks until the server
finishes synchronous materialization (HTTP response).
When run_async=True, sends ?async=true and returns after 202.
force=True is only valid with run_async=True (server force applies to async).
"""
if force and not run_async:
Expand Down Expand Up @@ -2550,7 +2550,7 @@ def materialize_incremental(
full_feature_names: bool = False,
version: Optional[str] = None,
force: bool = False,
run_async: bool = True,
run_async: bool = False,
) -> None:
"""
Materialize incremental new data from the offline store into the online store.
Expand All @@ -2571,9 +2571,9 @@ def materialize_incremental(
with exactly one entry and enable_online_feature_view_versioning to be enabled.
force (bool): When using remote topology with run_async=True, pass force=true to
override stuck MATERIALIZING state on the feature server. Ignored for local topology.
run_async (bool): When using remote topology, if True (default) POST with ?async=true
and return after 202. If False, POST without async and block until the server
finishes sync materialization. Ignored for local topology.
run_async (bool): When using remote topology, if False (default) POST without async and
block until the server finishes sync materialization. If True, POST with ?async=true
and return after 202. Ignored for local topology.

Raises:
Exception: A feature view being materialized does not have a TTL set.
Expand Down Expand Up @@ -2794,7 +2794,7 @@ def materialize(
full_feature_names: bool = False,
version: Optional[str] = None,
force: bool = False,
run_async: bool = True,
run_async: bool = False,
) -> None:
"""
Materialize data from the offline store into the online store.
Expand All @@ -2815,9 +2815,9 @@ def materialize(
with exactly one entry and enable_online_feature_view_versioning to be enabled.
force (bool): When using remote topology with run_async=True, pass force=true to
override stuck MATERIALIZING state on the feature server. Ignored for local topology.
run_async (bool): When using remote topology, if True (default) POST with ?async=true
and return after 202. If False, POST without async and block until the server
finishes sync materialization. Ignored for local topology.
run_async (bool): When using remote topology, if False (default) POST without async and
block until the server finishes sync materialization. If True, POST with ?async=true
and return after 202. Ignored for local topology.

Examples:
Materialize all features into the online store over the interval
Expand Down
169 changes: 169 additions & 0 deletions sdk/python/tests/unit/test_materialize_version_forwarding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Copyright 2026 The Feast Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Server tests: version is forwarded on materialize sync and async paths."""

from concurrent.futures import Future
from inspect import signature
from unittest.mock import AsyncMock, MagicMock, patch

from fastapi.testclient import TestClient

from feast.feature_server import _authorize_materialize_views, get_app
from feast.feature_store import FeatureStore
from feast.feature_view import FeatureViewState


def _mock_store_for_materialize():
fs = MagicMock()
fs.project = "test_project"
fs.initialize = AsyncMock()
fs.close = AsyncMock()
fs._validate_materialize_version.return_value = "v2"
fv = MagicMock()
fv.name = "driver_hourly_stats"
fv.state = FeatureViewState.GENERATED
fs._get_feature_views_to_materialize.return_value = [fv]
fs.registry.get_feature_view.return_value = fv
fs.materialize = MagicMock()
fs.materialize_incremental = MagicMock()
return fs


def _run_executor_inline(loop, executor, func, *args):
"""Make asyncio run_in_executor execute synchronously for tests."""
fut: Future = Future()
try:
fut.set_result(func(*args) if args else func())
except Exception as exc:
fut.set_exception(exc)
return fut


def test_run_async_defaults_to_false():
for method_name in (
"materialize",
"materialize_incremental",
"_delegate_remote_materialize",
):
param = signature(getattr(FeatureStore, method_name)).parameters["run_async"]
assert param.default is False, (
f"{method_name}.run_async default should be False"
)


@patch("feast.feature_server.assert_permissions")
def test_authorize_materialize_views_forwards_version(_mock_perms):
fs = _mock_store_for_materialize()
names = _authorize_materialize_views(fs, ["driver_hourly_stats"], version="v2")
assert names == ["driver_hourly_stats"]
fs._validate_materialize_version.assert_called_once_with(
"v2", ["driver_hourly_stats"]
)
fs._get_feature_views_to_materialize.assert_called_once_with(
["driver_hourly_stats"], version="v2"
)


@patch("feast.feature_server.assert_permissions")
def test_materialize_sync_forwards_version(_mock_perms):
fs = _mock_store_for_materialize()
client = TestClient(get_app(fs))

response = client.post(
"/materialize",
json={
"start_ts": "2021-01-01T00:00:00",
"end_ts": "2021-01-02T00:00:00",
"feature_views": ["driver_hourly_stats"],
"version": "v2",
},
)

assert response.status_code == 200
fs._validate_materialize_version.assert_called_with("v2", ["driver_hourly_stats"])
fs.materialize.assert_called_once()
_, kwargs = fs.materialize.call_args
assert kwargs.get("version") == "v2"
assert fs.materialize.call_args.args[2] == ["driver_hourly_stats"]


@patch("feast.feature_server.assert_permissions")
def test_materialize_incremental_sync_forwards_version(_mock_perms):
fs = _mock_store_for_materialize()
client = TestClient(get_app(fs))

response = client.post(
"/materialize-incremental",
json={
"end_ts": "2021-01-02T00:00:00",
"feature_views": ["driver_hourly_stats"],
"version": "v2",
},
)

assert response.status_code == 200
fs._validate_materialize_version.assert_called_with("v2", ["driver_hourly_stats"])
fs.materialize_incremental.assert_called_once()
_, kwargs = fs.materialize_incremental.call_args
assert kwargs.get("version") == "v2"
assert fs.materialize_incremental.call_args.args[1] == ["driver_hourly_stats"]


@patch(
"asyncio.BaseEventLoop.run_in_executor",
new=_run_executor_inline,
)
@patch("feast.feature_server.assert_permissions")
def test_materialize_async_forwards_version(_mock_perms):
fs = _mock_store_for_materialize()
client = TestClient(get_app(fs))

response = client.post(
"/materialize?async=true",
json={
"start_ts": "2021-01-01T00:00:00",
"end_ts": "2021-01-02T00:00:00",
"feature_views": ["driver_hourly_stats"],
"version": "v2",
},
)

assert response.status_code == 202
fs.materialize.assert_called_once()
_, kwargs = fs.materialize.call_args
assert kwargs.get("version") == "v2"


@patch(
"asyncio.BaseEventLoop.run_in_executor",
new=_run_executor_inline,
)
@patch("feast.feature_server.assert_permissions")
def test_materialize_incremental_async_forwards_version(_mock_perms):
fs = _mock_store_for_materialize()
client = TestClient(get_app(fs))

response = client.post(
"/materialize-incremental?async=true",
json={
"end_ts": "2021-01-02T00:00:00",
"feature_views": ["driver_hourly_stats"],
"version": "v2",
},
)

assert response.status_code == 202
fs.materialize_incremental.assert_called_once()
_, kwargs = fs.materialize_incremental.call_args
assert kwargs.get("version") == "v2"
Loading