Skip to content

fix: Feast apply silently ignoring ttl updates to None or timedelta(0) - #6709

Merged
ntkathole merged 4 commits into
feast-dev:masterfrom
saket3395:fix/ttl-clear-falsy-guard
Aug 14, 2026
Merged

fix: Feast apply silently ignoring ttl updates to None or timedelta(0)#6709
ntkathole merged 4 commits into
feast-dev:masterfrom
saket3395:fix/ttl-clear-falsy-guard

Conversation

@saket3395

Copy link
Copy Markdown
Contributor

Summary

Fixes #6703 — re-applying an existing FeatureView/LabelView with ttl=None or ttl=timedelta(0) (both the documented way to express "no ttl") silently keeps the old finite ttl in the registry. feast apply reports the change on every run, but nothing actually updates.

Root cause (as diagnosed in the issue): Registry._update_metadata_fields() routes ttl changes through a truthiness check:

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, the whole ttl-update block is skipped exactly when it should clear the ttl.

There's a second bug in the same block that the outer-gate fix alone doesn't cover: for the FeatureView branch, get_ttl_duration() returns Python None when self.ttl is None:

def get_ttl_duration(self):
    ttl_duration = None
    if self.ttl is not None:
        ttl_duration = Duration()
        ttl_duration.FromTimedelta(self.ttl)
    return ttl_duration

and the existing inner check in _update_metadata_fields:

ttl_duration = updated_fv.get_ttl_duration()
if ttl_duration:
    existing_proto.spec.ttl.CopyFrom(ttl_duration)

would still skip CopyFrom when ttl_duration is None — so removing only the outer gate fixes the timedelta(0) case but not ttl=None for FeatureView.

Fix

  • Removed the outer truthy gate; kept only the hasattr checks.
  • For FeatureView: always CopyFrom — using an explicit empty Duration() when get_ttl_duration() returns None, instead of skipping the write. An empty Duration decodes back to timedelta(0) via FeatureView.from_proto's existing ToNanoseconds() == 0 check, so this correctly round-trips as "no ttl".
  • For LabelView: guarded FromTimedelta() against ttl is None (previously unreachable because the outer gate prevented ttl=None from reaching this branch at all; now that the gate is gone, it needs its own guard) — falls through to the default zero Duration() in that case.

Verification

Traced all three cases (None, timedelta(0), a finite value) through the new branch logic in isolation (a minimal stand-in mirroring the real Duration/get_ttl_duration control flow, since protobuf isn't installed in this environment and I didn't want to add it just to verify branch logic):

ttl input FeatureView branch LabelView branch
None zero Duration zero Duration
timedelta(0) zero Duration zero Duration
timedelta(days=10) 864000s Duration 864000s Duration

Both branches now resolve identically for all three cases, and confirmed via feature_view.py's from_proto that a zero-value Duration round-trips to timedelta(0), matching the codebase's existing "no ttl" convention.

