Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 1 addition & 21 deletions sdk/python/feast/infra/registry/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ def apply_feature_view(
self._prepare_registry_for_changes(project)
assert self.cached_registry_proto

self._check_conflicting_feature_view_names(feature_view)
self._ensure_feature_view_name_is_unique(feature_view, project)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Cache overwrite in file registry causes loss of uncommitted changes during batch apply

The new _ensure_feature_view_name_is_unique (defined at sdk/python/feast/infra/registry/base_registry.py:277) calls getter methods like self.get_feature_view(name, project, allow_cache=False). In the file registry, these getters call _get_registry_proto(project, allow_cache=False) (sdk/python/feast/infra/registry/registry.py:1292), which when allow_cache=False always re-reads from the store and overwrites self.cached_registry_proto at line 1343. This discards any uncommitted in-memory changes.

The old method _check_conflicting_feature_view_names only read from self.cached_registry_proto directly without triggering a store re-read, so it preserved uncommitted state.

This is triggered in feature_store.py:1199-1202 where apply_feature_view is called in a loop with commit=False after other uncommitted apply_project and apply_data_source calls. The first apply_feature_view call's uniqueness check overwrites the cache, losing all prior uncommitted projects and data sources. The second call loses the first feature view, and so on.

Triggering call site in feature_store.py
for view in itertools.chain(views_to_update, odfvs_to_update, sfvs_to_update):
    self.registry.apply_feature_view(
        view, project=self.project, commit=False, no_promote=no_promote
    )
Prompt for agents
In sdk/python/feast/infra/registry/registry.py at line 758, the call to self._ensure_feature_view_name_is_unique(feature_view, project) uses allow_cache=False (the default), which causes the getter methods inside it to re-read from the registry store and overwrite self.cached_registry_proto, discarding uncommitted changes.

The fix should either:
1. Pass allow_cache=True to _ensure_feature_view_name_is_unique so the getters use the cached proto instead of re-reading from the store: self._ensure_feature_view_name_is_unique(feature_view, project, allow_cache=True)
2. Or, override _ensure_feature_view_name_is_unique in the file Registry class to check the in-memory cached_registry_proto directly (similar to the old _check_conflicting_feature_view_names approach), avoiding any calls to _get_registry_proto.

Option 1 is the simplest fix. The cache is already prepared by _prepare_registry_for_changes at line 755, so allow_cache=True is safe and appropriate here.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

existing_feature_views_of_same_type: RepeatedCompositeFieldContainer
if isinstance(feature_view, StreamFeatureView):
existing_feature_views_of_same_type = (
Expand Down Expand Up @@ -1351,26 +1351,6 @@ def _get_registry_proto(

return registry_proto

def _check_conflicting_feature_view_names(self, feature_view: BaseFeatureView):
name_to_fv_protos = self._existing_feature_view_names_to_fvs()
if feature_view.name in name_to_fv_protos:
if not isinstance(
name_to_fv_protos.get(feature_view.name), feature_view.proto_class
):
raise ConflictingFeatureViewNames(feature_view.name)

def _existing_feature_view_names_to_fvs(self) -> Dict[str, Message]:
assert self.cached_registry_proto
odfvs = {
fv.spec.name: fv
for fv in self.cached_registry_proto.on_demand_feature_views
}
fvs = {fv.spec.name: fv for fv in self.cached_registry_proto.feature_views}
sfv = {
fv.spec.name: fv for fv in self.cached_registry_proto.stream_feature_views
}
return {**odfvs, **fvs, **sfv}

def get_permission(
self, name: str, project: str, allow_cache: bool = False
) -> Permission:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2192,3 +2192,58 @@ def shared_odfv_name(inputs: pd.DataFrame) -> pd.DataFrame:
# Cleanup
test_registry.delete_feature_view("shared_odfv_name", project)
test_registry.teardown()


@pytest.mark.parametrize(
"test_registry",
all_fixtures,
)
def test_cross_project_feature_view_name_allowed(test_registry: BaseRegistry):
"""
Test that different projects can use the same feature view names.
This is a regression test for issue #6209.
"""
project_a = "project_a"
project_b = "project_b"

# Create a FeatureView in project A
feature_view_a = FeatureView(
name="shared_name",
entities=[],
schema=[Field(name="feature1", dtype=Float32)],
source=FileSource(path="data.parquet"),
)

# Create a StreamFeatureView with the same name in project B
stream_feature_view_b = StreamFeatureView(
name="shared_name",
entities=[],
schema=[Field(name="feature2", dtype=Float32)],
source=KafkaSource(
name="kafka_source",
kafka_bootstrap_servers="localhost:9092",
topic="test_topic",
timestamp_field="event_timestamp",
batch_source=FileSource(path="stream_data.parquet"),
),
aggregations=[],
)

# Both should apply successfully without ConflictingFeatureViewNames error
test_registry.apply_feature_view(feature_view_a, project_a)
test_registry.apply_feature_view(stream_feature_view_b, project_b)

# Verify both exist in their respective projects
retrieved_fv_a = test_registry.get_feature_view("shared_name", project_a)
assert retrieved_fv_a.name == "shared_name"
assert isinstance(retrieved_fv_a, FeatureView)
assert not isinstance(retrieved_fv_a, StreamFeatureView)

retrieved_sfv_b = test_registry.get_stream_feature_view("shared_name", project_b)
assert retrieved_sfv_b.name == "shared_name"
assert isinstance(retrieved_sfv_b, StreamFeatureView)

# Cleanup
test_registry.delete_feature_view("shared_name", project_a)
test_registry.delete_feature_view("shared_name", project_b)
test_registry.teardown()