Skip to content

feast apply silently ignores ttl updates when the new ttl is None or timedelta(0) #6703

Description

@erald02

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions