-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Add remote materialization support via feature server #6590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
44d9a11
82ce777
3bf35a2
b6fe54d
6f073fb
cf87b03
ef9ff5e
a1a316e
c1f53c9
4b302d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -90,12 +90,14 @@ class MaterializeRequest(BaseModel): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| feature_views: Optional[List[str]] = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disable_event_timestamp: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_feature_names: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version: Optional[str] = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MaterializeIncrementalRequest(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end_ts: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| feature_views: Optional[List[str]] = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_feature_names: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version: Optional[str] = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class GetOnlineFeaturesRequest(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -708,6 +710,152 @@ async def materialize_incremental(request: MaterializeIncrementalRequest) -> Non | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_feature_names=request.full_feature_names, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _authorize_materialize_views( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_feature_views: Optional[List[str]], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> List[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Shared authz + FV name resolution for async materialize endpoints.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| feature_views_to_materialize = store._get_feature_views_to_materialize( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_feature_views | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for fv in feature_views_to_materialize: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_permissions( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resource=fv, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actions=[AuthzedAction.WRITE_ONLINE], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [fv.name for fv in feature_views_to_materialize] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _parse_materialize_timestamps(request: MaterializeRequest): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Parse and validate timestamps from a materialize request.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if request.disable_event_timestamp: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now = datetime.now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return datetime(1970, 1, 1), now | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not request.start_ts or not request.end_ts: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "start_ts and end_ts are required when disable_event_timestamp is False" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start_date = utils.make_tzaware(parser.parse(request.start_ts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end_date = utils.make_tzaware(parser.parse(request.end_ts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (ValueError, TypeError) as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f"Invalid timestamp format: {e}") from e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if start_date >= end_date: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"start_ts ({start_date}) must be before end_ts ({end_date})" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return start_date, end_date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from feast.feature_view import FeatureViewState | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _check_already_materializing(fv_names: List[str]) -> Optional[JSONResponse]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return 409 if any requested FV is already materializing.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for fv_name in fv_names: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv = store.registry.get_feature_view( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv_name, store.project, allow_cache=False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if getattr(fv, "state", None) == FeatureViewState.MATERIALIZING: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSONResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status_code=409, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error": f"Feature view '{fv_name}' is already materializing" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+751
to
+767
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] Function defined inside endpoint handler with closure dependencies Similar to the previous issue, Suggested:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _update_fv_state(fv_names: List[str], state) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Update FV state in the registry.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for fv_name in fv_names: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv = store.registry.get_feature_view( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv_name, store.project, allow_cache=False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv.state = state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store.registry.apply_feature_view(fv, store.project) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"Failed to set state={state} for {fv_name}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.post("/materialize-async", dependencies=[Depends(inject_user_details)]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def materialize_async(request: MaterializeRequest) -> JSONResponse: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with feast_metrics.track_request_latency("/materialize-async"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv_names = _authorize_materialize_views(request.feature_views) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start_date, end_date = _parse_materialize_timestamps(request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conflict = _check_already_materializing(fv_names) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if conflict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return conflict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _update_fv_state(fv_names, FeatureViewState.MATERIALIZING) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _run_materialize(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store.materialize( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+780
to
+795
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Warning] Materialize async endpoint lacks proper error handling The Suggested:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start_date, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end_date, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.feature_views, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disable_event_timestamp=request.disable_event_timestamp, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_feature_names=request.full_feature_names, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version=request.version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _force_local=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Async materialization failed for {fv_names}: {e}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exc_info=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _update_fv_state(fv_names, FeatureViewState.GENERATED) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loop.run_in_executor(None, _run_materialize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSONResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status_code=202, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content={"status": "accepted", "feature_views": fv_names}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.post( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/materialize-incremental-async", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dependencies=[Depends(inject_user_details)], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def materialize_incremental_async( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request: MaterializeIncrementalRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> JSONResponse: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with feast_metrics.track_request_latency("/materialize-incremental-async"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fv_names = _authorize_materialize_views(request.feature_views) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conflict = _check_already_materializing(fv_names) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if conflict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return conflict | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _update_fv_state(fv_names, FeatureViewState.MATERIALIZING) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _run_materialize_incremental(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store.materialize_incremental( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils.make_tzaware(parser.parse(request.end_ts)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.feature_views, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| full_feature_names=request.full_feature_names, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version=request.version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _force_local=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"Async incremental materialization failed for {fv_names}: {e}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exc_info=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _update_fv_state(fv_names, FeatureViewState.GENERATED) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loop.run_in_executor(None, _run_materialize_incremental) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSONResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status_code=202, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content={"status": "accepted", "feature_views": fv_names}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @app.exception_handler(Exception) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def rest_exception_handler(request: Request, exc: Exception): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Print the original exception on the server side | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] Function defined inside endpoint handler scope
The function
_authorize_materialize_viewsis defined inside the endpoint handler scope but referencesstorefrom the outer scope. This creates a closure that captures thestorevariable, which could lead to unexpected behavior if the store changes or if this function is called in different contexts. This pattern violates the principle of explicit dependencies and makes testing difficult.Suggested: