-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(spark): SparkSource query+path and pre-computed offline read for BatchFeatureView #6440
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
f6ff08a
67f647c
cafab0a
7d14b5c
5312075
6794ef2
caecfe4
e7fc883
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 | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -79,6 +79,87 @@ class SparkFeatureViewQueryContext(offline_utils.FeatureViewQueryContext): | |||||||||||||||||||||||||||||||||||||||||||||
| max_date_partition: str | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _apply_bfv_transformations_for_historical( | ||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. I think instead of writing new helper, better approach is to extend the existing |
||||||||||||||||||||||||||||||||||||||||||||||
| spark_session: SparkSession, | ||||||||||||||||||||||||||||||||||||||||||||||
| feature_views: List[FeatureView], | ||||||||||||||||||||||||||||||||||||||||||||||
| query_context: List[offline_utils.FeatureViewQueryContext], | ||||||||||||||||||||||||||||||||||||||||||||||
| ) -> List[offline_utils.FeatureViewQueryContext]: | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| For BatchFeatureViews, redirect get_historical_features to read from the | ||||||||||||||||||||||||||||||||||||||||||||||
| pre-materialized offline store (batch_source.path) when available, avoiding | ||||||||||||||||||||||||||||||||||||||||||||||
| expensive UDF re-execution on raw data. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Precedence: | ||||||||||||||||||||||||||||||||||||||||||||||
| 1. offline=True + batch_source.path set -> read pre-computed parquet | ||||||||||||||||||||||||||||||||||||||||||||||
| 2. Python/pandas UDF present -> execute UDF on raw source (fallback) | ||||||||||||||||||||||||||||||||||||||||||||||
| 3. Otherwise -> pass through unchanged | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| from dataclasses import replace | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| fv_by_name = {fv.projection.name_to_use(): fv for fv in feature_views} | ||||||||||||||||||||||||||||||||||||||||||||||
| new_contexts = [] | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| for ctx in query_context: | ||||||||||||||||||||||||||||||||||||||||||||||
| fv = fv_by_name.get(ctx.name) | ||||||||||||||||||||||||||||||||||||||||||||||
| if fv is None or not isinstance(fv, BatchFeatureView): | ||||||||||||||||||||||||||||||||||||||||||||||
| new_contexts.append(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| getattr(fv, "offline", False) | ||||||||||||||||||||||||||||||||||||||||||||||
| and isinstance(fv.batch_source, SparkSource) | ||||||||||||||||||||||||||||||||||||||||||||||
| and fv.batch_source.path | ||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||
| tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}" | ||||||||||||||||||||||||||||||||||||||||||||||
| file_format = fv.batch_source.file_format or "parquet" | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| df = spark_session.read.format(file_format).load(fv.batch_source.path) | ||||||||||||||||||||||||||||||||||||||||||||||
| df.createOrReplaceTempView(tmp_view) | ||||||||||||||||||||||||||||||||||||||||||||||
| ctx = replace(ctx, table_subquery=tmp_view) | ||||||||||||||||||||||||||||||||||||||||||||||
| new_contexts.append(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+119
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] Path traversal vulnerability in file loading The code directly loads files from fv.batch_source.path without any validation. This creates a path traversal security vulnerability where malicious paths could access unauthorized files on the system. Suggested:
Suggested change
Contributor
Author
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. Thanks for flagging this. fv.batch_source.path is a trusted configuration value set by the feature store admin at definition time — it's not runtime user input flowing from an API boundary, so path traversal in the traditional web security sense doesn't apply here.
Contributor
Author
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. Also, the suggested fix using os.path.normpath() would silently corrupt S3/GCS URIs (s3://bucket/path - s3:/bucket/path) since normpath collapses double slashes. Happy to add a lightweight guard for local paths only if you'd like, but I'd keep it separate from object storage paths.
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. Make Sense. |
||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| except (FileNotFoundError, PermissionError) as e: | ||||||||||||||||||||||||||||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Offline path '{fv.batch_source.path}' not accessible for " | ||||||||||||||||||||||||||||||||||||||||||||||
| f"'{ctx.name}': {e}; falling back to source query.", | ||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeWarning, | ||||||||||||||||||||||||||||||||||||||||||||||
| stacklevel=2, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||
| warnings.warn( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"Unexpected error loading offline path '{fv.batch_source.path}' " | ||||||||||||||||||||||||||||||||||||||||||||||
| f"for '{ctx.name}': {e}; falling back to source query.", | ||||||||||||||||||||||||||||||||||||||||||||||
| RuntimeWarning, | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+132
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] Overly broad exception handling masks specific errors Catching all exceptions with 'except Exception:' is too broad and could mask important errors like permission issues, file corruption, or configuration problems. This makes debugging difficult and could hide security issues.
Suggested change
Contributor
Author
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. Good catch — updated to catch |
||||||||||||||||||||||||||||||||||||||||||||||
| stacklevel=2, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| hasattr(fv, "feature_transformation") | ||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. Use has_transformation() and get_transformation_function() instead |
||||||||||||||||||||||||||||||||||||||||||||||
| and fv.feature_transformation is not None | ||||||||||||||||||||||||||||||||||||||||||||||
| and ( | ||||||||||||||||||||||||||||||||||||||||||||||
| getattr(fv.feature_transformation, "mode", None) in ("python", "pandas") | ||||||||||||||||||||||||||||||||||||||||||||||
| or getattr( | ||||||||||||||||||||||||||||||||||||||||||||||
| getattr(fv.feature_transformation, "mode", None), "value", None | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| in ("python", "pandas") | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||
| udf = getattr(fv.feature_transformation, "udf", None) or getattr( | ||||||||||||||||||||||||||||||||||||||||||||||
| fv, "udf", None | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| if udf is not None: | ||||||||||||||||||||||||||||||||||||||||||||||
| temp_view_name = f"__feast_bfv_{ctx.name}_{uuid.uuid4().hex[:8]}" | ||||||||||||||||||||||||||||||||||||||||||||||
| spark_session.conf.set("spark.sql.runSQLOnFiles", "true") | ||||||||||||||||||||||||||||||||||||||||||||||
| raw_df = spark_session.sql(f"SELECT * FROM {ctx.table_subquery}") | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+150
to
+153
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] UDF execution without sandboxing poses security risks Executing user-defined functions without proper sandboxing or validation could allow arbitrary code execution. This is a significant security risk in production environments. Suggested:
Suggested change
Contributor
Author
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. The UDF here is |
||||||||||||||||||||||||||||||||||||||||||||||
| transformed_df = udf(raw_df) | ||||||||||||||||||||||||||||||||||||||||||||||
| transformed_df.createOrReplaceTempView(temp_view_name) | ||||||||||||||||||||||||||||||||||||||||||||||
| ctx = replace(ctx, table_subquery=temp_view_name) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| new_contexts.append(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return new_contexts | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class SparkOfflineStore(OfflineStore): | ||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| def pull_latest_from_table_or_query( | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -261,8 +342,10 @@ def get_historical_features( | |||||||||||||||||||||||||||||||||||||||||||||
| entity_df_event_timestamp_range, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| query_context = _apply_bfv_transformations( | ||||||||||||||||||||||||||||||||||||||||||||||
| spark_session, feature_views, query_context | ||||||||||||||||||||||||||||||||||||||||||||||
| query_context = _apply_bfv_transformations_for_historical( | ||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. so previous |
||||||||||||||||||||||||||||||||||||||||||||||
| spark_session=spark_session, | ||||||||||||||||||||||||||||||||||||||||||||||
| feature_views=feature_views, | ||||||||||||||||||||||||||||||||||||||||||||||
| query_context=query_context, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| spark_query_context = [ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.