Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions docs/reference/feature-store-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ registry:
| `path` | string | — | Connection string or file path |
| `schema_mode` | string | `auto` | SQL registry only. `auto`: create tables on startup; `verify`: check tables exist, error if missing; `skip`: no DDL or verification. See [SQL Registry docs](registries/sql.md#schema-management-schema_mode). |
| `mcp.enabled` | bool | `false` | Enable MCP (Model Context Protocol) on the REST registry server |
| `serve_features_while_materializing` | bool | `false` | Keep serving a feature view's last-materialized values while it is in the `MATERIALIZING` state, instead of rejecting online requests during materialization |

When `serve_features_while_materializing` is `true`, online serving continues for a
feature view while `feast materialize` runs against a shared registry (which
transitions the feature view to `MATERIALIZING`). This avoids serving interruptions
from routine incremental materialization; feature views serve their
last-materialized values until materialization completes. States that never had
online data (e.g. `CREATED`, `GENERATED`) remain gated.

When `registry.mcp.enabled` is `true`, the REST registry server exposes registry
metadata (entities, feature views, feature services) as MCP tool endpoints for
Expand Down
6 changes: 6 additions & 0 deletions sdk/python/feast/infra/registry/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ def __init__(
else False
)

self.serve_features_while_materializing = (
registry_config.serve_features_while_materializing
if registry_config is not None
else False
)

self.cache_mode = (
registry_config.cache_mode if registry_config is not None else "sync"
)
Expand Down
3 changes: 3 additions & 0 deletions sdk/python/feast/infra/registry/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@ def __init__(
self.enable_online_versioning = (
registry_config.enable_online_feature_view_versioning
)
self.serve_features_while_materializing = (
registry_config.serve_features_while_materializing
)
super().__init__(
project=project,
cache_ttl_seconds=registry_config.cache_ttl_seconds,
Expand Down
9 changes: 9 additions & 0 deletions sdk/python/feast/repo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ class RegistryConfig(FeastBaseModel):
online store table and can be queried independently. Version history
tracking in the registry is always active regardless of this setting. """

serve_features_while_materializing: StrictBool = False
""" bool: Allow online serving to continue for a feature view while it is in
the ``MATERIALIZING`` state. When ``feature_store.materialize()`` runs against
a shared registry it transitions the feature view to ``MATERIALIZING`` and
commits that state, which otherwise causes concurrent feature servers to
reject requests until materialization completes. When True, feature views in
the ``MATERIALIZING`` state keep serving their last-materialized values.
Truly-unavailable states (e.g. ``CREATED``, ``GENERATED``) remain gated. """

mcp: Optional[McpRegistryConfig] = None
""" McpRegistryConfig: MCP (Model Context Protocol) configuration for the registry REST server. """

Expand Down
13 changes: 12 additions & 1 deletion sdk/python/feast/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1333,9 +1333,20 @@ def _append_or_merge_source_fv(fv_with_projection: "FeatureView") -> None:
if hasattr(fv, "state"):
from feast.feature_view import FeatureViewState

if isinstance(fv.state, FeatureViewState) and fv.state not in (
servable_states = {
FeatureViewState.STATE_UNSPECIFIED,
FeatureViewState.AVAILABLE_ONLINE,
}
# When 'serve_features_while_materializing' is enabled, keep serving
# the last-materialized values while a feature view is MATERIALIZING,
# so routine incremental materialization against a shared registry does
# not interrupt concurrent feature servers (see issue #6780).
if getattr(registry, "serve_features_while_materializing", False):
servable_states.add(FeatureViewState.MATERIALIZING)

if (
isinstance(fv.state, FeatureViewState)
and fv.state not in servable_states
):
raise ValueError(
f"Feature view '{name}' is in state '{fv.state.name}' "
Expand Down
50 changes: 50 additions & 0 deletions sdk/python/tests/unit/test_feature_view_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from feast import utils
from feast.data_format import AvroFormat, ParquetFormat
from feast.data_source import KafkaSource
from feast.entity import Entity
Expand Down Expand Up @@ -431,3 +432,52 @@ def test_materialize_disabled_fv_by_name_raises(self, local_feature_store):
end_date=datetime.utcnow(),
)
store.teardown()


# ---------------------------------------------------------------------------
# Serving lifecycle gate: serve_features_while_materializing
# ---------------------------------------------------------------------------


class _StubRegistry:
"""Minimal registry stub exposing what _get_feature_views_to_use needs."""

def __init__(self, fv, serve_features_while_materializing=False):
self._fv = fv
self.serve_features_while_materializing = serve_features_while_materializing

def get_any_feature_view(self, name, project, allow_cache=False):
return self._fv


class TestServeWhileMaterializingGate:
def _fv_in_state(self, state):
fv = _simple_feature_view()
fv.state = state
return fv

def test_materializing_blocked_by_default(self):
registry = _StubRegistry(self._fv_in_state(FeatureViewState.MATERIALIZING))
with pytest.raises(ValueError, match="cannot serve features"):
utils._get_feature_views_to_use(registry, "default", ["test_fv:f1"])

def test_materializing_served_when_flag_enabled(self):
registry = _StubRegistry(
self._fv_in_state(FeatureViewState.MATERIALIZING),
serve_features_while_materializing=True,
)
fvs, _ = utils._get_feature_views_to_use(registry, "default", ["test_fv:f1"])
assert [fv.name for fv in fvs] == ["test_fv"]

def test_available_online_served_regardless_of_flag(self):
registry = _StubRegistry(self._fv_in_state(FeatureViewState.AVAILABLE_ONLINE))
fvs, _ = utils._get_feature_views_to_use(registry, "default", ["test_fv:f1"])
assert [fv.name for fv in fvs] == ["test_fv"]

def test_created_still_blocked_even_with_flag(self):
registry = _StubRegistry(
self._fv_in_state(FeatureViewState.CREATED),
serve_features_while_materializing=True,
)
with pytest.raises(ValueError, match="cannot serve features"):
utils._get_feature_views_to_use(registry, "default", ["test_fv:f1"])
Loading