Test plan

  • Traced all three ttl cases through the new logic in isolation, for both FeatureView and LabelView.
  • Didn't run this against feast's actual test suite / a live feast apply round-trip in this environment (protobuf not installed here, didn't want to pull in the dependency just to verify this). Flagging for CI/maintainer verification — happy to iterate if anything surfaces.

@saket3395
saket3395 requested a review from a team as a code owner August 6, 2026 06:37

@jyejare jyejare left a comment

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.

LGTM, some nitpicks.

# are falsy, so that check would silently drop the update exactly
# when a user clears an existing ttl.
if hasattr(existing_proto.spec, "ttl") and hasattr(updated_fv, "ttl"):
from google.protobuf.duration_pb2 import Duration

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 moving import to top of file

The Duration import is now used in both branches of the conditional. Moving it to the top of the file would be more conventional and slightly more efficient.

Suggested:

Suggested change
from google.protobuf.duration_pb2 import Duration
# Move this import to the top of the file with other imports
from google.protobuf.duration_pb2 import Duration

Comment on lines 596 to +597
ttl_duration = Duration()
ttl_duration.FromTimedelta(updated_fv.ttl)
if updated_fv.ttl is not None:

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 error handling for invalid timedelta

FromTimedelta could potentially raise an exception for invalid timedelta values. Consider adding basic validation or error handling to provide better user feedback.

Suggested:

Suggested change
ttl_duration = Duration()
ttl_duration.FromTimedelta(updated_fv.ttl)
if updated_fv.ttl is not None:
ttl_duration = Duration()
if updated_fv.ttl is not None:
try:
ttl_duration.FromTimedelta(updated_fv.ttl)
except (ValueError, OverflowError) as e:
raise ValueError(f"Invalid TTL value: {updated_fv.ttl}") from e

@saket3395

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @jyejare! Addressed both: moved the Duration import to the top of the file with the other google.protobuf imports, and wrapped FromTimedelta() in a try/except that re-raises as a ValueError naming the offending value.

@codecov-commenter

codecov-commenter commented Aug 11, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 25.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.80%. Comparing base (a9219d9) to head (7dcb610).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
sdk/python/feast/infra/registry/registry.py 25.00% 5 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #6709      +/-   ##
==========================================
- Coverage   46.80%   46.80%   -0.01%     
==========================================
  Files         415      415              
  Lines       50395    50398       +3     
  Branches     7214     7214              
==========================================
  Hits        23588    23588              
- Misses      25155    25158       +3     
  Partials     1652     1652              
Flag Coverage Δ
go-feature-server 30.58% <ø> (ø)
python-unit 48.13% <25.00%> (-0.01%) ⬇️
Files with missing lines Coverage Δ
sdk/python/feast/infra/registry/registry.py 62.00% <25.00%> (-0.11%) ⬇️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a9219d9...7dcb610. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntkathole

Copy link
Copy Markdown
Member

@saket3395 can we have test coverage for this ?

@saket3395

Copy link
Copy Markdown
Contributor Author

@ntkathole added unit coverage in sdk/python/tests/unit/infra/registry/test_update_metadata_fields.py:

  • clearing a finite ttl to None or timedelta(0) now writes a zero Duration (parametrized over both) — this is the regression that was silently dropped before
  • a finite→finite ttl update is preserved

Since _update_metadata_fields uses no instance state, the test exercises it directly via the class, mirroring the FeatureView/FileSource/Field construction used elsewhere in the unit tests. I wasn't able to run the suite in my local environment, so I'd appreciate CI confirming it — happy to adjust if anything needs tweaking.

@ntkathole ntkathole changed the title Fix feast apply silently ignoring ttl updates to None or timedelta(0) Fix: Feast apply silently ignoring ttl updates to None or timedelta(0) Aug 14, 2026
@ntkathole
ntkathole force-pushed the fix/ttl-clear-falsy-guard branch from 970b048 to 2c8a95d Compare August 14, 2026 03:21
@ntkathole

Copy link
Copy Markdown
Member

@saket3395 Please sign the commit and fix the linting checks

@saket3395
saket3395 force-pushed the fix/ttl-clear-falsy-guard branch from 2c8a95d to 7dcb610 Compare August 14, 2026 03:39
@saket3395

Copy link
Copy Markdown
Contributor Author

@ntkathole done — all commits are now DCO signed-off (DCO check is green), and I fixed the ruff format issue (the multi-line raise ValueError in the ttl block collapses to a single line under the 88-char limit). Ran ruff check and ruff format --check locally on both the changed source and the new test file — all clean now. Let me know if anything else is needed.

@saket3395 saket3395 changed the title Fix: Feast apply silently ignoring ttl updates to None or timedelta(0) fix: Feast apply silently ignoring ttl updates to None or timedelta(0) Aug 14, 2026
@saket3395

Copy link
Copy Markdown
Contributor Author

Fixed the lint-pr failure too — the "Validate PR title" job wanted a conventional-commit type, so I changed the title from Fix: to lowercase fix: (commitlint flagged type-case + type-enum). The re-run is showing action_required (pending maintainer approval to run on this fork PR) — should go green once it's approved or re-triggered.

saket3395 added 4 commits August 14, 2026 10:39
…a(0)

Fixes feast-dev#6703. Registry._update_metadata_fields() routes ttl changes on
re-apply through a truthiness check:

    if (... and updated_fv.ttl):

None and timedelta(0) are both the documented way to express "no
ttl", and both are falsy, so re-applying a FeatureView/LabelView with
ttl cleared silently kept the old finite ttl -- feast apply reported
the update but nothing changed in the registry.

Removing that outer truthy gate isn't sufficient by itself: for the
FeatureView branch, get_ttl_duration() returns Python None when
self.ttl is None, and the existing inner check

    if ttl_duration:
        existing_proto.spec.ttl.CopyFrom(ttl_duration)

would still silently skip CopyFrom in that case, leaving the stale
ttl in place. Fixed both by explicitly writing an empty Duration()
(which decodes back to timedelta(0), per FeatureView.from_proto's
existing ToNanoseconds()==0 check) whenever there's no real ttl to
write, instead of skipping the write.

The LabelView branch is adjusted the same way, guarding
FromTimedelta() against a None ttl now that the outer gate no longer
prevents ttl=None from reaching this branch.

Traced all three cases (None, timedelta(0), a finite value) through
the new branch logic in isolation and confirmed FeatureView and
LabelView now resolve identically: None and timedelta(0) both produce
a zero Duration, a finite ttl passes through unchanged.

Signed-off-by: saket3395 <sakettulsan95@gmail.com>
- Move the Duration import to the top of the file with the other
  google.protobuf imports, consistent with how Message and
  RepeatedCompositeFieldContainer are already imported there.
- Wrap FromTimedelta() in the LabelView branch with a try/except,
  re-raising as a ValueError naming the offending value, so an
  invalid timedelta surfaces a clear error instead of a raw
  protobuf exception.

Signed-off-by: saket3395 <sakettulsan95@gmail.com>
Adds unit coverage requested in review:
- clearing a finite ttl to None or timedelta(0) now writes a zero
  Duration (previously silently dropped)
- a finite-to-finite ttl update is preserved

_update_metadata_fields uses no instance state, so it is exercised
directly via the class, mirroring the FeatureView/FileSource/Field
construction used elsewhere in the unit tests.

Signed-off-by: saket3395 <sakettulsan95@gmail.com>
Collapse the multi-line raise ValueError back to a single line per
ruff format (it fits within the 88-char limit), fixing the format
check.

Signed-off-by: saket3395 <sakettulsan95@gmail.com>
@ntkathole
ntkathole force-pushed the fix/ttl-clear-falsy-guard branch from 7dcb610 to b34897a Compare August 14, 2026 05:09
@ntkathole
ntkathole merged commit 97b0f25 into feast-dev:master Aug 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants