← 输入框|github.com|刷新
Skip to content

Entity-less get_historical_features defaults end_date to the host's local time, read as UTC #7036

Open

Description

@luc-pimentel

Expected Behavior

With entity_df=None and no end_date, the window ends at the current time (docs, feature-retrieval.md: "If end_date is omitted, it defaults to the current time"), so the result matches passing end_date=datetime.now(timezone.utc).

Current Behavior

The default end bound is the host's naive local wall-clock time. Offline stores read naive datetimes as UTC, so the window shifts by the host's UTC offset: west of UTC the newest rows are missing, east of UTC rows stamped in the future are returned.

Dask offline store, one feature view with TTL 1 day. Source rows: driver 1 at now−5h (d1_old) and now−1h (d1_recent); driver 2 at now−5h (d2_old) and now+1h (d2_future). Call: store.get_historical_features(entity_df=None, features=["driver_stats:label"]), run under three host time zones.

TZ end_date=datetime.now(timezone.utc) end_date omitted
UTC (1, d1_recent), (2, d2_old) (1, d1_recent), (2, d2_old)
America/Sao_Paulo (UTC−3) (1, d1_recent), (2, d2_old) (1, d1_old), (2, d2_old)
Asia/Tokyo (UTC+9) (1, d1_recent), (2, d2_old) (1, d1_recent), (2, d2_future)

Passing end_date=datetime.now() explicitly returns the same wrong rows as the default. Same result on 0.67.0 and on current master.

Steps to reproduce

feature_repo/feature_store.yaml:

project: end_date_default
registry: data/registry.db
provider: local
online_store:
  type: sqlite
  path: data/online.db
offline_store:
  type: dask
entity_key_serialization_version: 3

feature_repo/features.py:

from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Int64, String

driver = Entity(name="driver", join_keys=["driver_id"])
source = FileSource(path="data/driver_stats.parquet", timestamp_field="event_timestamp")
driver_stats = FeatureView(
    name="driver_stats",
    entities=[driver],
    ttl=timedelta(days=1),
    schema=[Field(name="driver_id", dtype=Int64), Field(name="label", dtype=String)],
    source=source,
)

probe.py (next to feature_repo/):

import sys
from datetime import datetime, timedelta, timezone
import pandas as pd
from feast import FeatureStore

sys.path.insert(0, "feature_repo")
from features import driver, driver_stats

now = datetime.now(timezone.utc).replace(microsecond=0)
pd.DataFrame({
    "driver_id": [1, 1, 2, 2],
    "label": ["d1_old", "d1_recent", "d2_old", "d2_future"],
    "event_timestamp": [now - timedelta(hours=5), now - timedelta(hours=1),
                        now - timedelta(hours=5), now + timedelta(hours=1)],
}).to_parquet("feature_repo/data/driver_stats.parquet")

store = FeatureStore(repo_path="feature_repo")
store.apply([driver, driver_stats])

for label, kwargs in [("explicit utc now", {"end_date": now}), ("end_date omitted", {})]:
    df = store.get_historical_features(entity_df=None, features=["driver_stats:label"], **kwargs).to_df()
    print(label, sorted(zip(df["driver_id"], df["label"])))
mkdir -p feature_repo/data
TZ=UTC python probe.py
TZ=America/Sao_Paulo python probe.py
TZ=Asia/Tokyo python probe.py

Specifications

  • Version: 0.67.0; same on current master
  • Platform: macOS arm64, Python 3.11, pandas 2.3.3, pyarrow 26.0.0, dask 2026.8.0
  • Subsystem: Python SDK, FeatureStore.get_historical_features without entity_df, Dask offline store

Possible Solution

get_historical_features in sdk/python/feast/feature_store.py sets end_date = datetime.now() when entity_df is None and no end_date is given, and compute_non_entity_date_range then treats that naive value as UTC. Using utils._utc_now() there, as #6956 did for materialize and the feature server's /materialize, gives the expected rows in all three zones (checked locally with a unit test). I searched issues and PRs for the symptom and the mechanism and found only #6936/#6956, which did not touch this call site. Happy to open a PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions