Expected Behavior
Re-applying an existing FeatureView with ttl=None or ttl=timedelta(0) should clear the ttl in the registry. After feast apply, store.get_feature_view("foo_fv").ttl should reflect the new "no expiry" value.
Current Behavior
The old ttl is silently retained. store.get_feature_view("foo_fv").ttl still returns the previous finite value, with no warning or error, while feast apply keeps printing the same "changed" diff on every run.
Registry.apply_feature_view() routes ttl changes through _update_metadata_fields() instead of creating a new version, and that method gates the ttl copy behind a truthiness check on the raw timedelta:
if (
hasattr(existing_proto.spec, "ttl")
and hasattr(updated_fv, "ttl")
and updated_fv.ttl # falsy for both None and timedelta(0)
):
Since None and timedelta(0) are both falsy — and both are the documented way to express "no ttl" — the update is dropped exactly in the case it should support. get_ttl_duration() already handles None correctly; only the outer guard is wrong. Still present on master as of 2026-08.
Practical impact: a FeatureView intended to never expire silently kept a stale finite ttl, causing get_historical_features to drop ~11.6% of otherwise-valid rows from a point-in-time join, with no error or warning.
Steps to reproduce
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource, Project
from feast.types import Float32
project = Project(name="ttl_repro")
entity = Entity(name="foo", join_keys=["foo_id"])
source = FileSource(
name="foo_source",
path="./data/foo.parquet",
timestamp_field="event_timestamp",
)
# 1. Apply with a finite ttl -> fv.ttl == 10 days (correct)
fv = FeatureView(
name="foo_fv",
entities=[entity],
ttl=timedelta(days=10),
schema=[Field(name="bar", dtype=Float32)],
source=source,
)
# 2. Change ttl to "no expiry" and `feast apply` again
fv_v2 = FeatureView(
name="foo_fv",
entities=[entity],
ttl=None, # or timedelta(days=0) - same result
schema=[Field(name="bar", dtype=Float32)],
source=source,
)
Then check store.get_feature_view("foo_fv").ttl — it is still timedelta(days=10).
Specifications
- Version: 0.65.0 (PyPI); also confirmed on
master as of 2026-08
- Platform:
- Subsystem: Registry —
sdk/python/feast/infra/registry/registry.py, Registry._update_metadata_fields()
Possible Solution
Drop the outer truthiness check and always sync ttl on update, relying on get_ttl_duration()'s existing None-handling and defaulting to an explicit zero Duration when unset:
if hasattr(existing_proto.spec, "ttl") and hasattr(updated_fv, "ttl"):
if isinstance(updated_fv, FeatureView):
existing_proto.spec.ttl.CopyFrom(updated_fv.get_ttl_duration() or Duration())
elif isinstance(updated_fv, LabelView):
ttl_duration = Duration()
if updated_fv.ttl is not None:
ttl_duration.FromTimedelta(updated_fv.ttl)
existing_proto.spec.ttl.CopyFrom(ttl_duration)
Happy to open a PR with this plus a regression test (apply with finite ttl, re-apply with ttl=None, assert the registry reflects it)
Expected Behavior
Re-applying an existing
FeatureViewwithttl=Noneorttl=timedelta(0)should clear the ttl in the registry. Afterfeast apply,store.get_feature_view("foo_fv").ttlshould reflect the new "no expiry" value.Current Behavior
The old ttl is silently retained.
store.get_feature_view("foo_fv").ttlstill returns the previous finite value, with no warning or error, whilefeast applykeeps printing the same "changed" diff on every run.Registry.apply_feature_view()routes ttl changes through_update_metadata_fields()instead of creating a new version, and that method gates the ttl copy behind a truthiness check on the rawtimedelta:Since
Noneandtimedelta(0)are both falsy — and both are the documented way to express "no ttl" — the update is dropped exactly in the case it should support.get_ttl_duration()already handlesNonecorrectly; only the outer guard is wrong. Still present onmasteras of 2026-08.Practical impact: a
FeatureViewintended to never expire silently kept a stale finite ttl, causingget_historical_featuresto drop ~11.6% of otherwise-valid rows from a point-in-time join, with no error or warning.Steps to reproduce
Then check
store.get_feature_view("foo_fv").ttl— it is stilltimedelta(days=10).Specifications
masteras of 2026-08sdk/python/feast/infra/registry/registry.py,Registry._update_metadata_fields()Possible Solution
Drop the outer truthiness check and always sync ttl on update, relying on
get_ttl_duration()'s existingNone-handling and defaulting to an explicit zeroDurationwhen unset:Happy to open a PR with this plus a regression test (apply with finite ttl, re-apply with
ttl=None, assert the registry reflects it)