This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new d5aed746ad9 Avoid Vertex AI Feature Store system Dag import timeout
(#69823)
d5aed746ad9 is described below
commit d5aed746ad90ed731702ceaf0f87cb43d01a0d28
Author: Vic Wen <[email protected]>
AuthorDate: Sun Oct 18 04:07:03 2026 +0800
Avoid Vertex AI Feature Store system Dag import timeout (#69823)
* Avoid Vertex AI Feature Store system Dag import timeout
The Vertex AI client package performs expensive third-party imports during
Dag discovery, pushing the system example beyond the default 30-second limit
and exposing circular-import failures. Keeping parsing lightweight prevents
system Dag scans from failing before tasks can run.
Signed-off-by: viiccwen <[email protected]>
* Pass the Bigtable online store config as a dictionary
Rebasing onto #71238 kept its Bigtable autoscaling config as a typed
FeatureOnlineStore message, which brought back the aiplatform_v1beta1
import into the system Dag. Express it as a dictionary so Dag parsing no
longer imports the package, and document that the Feature Store operators
accept dictionaries.
---------
Signed-off-by: viiccwen <[email protected]>
Co-authored-by: Shahar Epstein <[email protected]>
---
.../google/docs/operators/cloud/vertex_ai.rst | 5 +++
.../google/cloud/hooks/vertex_ai/feature_store.py | 31 ++++++++++-------
.../cloud/operators/vertex_ai/feature_store.py | 17 ++++-----
.../vertex_ai/example_vertex_ai_feature_store.py | 36 +++++++++----------
.../cloud/hooks/vertex_ai/test_feature_store.py | 40 +++++++++++++++++++++-
5 files changed, 88 insertions(+), 41 deletions(-)
diff --git a/providers/google/docs/operators/cloud/vertex_ai.rst
b/providers/google/docs/operators/cloud/vertex_ai.rst
index 6cfd165fe53..e4fd2c3f70e 100644
--- a/providers/google/docs/operators/cloud/vertex_ai.rst
+++ b/providers/google/docs/operators/cloud/vertex_ai.rst
@@ -676,6 +676,11 @@ To create feature online store you can use
:class:`~airflow.providers.google.cloud.operators.vertex_ai.feature_store.CreateFeatureOnlineStoreOperator`.
The operator creation results in :ref:`XCom <concepts:xcom>` under
``return_value`` key.
+The ``feature_online_store``, ``feature_view`` and ``data_key`` parameters of
the Feature Store operators
+accept either the ``google.cloud.aiplatform_v1beta1`` message objects or
equivalent dictionaries.
+Dictionaries keep ``google.cloud.aiplatform_v1beta1`` out of Dag parsing,
which can otherwise
+exceed ``dagbag_import_timeout``.
+
.. exampleinclude::
/../../google/tests/system/google/cloud/vertex_ai/example_vertex_ai_feature_store.py
:language: python
:dedent: 4
diff --git
a/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/feature_store.py
b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/feature_store.py
index 17a1b6b7ad1..e5bbc84bda7 100644
---
a/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/feature_store.py
+++
b/providers/google/src/airflow/providers/google/cloud/hooks/vertex_ai/feature_store.py
@@ -19,15 +19,9 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import (
- TYPE_CHECKING,
-)
+from typing import TYPE_CHECKING
from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault
-from google.cloud.aiplatform_v1beta1 import (
- FeatureOnlineStoreAdminServiceClient,
- FeatureOnlineStoreServiceClient,
-)
from airflow.providers.common.compat.sdk import AirflowException
from airflow.providers.google.common.consts import CLIENT_INFO
@@ -36,6 +30,10 @@ from airflow.providers.google.common.hooks.base_google
import PROVIDE_PROJECT_ID
if TYPE_CHECKING:
from google.api_core.operation import Operation
from google.api_core.retry import Retry
+ from google.cloud.aiplatform_v1beta1 import (
+ FeatureOnlineStoreAdminServiceClient,
+ FeatureOnlineStoreServiceClient,
+ )
from google.cloud.aiplatform_v1beta1.types import (
FeatureOnlineStore,
FeatureView,
@@ -90,6 +88,8 @@ class FeatureStoreHook(GoogleBaseHook):
If provided and not 'global', the client will be configured to use
the
region-specific API endpoint.
"""
+ from google.cloud.aiplatform_v1beta1 import
FeatureOnlineStoreAdminServiceClient
+
return FeatureOnlineStoreAdminServiceClient(
credentials=self.get_credentials(),
client_info=CLIENT_INFO,
@@ -101,6 +101,8 @@ class FeatureStoreHook(GoogleBaseHook):
location: str | None = None,
custom_endpoint: str | None = None,
) -> FeatureOnlineStoreServiceClient:
+ from google.cloud.aiplatform_v1beta1 import
FeatureOnlineStoreServiceClient
+
return FeatureOnlineStoreServiceClient(
credentials=self.get_credentials(),
client_info=CLIENT_INFO,
@@ -113,7 +115,7 @@ class FeatureStoreHook(GoogleBaseHook):
def create_feature_online_store(
self,
feature_online_store_id: str,
- feature_online_store: FeatureOnlineStore,
+ feature_online_store: FeatureOnlineStore | dict,
project_id: str = PROVIDE_PROJECT_ID,
location: str | None = None,
timeout: float | _MethodDefault = DEFAULT,
@@ -127,7 +129,8 @@ class FeatureStoreHook(GoogleBaseHook):
Feature Online Store aims to serve and manage features data as a part
of VertexAI MLOps.
:param feature_online_store_id: The ID of the online feature store.
- :param feature_online_store: The configuration of the online
repository.
+ :param feature_online_store: The configuration of the online
repository as a
+ FeatureOnlineStore object or dictionary.
:param project_id: The ID of the Google Cloud project that contains the
feature store. If not provided, will attempt to determine from the
environment.
:param location: The Google Cloud region where the feature store is
located
@@ -196,7 +199,7 @@ class FeatureStoreHook(GoogleBaseHook):
def create_feature_view(
self,
feature_view_id: str,
- feature_view: FeatureView,
+ feature_view: FeatureView | dict,
feature_online_store_id: str,
project_id: str = PROVIDE_PROJECT_ID,
run_sync_immediately: bool = False,
@@ -215,7 +218,8 @@ class FeatureStoreHook(GoogleBaseHook):
become the final component of the FeatureView's resource name.
This value may be up to 60 characters, and valid characters are
``[a-z0-9_]``.
The first character cannot be a number. The value must be unique
within a FeatureOnlineStore.
- :param feature_view: The configuration of the FeatureView to create.
+ :param feature_view: The configuration of the FeatureView to create as
a FeatureView object
+ or dictionary.
:param feature_online_store_id: The ID of the online feature store.
:param run_sync_immediately: If set to true, one on demand sync will
be run
immediately, regardless the FeatureView.sync_config.
@@ -327,7 +331,7 @@ class FeatureStoreHook(GoogleBaseHook):
entity_id: str | None = None,
project_id: str = PROVIDE_PROJECT_ID,
endpoint_domain_name: str | None = None,
- data_key: FeatureViewDataKey | None = None,
+ data_key: FeatureViewDataKey | dict | None = None,
data_format: int | None = None,
location: str | None = None,
timeout: float | _MethodDefault = DEFAULT,
@@ -346,7 +350,8 @@ class FeatureStoreHook(GoogleBaseHook):
and default feature store endpoint will be used, based on location
provided.
:param feature_view_id: The FeatureView ID to fetch data from.
:param feature_online_store_id: The ID of the online feature store.
- :param data_key: Optional. The request key to fetch feature values for.
+ :param data_key: Optional. The request key to fetch feature values for
as a
+ FeatureViewDataKey object or dictionary.
:param data_format: Optional. Response data format. If not set,
FeatureViewDataFormat.KEY_VALUE
will be used.
:param project_id: The ID of the Google Cloud project that contains the
diff --git
a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/feature_store.py
b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/feature_store.py
index 64fcdf38b09..0d3aa58d8ce 100644
---
a/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/feature_store.py
+++
b/providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/feature_store.py
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Any
from google.api_core.exceptions import GoogleAPICallError
from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault
-from google.cloud.aiplatform_v1beta1.types import FeatureViewDataFormat
from airflow.providers.common.compat.sdk import AirflowException
from airflow.providers.google.cloud.hooks.vertex_ai.feature_store import
FeatureStoreHook
@@ -128,8 +127,8 @@ class
CreateFeatureOnlineStoreOperator(GoogleCloudBaseOperator, OperationHelper)
This specifies the Google Cloud region where the feature store
resources are located.
:param feature_online_store_id: Required. The ID of the online feature
store that contains
the feature view to be synchronized. This store serves as the online
serving layer.
- :param feature_online_store: FeatureOnlineStore configuration object of
the feature online store
- to be created.
+ :param feature_online_store: FeatureOnlineStore configuration object or
dictionary of the
+ feature online store to be created.
:param gcp_conn_id: The connection ID to use for connecting to Google
Cloud Platform.
Defaults to 'google_cloud_default'.
:param impersonation_chain: Optional service account to impersonate using
short-term
@@ -155,7 +154,7 @@ class
CreateFeatureOnlineStoreOperator(GoogleCloudBaseOperator, OperationHelper)
project_id: str,
location: str,
feature_online_store_id: str,
- feature_online_store: FeatureOnlineStore,
+ feature_online_store: FeatureOnlineStore | dict,
timeout: float | _MethodDefault = DEFAULT,
retry: Retry | _MethodDefault | None = DEFAULT,
metadata: Sequence[tuple[str, str]] = (),
@@ -289,7 +288,7 @@ class CreateFeatureViewOperator(GoogleCloudBaseOperator,
OperationHelper):
of the FeatureView's resource name. This value may be up to 60
characters, and valid characters
are ``[a-z0-9_]``. The first character cannot be a number.
The value must be unique within a FeatureOnlineStore.
- :param feature_view: The configuration of the FeatureView to create.
+ :param feature_view: The FeatureView configuration object or dictionary to
create.
:param feature_online_store_id: The ID of the online feature store.
:param run_sync_immediately: If set to true, one on demand sync will be run
immediately, regardless the FeatureView.sync_config.
@@ -320,7 +319,7 @@ class CreateFeatureViewOperator(GoogleCloudBaseOperator,
OperationHelper):
self,
*,
feature_view_id: str,
- feature_view: FeatureView,
+ feature_view: FeatureView | dict,
feature_online_store_id: str,
run_sync_immediately: bool = False,
project_id: str,
@@ -442,7 +441,7 @@ class FetchFeatureValuesOperator(GoogleCloudBaseOperator,
OperationHelper):
:param entity_id: Simple ID to identify Entity to fetch feature values for.
:param feature_view_id: The FeatureView ID to fetch data from.
:param feature_online_store_id: The ID of the online feature store.
- :param data_key: The request key to fetch feature values for.
+ :param data_key: The FeatureViewDataKey object or dictionary used to fetch
feature values.
:param project_id: Required. The ID of the Google Cloud project that
contains the feature store.
This is used to identify which project's resources to interact with.
:param location: Required. The location of the feature store (e.g.,
'us-central1', 'us-east1').
@@ -476,7 +475,7 @@ class FetchFeatureValuesOperator(GoogleCloudBaseOperator,
OperationHelper):
project_id: str,
location: str,
entity_id: str | None = None,
- data_key: FeatureViewDataKey | None = None,
+ data_key: FeatureViewDataKey | dict | None = None,
timeout: float | _MethodDefault = DEFAULT,
retry: Retry | _MethodDefault | None = DEFAULT,
metadata: Sequence[tuple[str, str]] = (),
@@ -499,6 +498,8 @@ class FetchFeatureValuesOperator(GoogleCloudBaseOperator,
OperationHelper):
def execute(self, context: Context) -> dict[str, Any]:
"""Execute the get feature view sync operation."""
+ from google.cloud.aiplatform_v1beta1.types import FeatureViewDataFormat
+
hook = FeatureStoreHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
diff --git
a/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_feature_store.py
b/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_feature_store.py
index 52e8b649064..bb35a86ca81 100644
---
a/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_feature_store.py
+++
b/providers/google/tests/system/google/cloud/vertex_ai/example_vertex_ai_feature_store.py
@@ -25,8 +25,6 @@ from __future__ import annotations
import os
from datetime import datetime, timedelta
-from google.cloud.aiplatform_v1beta1 import FeatureOnlineStore, FeatureView,
FeatureViewDataKey
-
from airflow import DAG
from airflow.providers.google.cloud.operators.bigquery import (
BigQueryCreateEmptyDatasetOperator,
@@ -127,15 +125,15 @@ with DAG(
project_id=PROJECT_ID,
location=REGION,
feature_online_store_id=FEATURE_ONLINE_STORE_ID,
- feature_online_store=FeatureOnlineStore(
- bigtable=FeatureOnlineStore.Bigtable(
- auto_scaling=FeatureOnlineStore.Bigtable.AutoScaling(
- min_node_count=1,
- max_node_count=3,
- cpu_utilization_target=50,
- )
- )
- ),
+ feature_online_store={
+ "bigtable": {
+ "auto_scaling": {
+ "min_node_count": 1,
+ "max_node_count": 3,
+ "cpu_utilization_target": 50,
+ }
+ }
+ },
)
# [END how_to_cloud_vertex_ai_create_feature_online_store_operator]
@@ -146,13 +144,13 @@ with DAG(
location=REGION,
feature_online_store_id=FEATURE_ONLINE_STORE_ID,
feature_view_id=FEATURE_VIEW_ID,
- feature_view=FeatureView(
- big_query_source=FeatureView.BigQuerySource(
- uri=f"bq://{BQ_VIEW_FQN}",
- entity_id_columns=["entity_id"],
- ),
- sync_config=FeatureView.SyncConfig(cron="TZ=America/Los_Angeles 56
* * * *"),
- ),
+ feature_view={
+ "big_query_source": {
+ "uri": f"bq://{BQ_VIEW_FQN}",
+ "entity_id_columns": ["entity_id"],
+ },
+ "sync_config": {"cron": "TZ=America/Los_Angeles 56 * * * *"},
+ },
)
# [END how_to_cloud_vertex_ai_create_feature_view_store_operator]
@@ -201,7 +199,7 @@ with DAG(
location=REGION,
feature_online_store_id=FEATURE_ONLINE_STORE_ID,
feature_view_id=FEATURE_VIEW_ID,
- data_key=FeatureViewDataKey(FEATURE_VIEW_DATA_KEY),
+ data_key=FEATURE_VIEW_DATA_KEY,
retries=3,
retry_delay=timedelta(minutes=3),
)
diff --git
a/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_feature_store.py
b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_feature_store.py
index f6b2a24d911..8b3e92def4e 100644
---
a/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_feature_store.py
+++
b/providers/google/tests/unit/google/cloud/hooks/vertex_ai/test_feature_store.py
@@ -17,8 +17,12 @@
from __future__ import annotations
+import subprocess
+import sys
from unittest import mock
+import pytest
+
from airflow.providers.google.cloud.hooks.vertex_ai.feature_store import
FeatureStoreHook
from unit.google.cloud.utils.base_gcp_mock import (
@@ -27,6 +31,7 @@ from unit.google.cloud.utils.base_gcp_mock import (
BASE_STRING = "airflow.providers.google.common.hooks.base_google.{}"
FEATURE_STORE_STRING =
"airflow.providers.google.cloud.hooks.vertex_ai.feature_store.{}"
+FEATURE_STORE_CLIENT_STRING = "google.cloud.aiplatform_v1beta1.{}"
TEST_GCP_CONN_ID = "test-gcp-conn-id"
TEST_PROJECT_ID = "test-project"
@@ -37,6 +42,22 @@ TEST_FEATURE_VIEW =
f"projects/{TEST_PROJECT_ID}/locations/{TEST_LOCATION}/featu
TEST_FEATURE_VIEW_SYNC_NAME = f"{TEST_FEATURE_VIEW}/featureViewSyncs/sync123"
[email protected](
+ "module",
+ (
+ "airflow.providers.google.cloud.hooks.vertex_ai.feature_store",
+ "airflow.providers.google.cloud.operators.vertex_ai.feature_store",
+ ),
+)
+def test_import_does_not_load_aiplatform_v1beta1(module):
+ code = (
+ f"import sys; import {module}; "
+ "assert 'google.cloud.aiplatform_v1beta1' not in sys.modules, "
+ "sorted(name for name in sys.modules if 'aiplatform_v1beta1' in name)"
+ )
+ subprocess.run([sys.executable, "-c", code], check=True,
capture_output=True, text=True)
+
+
class TestFeatureStoreHook:
def setup_method(self):
with mock.patch(
@@ -45,7 +66,7 @@ class TestFeatureStoreHook:
self.hook = FeatureStoreHook(gcp_conn_id=TEST_GCP_CONN_ID)
@mock.patch(BASE_STRING.format("GoogleBaseHook.get_client_options"))
-
@mock.patch(FEATURE_STORE_STRING.format("FeatureOnlineStoreAdminServiceClient"),
autospec=True)
+
@mock.patch(FEATURE_STORE_CLIENT_STRING.format("FeatureOnlineStoreAdminServiceClient"),
autospec=True)
@mock.patch(BASE_STRING.format("GoogleBaseHook.get_credentials"))
def test_get_feature_online_store_admin_service_client(
self, mock_get_credentials, mock_client, mock_get_client_options
@@ -69,6 +90,23 @@ class TestFeatureStoreHook:
api_endpoint_override =
mock_get_client_options.call_args[1]["api_endpoint_override"]
assert not api_endpoint_override
+ @mock.patch(BASE_STRING.format("GoogleBaseHook.get_client_options"))
+
@mock.patch(FEATURE_STORE_CLIENT_STRING.format("FeatureOnlineStoreServiceClient"),
autospec=True)
+ @mock.patch(BASE_STRING.format("GoogleBaseHook.get_credentials"))
+ def test_get_feature_online_store_service_client(
+ self,
+ mock_get_credentials,
+ mock_client,
+ mock_get_client_options,
+ ):
+
self.hook.get_feature_online_store_service_client(location=TEST_LOCATION)
+
+ mock_client.assert_called_once_with(
+ credentials=mock_get_credentials.return_value,
+ client_info=mock.ANY,
+ client_options=mock_get_client_options.return_value,
+ )
+
@mock.patch(FEATURE_STORE_STRING.format("FeatureStoreHook.get_feature_online_store_admin_service_client"))
def test_get_feature_view_sync(self, mock_client_getter):
mock_client = mock.MagicMock()