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
33 changes: 33 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. """
Comment on lines +206 to +213

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Consider adding validation for incompatible configurations

While the documentation is clear, consider adding runtime validation to ensure this setting doesn't conflict with other registry configurations that might make serving during materialization unsafe.


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

Expand Down Expand Up @@ -235,6 +244,30 @@ def validate_path(cls, path: str, values: ValidationInfo) -> str:
return cls._normalize_postgres_scheme(path, "path")
return path

@model_validator(mode="after")
def _warn_on_unsafe_serve_while_materializing(self) -> "RegistryConfig":
"""Warn when ``serve_features_while_materializing`` is combined with a
registry configuration where it is a no-op or where its guarantees may
not hold. These are advisory warnings only, never hard failures.
"""
if self.serve_features_while_materializing:
if self.registry_type != "sql":
_logger.warning(
"`serve_features_while_materializing` is enabled but "
f"`registry_type` is '{self.registry_type}'. The flag targets "
"the serving interruption caused by concurrent servers sharing "
"a `sql` registry that materialization flips to `MATERIALIZING`; "
"on other registry types it typically has no effect."
)
if self.cache_mode == "thread":
_logger.warning(
"`serve_features_while_materializing` is enabled together with "
"`cache_mode='thread'`. In thread mode the registry cache may lag "
"by up to `cache_ttl_seconds`, so feature-view state transitions "
"(including the exit from `MATERIALIZING`) may be observed stale."
)
return self


class MaterializationConfig(BaseModel):
"""Configuration options for feature materialization behavior."""
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"])
7 changes: 5 additions & 2 deletions sdk/python/tests/unit/test_offline_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,11 @@ def tracking_import(name, *args, **kwargs):
capture_output=True,
text=True,
env=env,
# Cold imports can be slow on macOS under parallel test load.
timeout=180,
# A cold subprocess importing feast.offline_server pulls in pyarrow and
# gRPC, which can take well over a minute on contended CI runners
# (notably macOS). Allow generous headroom so this does not flake while
# still catching a genuine import hang.
timeout=300,
)
except subprocess.TimeoutExpired as exc:
# TimeoutExpired captures bytes even when subprocess.run uses text=True.
Expand Down
44 changes: 44 additions & 0 deletions sdk/python/tests/unit/test_registry_string_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,47 @@ def test_pathlib_does_not_treat_s3_as_absolute(self):

joined = Path("/app").joinpath(s3_path)
assert str(joined).startswith("/app/s3:")


class TestServeWhileMaterializingWarnings:
"""`serve_features_while_materializing` emits advisory warnings when combined
with a registry configuration where the flag is a no-op or where its
guarantees may not hold. These are warnings only, never hard failures."""

def test_no_warning_when_flag_disabled(self, caplog):
with caplog.at_level("WARNING", logger="feast.repo_config"):
RegistryConfig(registry_type="sql")
assert "serve_features_while_materializing" not in caplog.text

def test_no_warning_for_safe_sql_sync_config(self, caplog):
with caplog.at_level("WARNING", logger="feast.repo_config"):
RegistryConfig(registry_type="sql", serve_features_while_materializing=True)
assert "serve_features_while_materializing" not in caplog.text

def test_warns_on_non_sql_registry(self, caplog):
with caplog.at_level("WARNING", logger="feast.repo_config"):
RegistryConfig(
registry_type="file", serve_features_while_materializing=True
)
assert "registry_type" in caplog.text
assert "cache_mode" not in caplog.text

def test_warns_on_thread_cache_mode(self, caplog):
with caplog.at_level("WARNING", logger="feast.repo_config"):
RegistryConfig(
registry_type="sql",
cache_mode="thread",
serve_features_while_materializing=True,
)
assert "cache_mode='thread'" in caplog.text
assert "registry_type" not in caplog.text

def test_warns_on_both_when_both_incompatible(self, caplog):
with caplog.at_level("WARNING", logger="feast.repo_config"):
RegistryConfig(
registry_type="file",
cache_mode="thread",
serve_features_while_materializing=True,
)
assert "registry_type" in caplog.text
assert "cache_mode='thread'" in caplog.text
Loading