Skip to content

Commit a3fcd1f

Browse files
authored
feat: Implement gRPC server to ingest streaming features (#3687)
* Implemented gRPC server for ingesting streaming features. Signed-off-by: mehmettokgoz <mehmet.tokgoz@hazelcast.com> Signed-off-by: Danny C <d.chiao@gmail.com>
1 parent f2c5988 commit a3fcd1f

File tree

11 files changed

+999
-696
lines changed

11 files changed

+999
-696
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ kill-trino-locally:
353353
cd ${ROOT_DIR}; docker stop trino
354354

355355
install-protoc-dependencies:
356-
pip install --ignore-installed protobuf==4.23.4 grpcio-tools==1.47.0 mypy-protobuf==3.1.0
356+
pip install --ignore-installed protobuf==4.23.4 "grpcio-tools>=1.56.2,<2" mypy-protobuf==3.1.0
357357

358358
install-feast-ci-locally:
359359
pip install -e ".[ci]"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
syntax = "proto3";
2+
3+
message PushRequest {
4+
map<string, string> features = 1;
5+
string stream_feature_view = 2;
6+
bool allow_registry_cache = 3;
7+
string to = 4;
8+
}
9+
10+
message PushResponse {
11+
bool status = 1;
12+
}
13+
14+
message WriteToOnlineStoreRequest {
15+
map<string, string> features = 1;
16+
string feature_view_name = 2;
17+
bool allow_registry_cache = 3;
18+
}
19+
20+
message WriteToOnlineStoreResponse {
21+
bool status = 1;
22+
}
23+
24+
service GrpcFeatureServer {
25+
rpc Push (PushRequest) returns (PushResponse) {};
26+
rpc WriteToOnlineStore (WriteToOnlineStoreRequest) returns (WriteToOnlineStoreResponse);
27+
}

sdk/python/feast/cli.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from feast.constants import DEFAULT_FEATURE_TRANSFORMATION_SERVER_PORT
2929
from feast.errors import FeastObjectNotFoundException, FeastProviderLoginError
3030
from feast.feature_view import FeatureView
31+
from feast.infra.contrib.grpc_server import get_grpc_server
3132
from feast.on_demand_feature_view import OnDemandFeatureView
3233
from feast.repo_config import load_repo_config
3334
from feast.repo_operations import (
@@ -689,6 +690,36 @@ def serve_command(
689690
)
690691

691692

693+
@cli.command("listen")
694+
@click.option(
695+
"--address",
696+
"-a",
697+
type=click.STRING,
698+
default="localhost:50051",
699+
show_default=True,
700+
help="Address of the gRPC server",
701+
)
702+
@click.option(
703+
"--max_workers",
704+
"-w",
705+
type=click.INT,
706+
default=10,
707+
show_default=False,
708+
help="The maximum number of threads that can be used to execute the gRPC calls",
709+
)
710+
@click.pass_context
711+
def listen_command(
712+
ctx: click.Context,
713+
address: str,
714+
max_workers: int,
715+
):
716+
"""Start a gRPC feature server to ingest streaming features on given address"""
717+
store = create_feature_store(ctx)
718+
server = get_grpc_server(address, store, max_workers)
719+
server.start()
720+
server.wait_for_termination()
721+
722+
692723
@cli.command("serve_transformations")
693724
@click.option(
694725
"--port",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import logging
2+
from concurrent import futures
3+
4+
import grpc
5+
import pandas as pd
6+
from grpc_health.v1 import health, health_pb2_grpc
7+
8+
from feast.data_source import PushMode
9+
from feast.errors import PushSourceNotFoundException
10+
from feast.feature_store import FeatureStore
11+
from feast.protos.feast.serving.GrpcServer_pb2 import (
12+
PushResponse,
13+
WriteToOnlineStoreResponse,
14+
)
15+
from feast.protos.feast.serving.GrpcServer_pb2_grpc import (
16+
GrpcFeatureServerServicer,
17+
add_GrpcFeatureServerServicer_to_server,
18+
)
19+
20+
21+
def parse(features):
22+
df = {}
23+
for i in features.keys():
24+
df[i] = [features.get(i)]
25+
return pd.DataFrame.from_dict(df)
26+
27+
28+
class GrpcFeatureServer(GrpcFeatureServerServicer):
29+
fs: FeatureStore
30+
31+
def __init__(self, fs: FeatureStore):
32+
self.fs = fs
33+
super().__init__()
34+
35+
def Push(self, request, context):
36+
try:
37+
df = parse(request.features)
38+
if request.to == "offline":
39+
to = PushMode.OFFLINE
40+
elif request.to == "online":
41+
to = PushMode.ONLINE
42+
elif request.to == "online_and_offline":
43+
to = PushMode.ONLINE_AND_OFFLINE
44+
else:
45+
raise ValueError(
46+
f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', "
47+
f"'online_and_offline']."
48+
)
49+
self.fs.push(
50+
push_source_name=request.push_source_name,
51+
df=df,
52+
allow_registry_cache=request.allow_registry_cache,
53+
to=to,
54+
)
55+
except PushSourceNotFoundException as e:
56+
logging.exception(str(e))
57+
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
58+
context.set_details(str(e))
59+
return PushResponse(status=False)
60+
except Exception as e:
61+
logging.exception(str(e))
62+
context.set_code(grpc.StatusCode.INTERNAL)
63+
context.set_details(str(e))
64+
return PushResponse(status=False)
65+
return PushResponse(status=True)
66+
67+
def WriteToOnlineStore(self, request, context):
68+
logging.warning(
69+
"write_to_online_store is deprecated. Please consider using Push instead"
70+
)
71+
try:
72+
df = parse(request.features)
73+
self.fs.write_to_online_store(
74+
feature_view_name=request.feature_view_name,
75+
df=df,
76+
allow_registry_cache=request.allow_registry_cache,
77+
)
78+
except Exception as e:
79+
logging.exception(str(e))
80+
context.set_code(grpc.StatusCode.INTERNAL)
81+
context.set_details(str(e))
82+
return PushResponse(status=False)
83+
return WriteToOnlineStoreResponse(status=True)
84+
85+
86+
def get_grpc_server(address: str, fs: FeatureStore, max_workers: int):
87+
server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers))
88+
add_GrpcFeatureServerServicer_to_server(GrpcFeatureServer(fs), server)
89+
health_servicer = health.HealthServicer(
90+
experimental_non_blocking=True,
91+
experimental_thread_pool=futures.ThreadPoolExecutor(max_workers=max_workers),
92+
)
93+
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
94+
server.add_insecure_port(address)
95+
return server

0 commit comments

Comments
 (0)