-
Notifications
You must be signed in to change notification settings - Fork 308
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
External Plugin Service (grpc) #1524
Changes from all commits
2437e6a
063dd3b
cd14658
93a4ed2
a2a5305
f6b0d81
84ffbfe
b39fe48
8ba641c
bda6432
609f852
c06662d
625548b
c446900
787031e
996552c
ce60f20
18af9ac
047b7f1
cf9cf3e
82c048f
19c198c
d66c3f8
5f0084f
2edc620
fd2b9b3
e71b6f6
2b76331
2f598b2
c342d37
ee4a180
1a908c4
764c0f5
f402d57
bc30f51
1c16952
f044e28
8385e02
1dd716d
59714f9
26eab42
4ee1417
a70c12e
dbd26b5
2c1cce8
5a2bdc4
1fba9d4
ae8c37e
0b151cf
9f8337d
4b92275
f28183e
e07c72d
9cadb80
0357806
f594df9
c524560
f11dd2b
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 |
---|---|---|
@@ -0,0 +1,10 @@ | ||
FROM python:3.9-slim-buster | ||
|
||
MAINTAINER Flyte Team <[email protected]> | ||
LABEL org.opencontainers.image.source=https://github.com/flyteorg/flytekit | ||
|
||
ARG VERSION | ||
RUN pip install -U flytekit==$VERSION \ | ||
flytekitplugins-bigquery==$VERSION \ | ||
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. why is bigquery special? 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. because I add a new backend plugin for BQ in this pr |
||
|
||
CMD pyflyte serve --port 8000 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from concurrent import futures | ||
|
||
import click | ||
import grpc | ||
from flyteidl.service.external_plugin_service_pb2_grpc import add_ExternalPluginServiceServicer_to_server | ||
|
||
from flytekit.extend.backend.external_plugin_service import BackendPluginServer | ||
|
||
_serve_help = """Start a grpc server for the external plugin service.""" | ||
|
||
|
||
@click.command("serve", help=_serve_help) | ||
@click.option( | ||
"--port", | ||
default="8000", | ||
is_flag=False, | ||
type=int, | ||
help="Grpc port for the external plugin service", | ||
) | ||
@click.option( | ||
"--worker", | ||
default="10", | ||
is_flag=False, | ||
type=int, | ||
help="Number of workers for the grpc server", | ||
) | ||
@click.option( | ||
"--timeout", | ||
default=None, | ||
is_flag=False, | ||
type=int, | ||
help="It will wait for the specified number of seconds before shutting down grpc server. It should only be used " | ||
"for testing.", | ||
) | ||
@click.pass_context | ||
def serve(_: click.Context, port, worker, timeout): | ||
""" | ||
Start a grpc server for the external plugin service. | ||
""" | ||
click.secho("Starting the external plugin service...", fg="blue") | ||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=worker)) | ||
add_ExternalPluginServiceServicer_to_server(BackendPluginServer(), server) | ||
|
||
server.add_insecure_port(f"[::]:{port}") | ||
server.start() | ||
server.wait_for_termination(timeout=timeout) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import typing | ||
from abc import ABC, abstractmethod | ||
|
||
import grpc | ||
from flyteidl.core.tasks_pb2 import TaskTemplate | ||
from flyteidl.service.external_plugin_service_pb2 import ( | ||
RETRYABLE_FAILURE, | ||
RUNNING, | ||
SUCCEEDED, | ||
State, | ||
TaskCreateResponse, | ||
TaskDeleteResponse, | ||
TaskGetResponse, | ||
) | ||
|
||
from flytekit import logger | ||
from flytekit.models.literals import LiteralMap | ||
|
||
|
||
class BackendPluginBase(ABC): | ||
pingsutw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
This is the base class for all backend plugins. It defines the interface that all plugins must implement. | ||
The external plugins service will be run either locally or in a pod, and will be responsible for | ||
invoking backend plugins. The propeller will communicate with the external plugins service | ||
to create tasks, get the status of tasks, and delete tasks. | ||
|
||
All the backend plugins should be registered in the BackendPluginRegistry. External plugins service | ||
will look up the plugin based on the task type. Every task type can only have one plugin. | ||
""" | ||
|
||
def __init__(self, task_type: str): | ||
self._task_type = task_type | ||
|
||
@property | ||
def task_type(self) -> str: | ||
""" | ||
task_type is the name of the task type that this plugin supports. | ||
""" | ||
return self._task_type | ||
|
||
@abstractmethod | ||
def create( | ||
self, | ||
context: grpc.ServicerContext, | ||
output_prefix: str, | ||
task_template: TaskTemplate, | ||
inputs: typing.Optional[LiteralMap] = None, | ||
) -> TaskCreateResponse: | ||
""" | ||
Return a Unique ID for the task that was created. It should return error code if the task creation failed. | ||
""" | ||
pass | ||
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. like the fact that this is supposed to return a unique job id. |
||
|
||
@abstractmethod | ||
def get(self, context: grpc.ServicerContext, job_id: str) -> TaskGetResponse: | ||
""" | ||
Return the status of the task, and return the outputs in some cases. For example, bigquery job | ||
can't write the structured dataset to the output location, so it returns the output literals to the propeller, | ||
and the propeller will write the structured dataset to the blob store. | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def delete(self, context: grpc.ServicerContext, job_id: str) -> TaskDeleteResponse: | ||
""" | ||
Delete the task. This call should be idempotent. | ||
""" | ||
pass | ||
|
||
|
||
class BackendPluginRegistry(object): | ||
""" | ||
This is the registry for all backend plugins. The external plugins service will look up the plugin | ||
based on the task type. | ||
""" | ||
|
||
_REGISTRY: typing.Dict[str, BackendPluginBase] = {} | ||
|
||
@staticmethod | ||
def register(plugin: BackendPluginBase): | ||
if plugin.task_type in BackendPluginRegistry._REGISTRY: | ||
raise ValueError(f"Duplicate plugin for task type {plugin.task_type}") | ||
BackendPluginRegistry._REGISTRY[plugin.task_type] = plugin | ||
pingsutw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logger.info(f"Registering backend plugin for task type {plugin.task_type}") | ||
|
||
@staticmethod | ||
def get_plugin(context: grpc.ServicerContext, task_type: str) -> typing.Optional[BackendPluginBase]: | ||
if task_type not in BackendPluginRegistry._REGISTRY: | ||
logger.error(f"Cannot find backend plugin for task type [{task_type}]") | ||
context.set_code(grpc.StatusCode.NOT_FOUND) | ||
context.set_details(f"Cannot find backend plugin for task type [{task_type}]") | ||
return None | ||
return BackendPluginRegistry._REGISTRY[task_type] | ||
|
||
|
||
def convert_to_flyte_state(state: str) -> State: | ||
""" | ||
Convert the state from the backend plugin to the state in flyte. | ||
""" | ||
state = state.lower() | ||
if state in ["failed"]: | ||
return RETRYABLE_FAILURE | ||
elif state in ["done", "succeeded"]: | ||
return SUCCEEDED | ||
elif state in ["running"]: | ||
return RUNNING | ||
raise ValueError(f"Unrecognized state: {state}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import grpc | ||
from flyteidl.service.external_plugin_service_pb2 import ( | ||
PERMANENT_FAILURE, | ||
TaskCreateRequest, | ||
TaskCreateResponse, | ||
TaskDeleteRequest, | ||
TaskDeleteResponse, | ||
TaskGetRequest, | ||
TaskGetResponse, | ||
) | ||
from flyteidl.service.external_plugin_service_pb2_grpc import ExternalPluginServiceServicer | ||
|
||
from flytekit import logger | ||
from flytekit.extend.backend.base_plugin import BackendPluginRegistry | ||
from flytekit.models.literals import LiteralMap | ||
from flytekit.models.task import TaskTemplate | ||
|
||
|
||
class BackendPluginServer(ExternalPluginServiceServicer): | ||
def CreateTask(self, request: TaskCreateRequest, context: grpc.ServicerContext) -> TaskCreateResponse: | ||
try: | ||
tmp = TaskTemplate.from_flyte_idl(request.template) | ||
inputs = LiteralMap.from_flyte_idl(request.inputs) if request.inputs else None | ||
plugin = BackendPluginRegistry.get_plugin(context, tmp.type) | ||
if plugin is None: | ||
return TaskCreateResponse() | ||
return plugin.create(context=context, inputs=inputs, output_prefix=request.output_prefix, task_template=tmp) | ||
except Exception as e: | ||
logger.error(f"failed to create task with error {e}") | ||
context.set_code(grpc.StatusCode.INTERNAL) | ||
context.set_details(f"failed to create task with error {e}") | ||
pingsutw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def GetTask(self, request: TaskGetRequest, context: grpc.ServicerContext) -> TaskGetResponse: | ||
try: | ||
plugin = BackendPluginRegistry.get_plugin(context, request.task_type) | ||
if plugin is None: | ||
return TaskGetResponse(state=PERMANENT_FAILURE) | ||
return plugin.get(context=context, job_id=request.job_id) | ||
except Exception as e: | ||
logger.error(f"failed to get task with error {e}") | ||
context.set_code(grpc.StatusCode.INTERNAL) | ||
context.set_details(f"failed to get task with error {e}") | ||
|
||
def DeleteTask(self, request: TaskDeleteRequest, context: grpc.ServicerContext) -> TaskDeleteResponse: | ||
try: | ||
plugin = BackendPluginRegistry.get_plugin(context, request.task_type) | ||
if plugin is None: | ||
return TaskDeleteResponse() | ||
return plugin.delete(context=context, job_id=request.job_id) | ||
except Exception as e: | ||
logger.error(f"failed to delete task with error {e}") | ||
context.set_code(grpc.StatusCode.INTERNAL) | ||
context.set_details(f"failed to delete task with error {e}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,4 +11,5 @@ | |
BigQueryTask | ||
""" | ||
|
||
from .backend_plugin import BigQueryPlugin | ||
from .task import BigQueryConfig, BigQueryTask |
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.
should we build one for each python version?
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.
I think we can use a default version first because it's experimental feature. we can add more version when we need it