Skip to content

Commit 194555c

Browse files
Merge branch 'master' into fix-context-cache
2 parents 6d6b1c8 + 39d408d commit 194555c

7 files changed

Lines changed: 459 additions & 11 deletions

File tree

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
* [Snowflake](reference/compute-engine/snowflake.md)
189189
* [AWS Lambda (alpha)](reference/compute-engine/lambda.md)
190190
* [Spark (contrib)](reference/compute-engine/spark.md)
191+
* [SparkApplication](reference/compute-engine/spark_application.md)
191192
* [Apache Flink](reference/compute-engine/flink.md)
192193
* [Ray (contrib)](reference/compute-engine/ray.md)
193194
* [Feature repository](reference/feature-repository/README.md)

docs/how-to-guides/feast-operator/06-batch-and-jobs.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,53 @@ spec:
5050
configMapKey: config # key inside the ConfigMap (default: "config")
5151
```
5252
53+
### SparkApplication batch engine (optional)
54+
55+
For Bring Your Own Spark on Kubernetes, use `spark_application` instead of in-process Spark.
56+
The Feast Operator auto-creates RBAC for this type. See
57+
[SparkApplication](../reference/compute-engine/spark_application.md) for the full config reference.
58+
Build an image from the reference
59+
[Dockerfile](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile)
60+
(or equivalent):
61+
62+
```yaml
63+
apiVersion: v1
64+
kind: ConfigMap
65+
metadata:
66+
name: feast-spark-application-engine
67+
data:
68+
config: |
69+
type: spark_application
70+
image: my-registry.example.com/feast-spark-driver:latest
71+
namespace: feast
72+
executor_instances: 2
73+
driver_memory: "2g"
74+
executor_memory: "2g"
75+
```
76+
77+
```yaml
78+
apiVersion: feast.dev/v1
79+
kind: FeatureStore
80+
metadata:
81+
name: sample-spark-application
82+
spec:
83+
feastProject: my_project
84+
batchEngine:
85+
configMapRef:
86+
name: feast-spark-application-engine
87+
configMapKey: config
88+
# Optional: use the Spark driver image for feast-apply / init containers
89+
# services:
90+
# initImage: my-registry.example.com/feast-spark-driver:latest
91+
```
92+
5393
### Engine types
5494

5595
| `type` | Notes |
5696
|--------|-------|
5797
| `local` | Default; in-process Python, no extra infra |
5898
| `spark` | Apache Spark; requires a Spark operator or standalone cluster |
99+
| `spark_application` | Kubeflow Spark Operator `SparkApplication` CRs; requires Spark Operator + custom image; operator auto-creates RBAC |
59100
| `ray` | Ray cluster; requires a Ray operator |
60101
| `bytewax` | Bytewax streaming engine |
61102
| `snowflake.engine` | Snowflake Snowpark compute |

docs/reference/compute-engine/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ An example of built output from FeatureBuilder:
5757
- Supports point-in-time joins and large-scale materialization
5858
- Integrates with `SparkOfflineStore` and `SparkMaterializationJob`
5959

60+
### ☸️ SparkApplicationComputeEngine
61+
62+
{% page-ref page="spark_application.md" %}
63+
64+
- Batch materialization via Kubeflow Spark Operator `SparkApplication` CRs
65+
- One SparkApplication per materialize call (multi–feature-view batching)
66+
- Requires network-accessible online/offline/registry stores (no file-based backends)
67+
6068
### 🌊 FlinkComputeEngine
6169

6270
{% page-ref page="flink.md" %}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# SparkApplication Compute Engine
2+
3+
## Description
4+
5+
The **SparkApplication** compute engine runs Feast **batch materialization** on Kubernetes by creating a [Kubeflow Spark Operator](https://github.com/kubeflow/spark-operator) `SparkApplication` custom resource for each materialization job.
6+
7+
Unlike the in-process [`spark.engine`](spark.md) compute engine (which uses a Spark session inside the Feast process), `spark_application` submits work to the Spark Operator. The operator starts a driver pod and executors from your configured image; Feast polls the SparkApplication until it completes.
8+
9+
| Capability | Supported |
10+
|------------|-----------|
11+
| `materialize` / `materialize-incremental` | Yes |
12+
| Multiple feature views in one job | Yes — one SparkApplication per materialize call |
13+
| `get_historical_features` | Not yet |
14+
| SparkConnect | Separate approach — not this engine |
15+
16+
### Design
17+
18+
1. Feast creates a ConfigMap with job tasks and a driver copy of `feature_store.yaml`.
19+
2. Feast creates a `SparkApplication` CR pointing at the driver entrypoint (`main.py` in the image).
20+
3. Inside the pod, the batch engine type is rewritten to `spark.engine` so materialization uses the Spark session created by `spark-submit` (avoids recursive SparkApplication creation).
21+
4. The driver writes features to your configured **online store** and updates the **registry** (same network backends as the server).
22+
23+
### Requirements
24+
25+
- Kubeflow Spark Operator installed and watching the target namespace.
26+
- A container **image** that includes the Feast SDK, PySpark, and clients for your stores. See the reference [Dockerfile](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile).
27+
- **Network-accessible** online store, offline store, and registry. File-based backends are rejected because Spark pods have an ephemeral filesystem:
28+
29+
| Rejected | Examples | Use instead |
30+
|----------|----------|-------------|
31+
| File online | `sqlite`, `faiss` | Redis, remote online, etc. |
32+
| File offline | `dask`, `file`, `duckdb` | `spark`, Postgres, Snowflake, BigQuery, etc. |
33+
| File registry | `file` | SQL registry, Snowflake |
34+
35+
For distributed reads, configure `offline_store.type: spark` (or another store Spark can read efficiently).
36+
37+
### Kubernetes / Feast Operator notes
38+
39+
When using the Feast Operator:
40+
41+
- Point `spec.batchEngine.configMapRef` at a ConfigMap whose `type` is `spark_application` (see [Guide 6 — Batch Engine & Scheduled Jobs](../../how-to-guides/feast-operator/06-batch-and-jobs.md)).
42+
- The operator auto-creates RBAC for the `spark_application` batch engine (server and driver service accounts).
43+
- Set `spec.services.initImage` if init / `feast-apply` containers need the Spark-capable image.
44+
45+
---
46+
47+
## Example
48+
49+
{% code title="feature_store.yaml" %}
50+
```yaml
51+
project: my_project
52+
registry:
53+
registry_type: sql
54+
path: postgresql+psycopg://feast:****@postgres:5432/feast
55+
online_store:
56+
type: redis
57+
connection_string: redis:6379
58+
offline_store:
59+
type: spark
60+
spark_conf:
61+
spark.master: local[*]
62+
batch_engine:
63+
type: spark_application
64+
image: my-registry.example.com/feast-spark-driver:latest
65+
namespace: feast
66+
spark_version: "4.0.1"
67+
driver_cores: 1
68+
driver_memory: "2g"
69+
executor_instances: 2
70+
executor_cores: 1
71+
executor_memory: "2g"
72+
spark_conf:
73+
spark.sql.shuffle.partitions: "100"
74+
```
75+
{% endcode %}
76+
77+
### Feast Operator ConfigMap
78+
79+
```yaml
80+
apiVersion: v1
81+
kind: ConfigMap
82+
metadata:
83+
name: feast-spark-batch-engine
84+
namespace: feast
85+
data:
86+
config: |
87+
type: spark_application
88+
image: my-registry.example.com/feast-spark-driver:latest
89+
namespace: feast
90+
executor_instances: 2
91+
driver_memory: "2g"
92+
executor_memory: "2g"
93+
---
94+
apiVersion: feast.dev/v1
95+
kind: FeatureStore
96+
metadata:
97+
name: feast
98+
namespace: feast
99+
spec:
100+
feastProject: my_project
101+
batchEngine:
102+
configMapRef:
103+
name: feast-spark-batch-engine
104+
configMapKey: config
105+
```
106+
107+
---
108+
109+
## Remote materialization
110+
111+
If the client uses a **remote** online store (`online_store.type: remote`), `FeatureStore.materialize()` delegates to the feature server HTTP API. The server runs the SparkApplication engine.
112+
113+
- Default (`run_async=False`): block until the server finishes sync materialization.
114+
- `run_async=True`: accept asynchronously (`?async=true`); poll feature-view state in the registry for completion.
115+
- `force=True` (with `run_async=True`): override stuck `MATERIALIZING` state on the server.
116+
117+
```python
118+
from datetime import datetime, timedelta
119+
from feast import FeatureStore
120+
121+
store = FeatureStore(repo_path=".") # client feature_store.yaml with online_store.type: remote
122+
123+
store.materialize(
124+
start_date=datetime.utcnow() - timedelta(days=1),
125+
end_date=datetime.utcnow(),
126+
)
127+
```
128+
129+
---
130+
131+
## Configuration reference
132+
133+
| Field | Type | Default | Description |
134+
|-------|------|---------|-------------|
135+
| `type` | string | `spark_application` | Engine type key |
136+
| `image` | string | **required** | Container image for the Spark driver/executors |
137+
| `image_pull_secrets` | list[str] | `[]` | Image pull secret names |
138+
| `namespace` | string | `default` | Namespace for SparkApplication and ConfigMap |
139+
| `service_account` | string | `""` | Driver service account; empty uses platform/operator default |
140+
| `spark_version` | string | `4.0.1` | Spark version for the CR |
141+
| `driver_cores` | int | `1` | Driver cores |
142+
| `driver_memory` | string | `1g` | Driver memory |
143+
| `executor_instances` | int | `1` | Number of executors |
144+
| `executor_cores` | int | `1` | Cores per executor |
145+
| `executor_memory` | string | `1g` | Memory per executor |
146+
| `spark_conf` | dict | `null` | Extra Spark configuration |
147+
| `hadoop_conf` | dict | `null` | Extra Hadoop configuration |
148+
| `env` | list[dict] | `[]` | Driver env vars (`name` + `value` or `valueFrom`) |
149+
| `env_from` | list[dict] | `[]` | EnvFrom sources |
150+
| `queue_name` | string | `null` | Optional queue / Kueue label |
151+
| `job_timeout_seconds` | int | `3600` | Max wait for SparkApplication completion |
152+
| `poll_interval_seconds` | int | `10` | Status poll interval |
153+
| `ttl_seconds_after_finished` | int | `3600` | CR TTL after finish |
154+
| `restart_policy` | string | `Never` | SparkApplication restart policy |
155+
| `max_retries` | int | `3` | Retries when restart policy allows |
156+
| `concurrency` | int | `1` | Parallel feature views inside one driver |
157+
| `labels` | dict | `{}` | Extra labels on the CR |
158+
| `volumes` / `volume_mounts` | list | `[]` | Extra volumes for the driver |
159+
| `py_files` | list[str] | `[]` | Additional Python files for Spark |
160+
| `node_selector` | dict | `null` | Pod node selector |
161+
| `tolerations` | list | `[]` | Pod tolerations |
162+
| `staging_location` | string | `null` | Reserved for historical retrieval (ignored for materialize) |
163+
164+
---
165+
166+
## Troubleshooting
167+
168+
| Symptom | What to check |
169+
|---------|----------------|
170+
| SparkApplication Pending / insufficient CPU | Lower resource requests via `spark_conf` (for example `spark.kubernetes.driver.request.cores`) or free cluster capacity |
171+
| ImagePullBackOff | Image name, tag, and `image_pull_secrets` |
172+
| 403 on ConfigMap or SparkApplication | RBAC for the Feast server and Spark driver service accounts |
173+
| Init `ValueError` about file-based stores | Switch online/offline/registry to network backends |
174+
| Init / feast-apply failures missing Spark deps | Use a Spark-capable image (`initImage` with the Feast Operator) |
175+
176+
---
177+
178+
## Related
179+
180+
- [Spark compute engine (in-process)](spark.md)
181+
- [Feast Operator — batch engine ConfigMap](../../how-to-guides/feast-operator/06-batch-and-jobs.md)
182+
- [Creating a custom compute engine](../../how-to-guides/customizing-feast/creating-a-custom-compute-engine.md)

sdk/python/feast/online_response.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import uuid as uuid_module
16-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeAlias, Union
16+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
1717

1818
import pandas as pd
1919
import pyarrow as pa
@@ -25,11 +25,9 @@
2525
from feast.value_type import ValueType
2626

2727
if TYPE_CHECKING:
28-
import torch
29-
30-
TorchTensor: TypeAlias = torch.Tensor
28+
from torch import Tensor as TorchTensor
3129
else:
32-
TorchTensor: TypeAlias = Any
30+
TorchTensor = Any
3331

3432
TIMESTAMP_POSTFIX: str = "__ts"
3533

sdk/python/feast/registry_server.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from feast.base_feature_view import BaseFeatureView
1313
from feast.data_source import DataSource
1414
from feast.entity import Entity
15-
from feast.errors import FeastObjectNotFoundException
15+
from feast.errors import FeastObjectNotFoundException, FeastPermissionError
1616
from feast.feast_object import FeastObject
1717
from feast.feature_view import FeatureView
1818
from feast.grpc_error_interceptor import ErrorInterceptor
@@ -24,6 +24,8 @@
2424
from feast.permissions.security_manager import (
2525
assert_permissions,
2626
assert_permissions_to_update,
27+
get_security_manager,
28+
is_auth_necessary,
2729
permitted_resources,
2830
)
2931
from feast.permissions.server.grpc import AuthInterceptor
@@ -1603,16 +1605,37 @@ def GetObjectRelationships(
16031605
)
16041606

16051607
def Commit(self, request, context):
1606-
for project in self.proxied_registry.list_projects(allow_cache=True):
1607-
assert_permissions(resource=project, actions=[AuthzedAction.UPDATE])
1608+
# Per-object mutations are authorized in Apply*/Delete* RPCs.
1609+
# Requiring UPDATE on every project breaks shared-registry multi-tenant
1610+
# commits (remote feastRef with different feastProjects). Require CREATE
1611+
# or UPDATE on at least one project when auth is enabled instead.
1612+
projects = cast(
1613+
list[FeastObject],
1614+
list(self.proxied_registry.list_projects(allow_cache=True)),
1615+
)
1616+
if projects and is_auth_necessary(get_security_manager()):
1617+
can_update = permitted_resources(
1618+
resources=projects, actions=AuthzedAction.UPDATE
1619+
)
1620+
can_create = permitted_resources(
1621+
resources=projects, actions=AuthzedAction.CREATE
1622+
)
1623+
if not can_update and not can_create:
1624+
raise FeastPermissionError(
1625+
"Not authorized to commit registry changes: "
1626+
"CREATE or UPDATE permission required on at least one project"
1627+
)
16081628
self.proxied_registry.commit()
16091629
return Empty()
16101630

16111631
def Refresh(self, request, context):
1612-
project = self.proxied_registry.get_project(
1613-
name=request.project, allow_cache=True
1632+
# Use create-or-update authorization so first apply of a new project over
1633+
# a remote registry can refresh before the project exists yet.
1634+
assert_permissions_to_update(
1635+
resource=Project(name=request.project),
1636+
getter=self.proxied_registry.get_project,
1637+
project=request.project,
16141638
)
1615-
assert_permissions(resource=project, actions=[AuthzedAction.UPDATE])
16161639
self.proxied_registry.refresh(request.project)
16171640
return Empty()
16181641

0 commit comments

Comments
 (0)