From b7a8c26c8d93ea8cbd2dfc7cf47e86d04720cc84 Mon Sep 17 00:00:00 2001 From: Alan Gauthier Date: Thu, 27 Aug 2026 16:05:32 +0200 Subject: [PATCH 1/3] fix: Allow serving features while a feature view is MATERIALIZING The lifecycle serving gate added in v0.64.0 rejects any feature view not in AVAILABLE_ONLINE / STATE_UNSPECIFIED state. Because materialization transitions a feature view to MATERIALIZING in the shared registry, routine incremental materialization interrupts concurrent feature servers with "cannot serve features. Only AVAILABLE_ONLINE feature views can serve." Add an opt-in registry config flag, serve_features_while_materializing (default False), that also permits serving while a feature view is MATERIALIZING, letting servers keep returning last-materialized values during materialization. States that never had online data (CREATED, GENERATED) remain gated. Fixes #6780 Signed-off-by: Alan Gauthier --- docs/reference/feature-store-yaml.md | 8 +++ sdk/python/feast/infra/registry/registry.py | 6 +++ sdk/python/feast/infra/registry/sql.py | 3 ++ sdk/python/feast/repo_config.py | 9 ++++ sdk/python/feast/utils.py | 13 ++++- .../tests/unit/test_feature_view_state.py | 50 +++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/reference/feature-store-yaml.md b/docs/reference/feature-store-yaml.md index 1aac166bd8b..f5cc1cecb0a 100644 --- a/docs/reference/feature-store-yaml.md +++ b/docs/reference/feature-store-yaml.md @@ -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 diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 5737df06881..a16c4538a92 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -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" ) diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 4f1b6c174f0..c2b57d06cf1 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -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, diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 54e7b2f5d68..3ffbf279966 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -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. """ diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 4f8d18ad080..aa04380728d 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -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}' " diff --git a/sdk/python/tests/unit/test_feature_view_state.py b/sdk/python/tests/unit/test_feature_view_state.py index 3af91b3b469..8e161a080a4 100644 --- a/sdk/python/tests/unit/test_feature_view_state.py +++ b/sdk/python/tests/unit/test_feature_view_state.py @@ -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 @@ -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"]) From 05d2860805d1537c218cbf604af17d9a291be410 Mon Sep 17 00:00:00 2001 From: Alan Gauthier Date: Wed, 2 Sep 2026 16:01:58 +0200 Subject: [PATCH 2/3] test: Increase FIPS offline-server subprocess timeout to avoid CI flake test_module_level_fips_sets_env_before_pyarrow_import spawns a cold subprocess that imports feast.offline_server, pulling in pyarrow and gRPC. On contended CI runners (observed on macOS) this can exceed the 60s timeout and fail with subprocess.TimeoutExpired, even though the import itself is healthy (~3-7s locally). Raise the timeout to 300s to give ample headroom against runner contention while still catching a genuine import hang. Signed-off-by: Alan Gauthier --- sdk/python/tests/unit/test_offline_server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index 321b19bb9b7..d19e1f52baf 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -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. From a31824231470ec793e27a13f296bd391fe141c85 Mon Sep 17 00:00:00 2001 From: Alan Gauthier Date: Thu, 10 Sep 2026 16:58:11 +0200 Subject: [PATCH 3/3] feat: Warn on unsafe serve_features_while_materializing configs Emit advisory (non-fatal) warnings when the flag is combined with a non-sql registry (no-op) or cache_mode='thread' (state transitions may be observed stale). Signed-off-by: Alan Gauthier --- sdk/python/feast/repo_config.py | 24 ++++++++++ .../tests/unit/test_registry_string_config.py | 44 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 3ffbf279966..8fe63337588 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -244,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.""" diff --git a/sdk/python/tests/unit/test_registry_string_config.py b/sdk/python/tests/unit/test_registry_string_config.py index ca337454e52..bd19ab19416 100644 --- a/sdk/python/tests/unit/test_registry_string_config.py +++ b/sdk/python/tests/unit/test_registry_string_config.py @@ -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