indexify 0.2.44__py3-none-any.whl → 0.2.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- indexify/__init__.py +2 -0
- indexify/cli.py +41 -80
- indexify/executor/api_objects.py +2 -0
- indexify/executor/downloader.py +23 -25
- indexify/executor/executor.py +29 -35
- indexify/executor/function_executor/function_executor.py +120 -19
- indexify/executor/function_executor/function_executor_state.py +75 -0
- indexify/executor/function_executor/invocation_state_client.py +232 -0
- indexify/executor/function_executor/server/function_executor_server.py +24 -0
- indexify/executor/function_executor/server/function_executor_server_factory.py +43 -0
- indexify/executor/function_executor/server/subprocess_function_executor_server.py +25 -0
- indexify/executor/function_executor/{process_function_executor_factory.py → server/subprocess_function_executor_server_factory.py} +21 -21
- indexify/executor/function_executor/single_task_runner.py +160 -0
- indexify/executor/function_executor/task_input.py +23 -0
- indexify/executor/function_executor/task_output.py +36 -0
- indexify/executor/task_reporter.py +10 -17
- indexify/executor/task_runner.py +104 -0
- indexify/function_executor/function_executor_service.py +22 -7
- indexify/function_executor/handlers/run_function/handler.py +13 -12
- indexify/function_executor/invocation_state/invocation_state_proxy_server.py +170 -0
- indexify/function_executor/invocation_state/proxied_invocation_state.py +24 -0
- indexify/function_executor/invocation_state/response_validator.py +29 -0
- indexify/function_executor/proto/function_executor.proto +47 -0
- indexify/function_executor/proto/function_executor_pb2.py +23 -11
- indexify/function_executor/proto/function_executor_pb2.pyi +70 -0
- indexify/function_executor/proto/function_executor_pb2_grpc.py +50 -0
- indexify/functions_sdk/graph.py +3 -3
- indexify/functions_sdk/image.py +142 -9
- indexify/functions_sdk/indexify_functions.py +45 -79
- indexify/functions_sdk/invocation_state/invocation_state.py +22 -0
- indexify/functions_sdk/invocation_state/local_invocation_state.py +30 -0
- indexify/http_client.py +0 -17
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/METADATA +1 -1
- indexify-0.2.45.dist-info/RECORD +60 -0
- indexify/executor/function_executor/function_executor_factory.py +0 -26
- indexify/executor/function_executor/function_executor_map.py +0 -91
- indexify/executor/function_executor/process_function_executor.py +0 -64
- indexify/executor/function_worker.py +0 -253
- indexify-0.2.44.dist-info/RECORD +0 -50
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/LICENSE.txt +0 -0
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/WHEEL +0 -0
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/entry_points.txt +0 -0
@@ -10,7 +10,7 @@ from indexify.function_executor.proto.function_executor_pb2 import (
|
|
10
10
|
FunctionOutput,
|
11
11
|
)
|
12
12
|
|
13
|
-
from .
|
13
|
+
from .task_runner import TaskOutput
|
14
14
|
|
15
15
|
|
16
16
|
# https://github.com/psf/requests/issues/1081#issuecomment-428504128
|
@@ -48,17 +48,10 @@ class TaskReporter:
|
|
48
48
|
# results in not reusing established TCP connections to server.
|
49
49
|
self._client = get_httpx_client(config_path, make_async=False)
|
50
50
|
|
51
|
-
async def report(
|
52
|
-
|
53
|
-
):
|
54
|
-
"""Reports result of the supplied task.
|
55
|
-
|
56
|
-
If FunctionWorkerOutput is None this means that the task didn't finish and failed with internal error.
|
57
|
-
"""
|
51
|
+
async def report(self, output: TaskOutput, logger: Any):
|
52
|
+
"""Reports result of the supplied task."""
|
58
53
|
logger = logger.bind(module=__name__)
|
59
|
-
task_result, output_files, output_summary = self._process_task_output(
|
60
|
-
task, output
|
61
|
-
)
|
54
|
+
task_result, output_files, output_summary = self._process_task_output(output)
|
62
55
|
task_result_data = task_result.model_dump_json(exclude_none=True)
|
63
56
|
|
64
57
|
logger.info(
|
@@ -100,16 +93,16 @@ class TaskReporter:
|
|
100
93
|
) from e
|
101
94
|
|
102
95
|
def _process_task_output(
|
103
|
-
self,
|
96
|
+
self, output: TaskOutput
|
104
97
|
) -> Tuple[TaskResult, List[Any], TaskOutputSummary]:
|
105
98
|
task_result = TaskResult(
|
106
99
|
outcome="failure",
|
107
|
-
namespace=task.namespace,
|
108
|
-
compute_graph=task.compute_graph,
|
109
|
-
compute_fn=task.compute_fn,
|
110
|
-
invocation_id=task.invocation_id,
|
100
|
+
namespace=output.task.namespace,
|
101
|
+
compute_graph=output.task.compute_graph,
|
102
|
+
compute_fn=output.task.compute_fn,
|
103
|
+
invocation_id=output.task.invocation_id,
|
111
104
|
executor_id=self._executor_id,
|
112
|
-
task_id=task.id,
|
105
|
+
task_id=output.task.id,
|
113
106
|
)
|
114
107
|
output_files: List[Any] = []
|
115
108
|
summary: TaskOutputSummary = TaskOutputSummary()
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
|
3
|
+
from .api_objects import Task
|
4
|
+
from .function_executor.function_executor_state import FunctionExecutorState
|
5
|
+
from .function_executor.server.function_executor_server_factory import (
|
6
|
+
FunctionExecutorServerFactory,
|
7
|
+
)
|
8
|
+
from .function_executor.single_task_runner import SingleTaskRunner
|
9
|
+
from .function_executor.task_input import TaskInput
|
10
|
+
from .function_executor.task_output import TaskOutput
|
11
|
+
|
12
|
+
|
13
|
+
class TaskRunner:
|
14
|
+
"""Routes a task to its container following a scheduling policy.
|
15
|
+
|
16
|
+
Due to the scheduling policy a task might be blocked for a while."""
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
function_executor_server_factory: FunctionExecutorServerFactory,
|
21
|
+
base_url: str,
|
22
|
+
config_path: Optional[str],
|
23
|
+
):
|
24
|
+
self._factory: FunctionExecutorServerFactory = function_executor_server_factory
|
25
|
+
self._base_url: str = base_url
|
26
|
+
self._config_path: Optional[str] = config_path
|
27
|
+
# We don't lock this map cause we never await while reading and modifying it.
|
28
|
+
self._function_executor_states: Dict[str, FunctionExecutorState] = {}
|
29
|
+
|
30
|
+
async def run(self, task_input: TaskInput, logger: Any) -> TaskOutput:
|
31
|
+
logger = logger.bind(module=__name__)
|
32
|
+
try:
|
33
|
+
return await self._run(task_input, logger)
|
34
|
+
except Exception as e:
|
35
|
+
logger.error(
|
36
|
+
"failed running the task",
|
37
|
+
exc_info=e,
|
38
|
+
)
|
39
|
+
return TaskOutput.internal_error(task_input.task)
|
40
|
+
|
41
|
+
async def _run(self, task_input: TaskInput, logger: Any) -> TaskOutput:
|
42
|
+
state = self._get_or_create_state(task_input.task)
|
43
|
+
async with state.lock:
|
44
|
+
await self._run_task_policy(state, task_input.task)
|
45
|
+
return await self._run_task(state, task_input, logger)
|
46
|
+
|
47
|
+
async def _run_task_policy(self, state: FunctionExecutorState, task: Task) -> None:
|
48
|
+
# Current policy for running tasks:
|
49
|
+
# - There can only be a single Function Executor per function regardless of function versions.
|
50
|
+
# -- If a Function Executor already exists for a different function version then wait until
|
51
|
+
# all the tasks finish in the existing Function Executor and then destroy it.
|
52
|
+
# -- This prevents failed tasks for different versions of the same function continiously
|
53
|
+
# destroying each other's Function Executors.
|
54
|
+
# - Each Function Executor rans at most 1 task concurrently.
|
55
|
+
await state.wait_running_tasks_less(1)
|
56
|
+
|
57
|
+
if state.function_id_with_version != _function_id_with_version(task):
|
58
|
+
await state.destroy_function_executor()
|
59
|
+
state.function_id_with_version = _function_id_with_version(task)
|
60
|
+
# At this point the state belongs to the version of the function from the task
|
61
|
+
# and there are no running tasks in the Function Executor.
|
62
|
+
|
63
|
+
def _get_or_create_state(self, task: Task) -> FunctionExecutorState:
|
64
|
+
id = _function_id_without_version(task)
|
65
|
+
if id not in self._function_executor_states:
|
66
|
+
state = FunctionExecutorState(
|
67
|
+
function_id_with_version=_function_id_with_version(task),
|
68
|
+
function_id_without_version=id,
|
69
|
+
)
|
70
|
+
self._function_executor_states[id] = state
|
71
|
+
return self._function_executor_states[id]
|
72
|
+
|
73
|
+
async def _run_task(
|
74
|
+
self, state: FunctionExecutorState, task_input: TaskInput, logger: Any
|
75
|
+
) -> TaskOutput:
|
76
|
+
runner: SingleTaskRunner = SingleTaskRunner(
|
77
|
+
function_executor_state=state,
|
78
|
+
task_input=task_input,
|
79
|
+
function_executor_server_factory=self._factory,
|
80
|
+
base_url=self._base_url,
|
81
|
+
config_path=self._config_path,
|
82
|
+
logger=logger,
|
83
|
+
)
|
84
|
+
return await runner.run()
|
85
|
+
|
86
|
+
async def shutdown(self) -> None:
|
87
|
+
# When shutting down there's no need to wait for completion of the running
|
88
|
+
# FunctionExecutor tasks.
|
89
|
+
while self._function_executor_states:
|
90
|
+
id, state = self._function_executor_states.popitem()
|
91
|
+
# At this point the state is not visible to new tasks.
|
92
|
+
# Only ongoing tasks who read it already have a reference to it.
|
93
|
+
await state.destroy_function_executor_not_locked()
|
94
|
+
# The task running inside the Function Executor will fail because it's destroyed.
|
95
|
+
# asyncio tasks waiting to run inside the Function Executor will get cancelled by
|
96
|
+
# the caller's shutdown code.
|
97
|
+
|
98
|
+
|
99
|
+
def _function_id_with_version(task: Task) -> str:
|
100
|
+
return f"versioned/{task.namespace}/{task.compute_graph}/{task.graph_version}/{task.compute_fn}"
|
101
|
+
|
102
|
+
|
103
|
+
def _function_id_without_version(task: Task) -> str:
|
104
|
+
return f"not_versioned/{task.namespace}/{task.compute_graph}/{task.compute_fn}"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Optional, Union
|
1
|
+
from typing import Iterator, Optional, Union
|
2
2
|
|
3
3
|
import grpc
|
4
4
|
import structlog
|
@@ -14,9 +14,14 @@ from .handlers.run_function.request_validator import (
|
|
14
14
|
RequestValidator as RunTaskRequestValidator,
|
15
15
|
)
|
16
16
|
from .initialize_request_validator import InitializeRequestValidator
|
17
|
+
from .invocation_state.invocation_state_proxy_server import (
|
18
|
+
InvocationStateProxyServer,
|
19
|
+
)
|
20
|
+
from .invocation_state.proxied_invocation_state import ProxiedInvocationState
|
17
21
|
from .proto.function_executor_pb2 import (
|
18
22
|
InitializeRequest,
|
19
23
|
InitializeResponse,
|
24
|
+
InvocationStateResponse,
|
20
25
|
RunTaskRequest,
|
21
26
|
RunTaskResponse,
|
22
27
|
)
|
@@ -24,15 +29,14 @@ from .proto.function_executor_pb2_grpc import FunctionExecutorServicer
|
|
24
29
|
|
25
30
|
|
26
31
|
class FunctionExecutorService(FunctionExecutorServicer):
|
27
|
-
def __init__(self
|
28
|
-
self._indexify_server_address = indexify_server_address
|
29
|
-
self._config_path = config_path
|
32
|
+
def __init__(self):
|
30
33
|
self._logger = structlog.get_logger(module=__name__)
|
31
34
|
self._namespace: Optional[str] = None
|
32
35
|
self._graph_name: Optional[str] = None
|
33
36
|
self._graph_version: Optional[int] = None
|
34
37
|
self._function_name: Optional[str] = None
|
35
38
|
self._function: Optional[Union[IndexifyFunction, IndexifyRouter]] = None
|
39
|
+
self._invocation_state_proxy_server: Optional[InvocationStateProxyServer] = None
|
36
40
|
|
37
41
|
def initialize(
|
38
42
|
self, request: InitializeRequest, context: grpc.ServicerContext
|
@@ -65,6 +69,17 @@ class FunctionExecutorService(FunctionExecutorServicer):
|
|
65
69
|
|
66
70
|
return InitializeResponse(success=True)
|
67
71
|
|
72
|
+
def initialize_invocation_state_server(
|
73
|
+
self,
|
74
|
+
client_responses: Iterator[InvocationStateResponse],
|
75
|
+
context: grpc.ServicerContext,
|
76
|
+
):
|
77
|
+
self._invocation_state_proxy_server = InvocationStateProxyServer(
|
78
|
+
client_responses, self._logger
|
79
|
+
)
|
80
|
+
self._logger.info("initialized invocation proxy server")
|
81
|
+
yield from self._invocation_state_proxy_server.run()
|
82
|
+
|
68
83
|
def run_task(
|
69
84
|
self, request: RunTaskRequest, context: grpc.ServicerContext
|
70
85
|
) -> RunTaskResponse:
|
@@ -75,12 +90,12 @@ class FunctionExecutorService(FunctionExecutorServicer):
|
|
75
90
|
RunTaskRequestValidator(request=request).check()
|
76
91
|
return RunTaskHandler(
|
77
92
|
request=request,
|
78
|
-
namespace=self._namespace,
|
79
93
|
graph_name=self._graph_name,
|
80
94
|
graph_version=self._graph_version,
|
81
95
|
function_name=self._function_name,
|
82
96
|
function=self._function,
|
97
|
+
invocation_state=ProxiedInvocationState(
|
98
|
+
request.task_id, self._invocation_state_proxy_server
|
99
|
+
),
|
83
100
|
logger=self._logger,
|
84
|
-
indexify_server_addr=self._indexify_server_address,
|
85
|
-
config_path=self._config_path,
|
86
101
|
).run()
|
@@ -16,6 +16,9 @@ from indexify.functions_sdk.indexify_functions import (
|
|
16
16
|
IndexifyRouter,
|
17
17
|
RouterCallResult,
|
18
18
|
)
|
19
|
+
from indexify.functions_sdk.invocation_state.invocation_state import (
|
20
|
+
InvocationState,
|
21
|
+
)
|
19
22
|
from indexify.http_client import IndexifyClient
|
20
23
|
|
21
24
|
from .function_inputs_loader import FunctionInputs, FunctionInputsLoader
|
@@ -26,14 +29,12 @@ class Handler:
|
|
26
29
|
def __init__(
|
27
30
|
self,
|
28
31
|
request: RunTaskRequest,
|
29
|
-
namespace: str,
|
30
32
|
graph_name: str,
|
31
33
|
graph_version: int,
|
32
34
|
function_name: str,
|
33
35
|
function: Union[IndexifyFunction, IndexifyRouter],
|
36
|
+
invocation_state: InvocationState,
|
34
37
|
logger: Any,
|
35
|
-
indexify_server_addr: str,
|
36
|
-
config_path: Optional[str],
|
37
38
|
):
|
38
39
|
self._function_name: str = function_name
|
39
40
|
self._logger = logger.bind(
|
@@ -53,12 +54,7 @@ class Handler:
|
|
53
54
|
invocation_id=request.graph_invocation_id,
|
54
55
|
graph_name=graph_name,
|
55
56
|
graph_version=str(graph_version),
|
56
|
-
|
57
|
-
logger=self._logger,
|
58
|
-
namespace=namespace,
|
59
|
-
indexify_server_addr=indexify_server_addr,
|
60
|
-
config_path=config_path,
|
61
|
-
),
|
57
|
+
invocation_state=invocation_state,
|
62
58
|
),
|
63
59
|
)
|
64
60
|
|
@@ -139,9 +135,14 @@ def _indexify_client(
|
|
139
135
|
|
140
136
|
|
141
137
|
def _is_router(func_wrapper: IndexifyFunctionWrapper) -> bool:
|
142
|
-
|
143
|
-
|
144
|
-
|
138
|
+
"""Determines if the function is a router.
|
139
|
+
|
140
|
+
A function is a router if it is an instance of IndexifyRouter or if it is an IndexifyRouter class.
|
141
|
+
"""
|
142
|
+
return str(
|
143
|
+
type(func_wrapper.indexify_function)
|
144
|
+
) == "<class 'indexify.functions_sdk.indexify_functions.IndexifyRouter'>" or isinstance(
|
145
|
+
func_wrapper.indexify_function, IndexifyRouter
|
145
146
|
)
|
146
147
|
|
147
148
|
|
@@ -0,0 +1,170 @@
|
|
1
|
+
import queue
|
2
|
+
import threading
|
3
|
+
from typing import Any, Iterator, Optional
|
4
|
+
|
5
|
+
from indexify.functions_sdk.object_serializer import (
|
6
|
+
CloudPickleSerializer,
|
7
|
+
get_serializer,
|
8
|
+
)
|
9
|
+
|
10
|
+
from ..proto.function_executor_pb2 import (
|
11
|
+
GetInvocationStateRequest,
|
12
|
+
InvocationStateRequest,
|
13
|
+
InvocationStateResponse,
|
14
|
+
SerializedObject,
|
15
|
+
SetInvocationStateRequest,
|
16
|
+
)
|
17
|
+
from .response_validator import ResponseValidator
|
18
|
+
|
19
|
+
|
20
|
+
class InvocationStateProxyServer:
|
21
|
+
"""A gRPC server that proxies InvocationState calls to the gRPC client.
|
22
|
+
|
23
|
+
The gRPC client is responsible for the actual implementation of the InvocationState.
|
24
|
+
We do the proxying to remove authorization logic and credentials from Function Executor.
|
25
|
+
This improves security posture of Function Executor because it may run untrusted code.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self, client_responses: Iterator[InvocationStateResponse], logger: Any
|
30
|
+
):
|
31
|
+
self._client_responses: Iterator[InvocationStateResponse] = client_responses
|
32
|
+
self._logger: Any = logger.bind(module=__name__)
|
33
|
+
self._reciever_thread: threading.Thread = threading.Thread(
|
34
|
+
target=self._reciever
|
35
|
+
)
|
36
|
+
self._request_queue: queue.SimpleQueue = queue.SimpleQueue()
|
37
|
+
# This lock protects everything below.
|
38
|
+
self._lock: threading.Lock = threading.Lock()
|
39
|
+
# Python supports big integers natively so we don't need
|
40
|
+
# to be worried about interger overflows.
|
41
|
+
self._request_seq_num: int = 0
|
42
|
+
# Request ID -> Client Response.
|
43
|
+
self._response_map: dict[str, InvocationStateResponse] = {}
|
44
|
+
self._new_response: threading.Condition = threading.Condition(self._lock)
|
45
|
+
|
46
|
+
def run(self) -> Iterator[InvocationStateRequest]:
|
47
|
+
# There's no need to implement shutdown of the server and its threads because
|
48
|
+
# the server lives while the Function Executor process lives.
|
49
|
+
self._reciever_thread.start()
|
50
|
+
yield from self._sender()
|
51
|
+
|
52
|
+
def _reciever(self) -> None:
|
53
|
+
self._logger.info("reciever thread started")
|
54
|
+
try:
|
55
|
+
for response in self._client_responses:
|
56
|
+
validator = ResponseValidator(response)
|
57
|
+
try:
|
58
|
+
validator.check()
|
59
|
+
except ValueError as e:
|
60
|
+
self._logger.error("invalid response from the client", exc_info=e)
|
61
|
+
continue
|
62
|
+
|
63
|
+
with self._lock:
|
64
|
+
self._response_map[response.request_id] = response
|
65
|
+
self._new_response.notify_all()
|
66
|
+
except Exception as e:
|
67
|
+
self._logger.error("error in reciever thread, exiting", exc_info=e)
|
68
|
+
|
69
|
+
def _sender(self) -> Iterator[InvocationStateRequest]:
|
70
|
+
while True:
|
71
|
+
yield self._request_queue.get()
|
72
|
+
with self._lock:
|
73
|
+
# Wait until we get a response for the request.
|
74
|
+
# This allows to ensure a serialized order of reads and writes so
|
75
|
+
# we can avoid a read returning not previously written value.
|
76
|
+
self._new_response.wait()
|
77
|
+
|
78
|
+
def set(self, task_id: str, key: str, value: Any) -> None:
|
79
|
+
with self._lock:
|
80
|
+
request_id: str = str(self._request_seq_num)
|
81
|
+
self._request_seq_num += 1
|
82
|
+
|
83
|
+
# We currently use CloudPickleSerializer for function inputs,
|
84
|
+
# outputs and invocation state values. This provides consistent UX.
|
85
|
+
request = InvocationStateRequest(
|
86
|
+
request_id=request_id,
|
87
|
+
task_id=task_id,
|
88
|
+
set=SetInvocationStateRequest(
|
89
|
+
key=key,
|
90
|
+
value=SerializedObject(
|
91
|
+
content_type=CloudPickleSerializer.content_type,
|
92
|
+
bytes=CloudPickleSerializer.serialize(value),
|
93
|
+
),
|
94
|
+
),
|
95
|
+
)
|
96
|
+
self._request_queue.put(request)
|
97
|
+
while request_id not in self._response_map:
|
98
|
+
self._new_response.wait()
|
99
|
+
|
100
|
+
response: InvocationStateResponse = self._response_map.pop(request_id)
|
101
|
+
if response.request_id != request_id:
|
102
|
+
self._logger.error(
|
103
|
+
"response request_id doesn't match actual request_id",
|
104
|
+
request_id=request_id,
|
105
|
+
response=response,
|
106
|
+
)
|
107
|
+
raise RuntimeError(
|
108
|
+
"response request_id doesn't match actual request_id"
|
109
|
+
)
|
110
|
+
if not response.HasField("set"):
|
111
|
+
self._logger.error(
|
112
|
+
"set response is missing in the client response",
|
113
|
+
request_id=request_id,
|
114
|
+
response=response,
|
115
|
+
)
|
116
|
+
raise RuntimeError("set response is missing in the client response")
|
117
|
+
if not response.success:
|
118
|
+
self._logger.error(
|
119
|
+
"failed to set the invocation state for key",
|
120
|
+
key=key,
|
121
|
+
)
|
122
|
+
raise RuntimeError("failed to set the invocation state for key")
|
123
|
+
|
124
|
+
def get(self, task_id: str, key: str) -> Optional[Any]:
|
125
|
+
with self._lock:
|
126
|
+
request_id: str = str(self._request_seq_num)
|
127
|
+
self._request_seq_num += 1
|
128
|
+
|
129
|
+
request = InvocationStateRequest(
|
130
|
+
request_id=request_id,
|
131
|
+
task_id=task_id,
|
132
|
+
get=GetInvocationStateRequest(
|
133
|
+
key=key,
|
134
|
+
),
|
135
|
+
)
|
136
|
+
self._request_queue.put(request)
|
137
|
+
while request_id not in self._response_map:
|
138
|
+
self._new_response.wait()
|
139
|
+
|
140
|
+
response: InvocationStateResponse = self._response_map.pop(request_id)
|
141
|
+
if response.request_id != request_id:
|
142
|
+
self._logger.error(
|
143
|
+
"response request_id doesn't match actual request_id",
|
144
|
+
request_id=request_id,
|
145
|
+
response=response,
|
146
|
+
)
|
147
|
+
raise RuntimeError(
|
148
|
+
"response request_id doesn't match actual request_id"
|
149
|
+
)
|
150
|
+
if not response.HasField("get"):
|
151
|
+
self._logger.error(
|
152
|
+
"get response is missing in the client response",
|
153
|
+
request_id=request_id,
|
154
|
+
response=response,
|
155
|
+
)
|
156
|
+
raise RuntimeError("get response is missing in the client response")
|
157
|
+
if not response.success:
|
158
|
+
self._logger.error(
|
159
|
+
"failed to get the invocation state for key",
|
160
|
+
key=key,
|
161
|
+
)
|
162
|
+
raise RuntimeError("failed to get the invocation state for key")
|
163
|
+
if not response.get.HasField("value"):
|
164
|
+
return None
|
165
|
+
|
166
|
+
return get_serializer(response.get.value.content_type).deserialize(
|
167
|
+
response.get.value.bytes
|
168
|
+
if response.get.value.HasField("bytes")
|
169
|
+
else response.get.value.string
|
170
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from typing import Any, Optional
|
2
|
+
|
3
|
+
from indexify.functions_sdk.invocation_state.invocation_state import (
|
4
|
+
InvocationState,
|
5
|
+
)
|
6
|
+
|
7
|
+
from .invocation_state_proxy_server import InvocationStateProxyServer
|
8
|
+
|
9
|
+
|
10
|
+
class ProxiedInvocationState(InvocationState):
|
11
|
+
"""InvocationState that proxies the calls via InvocationStateProxyServer."""
|
12
|
+
|
13
|
+
def __init__(self, task_id: str, proxy_server: InvocationStateProxyServer):
|
14
|
+
self._task_id: str = task_id
|
15
|
+
self._proxy_server: InvocationStateProxyServer = proxy_server
|
16
|
+
|
17
|
+
def set(self, key: str, value: Any) -> None:
|
18
|
+
"""Set a key-value pair."""
|
19
|
+
self._proxy_server.set(self._task_id, key, value)
|
20
|
+
|
21
|
+
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
22
|
+
"""Get a value by key. If the key does not exist, return the default value."""
|
23
|
+
value: Optional[Any] = self._proxy_server.get(self._task_id, key)
|
24
|
+
return default if value is None else value
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from ..proto.function_executor_pb2 import InvocationStateResponse
|
2
|
+
from ..proto.message_validator import MessageValidator
|
3
|
+
|
4
|
+
|
5
|
+
class ResponseValidator(MessageValidator):
|
6
|
+
def __init__(self, response: InvocationStateResponse):
|
7
|
+
self._response = response
|
8
|
+
|
9
|
+
def check(self):
|
10
|
+
"""Validates the request.
|
11
|
+
|
12
|
+
Raises: ValueError: If the response is invalid.
|
13
|
+
"""
|
14
|
+
(
|
15
|
+
MessageValidator(self._response)
|
16
|
+
.required_field("request_id")
|
17
|
+
.required_field("success")
|
18
|
+
)
|
19
|
+
|
20
|
+
if self._response.HasField("set"):
|
21
|
+
pass
|
22
|
+
elif self._response.HasField("get"):
|
23
|
+
(
|
24
|
+
MessageValidator(self._response.get)
|
25
|
+
.required_field("key")
|
26
|
+
.optional_serialized_object("value")
|
27
|
+
)
|
28
|
+
else:
|
29
|
+
raise ValueError(f"Unknown response type: {self._response}")
|
@@ -33,6 +33,49 @@ message InitializeResponse {
|
|
33
33
|
optional bool success = 1;
|
34
34
|
}
|
35
35
|
|
36
|
+
message SetInvocationStateRequest {
|
37
|
+
optional string key = 1;
|
38
|
+
optional SerializedObject value = 2;
|
39
|
+
}
|
40
|
+
|
41
|
+
message SetInvocationStateResponse {}
|
42
|
+
|
43
|
+
message GetInvocationStateRequest {
|
44
|
+
optional string key = 1;
|
45
|
+
}
|
46
|
+
|
47
|
+
message GetInvocationStateResponse {
|
48
|
+
optional string key = 1;
|
49
|
+
optional SerializedObject value = 2;
|
50
|
+
}
|
51
|
+
|
52
|
+
// InvocationStateRequest is sent by RPC Server to the client
|
53
|
+
// to perform actions on a task's graph invocation state.
|
54
|
+
message InvocationStateRequest {
|
55
|
+
// The ID of the request sent by the client.
|
56
|
+
// Must be unique per Function Executor.
|
57
|
+
optional string request_id = 1;
|
58
|
+
// The ID of the task initiated the request.
|
59
|
+
optional string task_id = 2;
|
60
|
+
oneof request {
|
61
|
+
SetInvocationStateRequest set = 3;
|
62
|
+
GetInvocationStateRequest get = 4;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
// InvocationStateResponse is sent by RPC client to the Server.
|
67
|
+
// A response contains the result of the action performed on the
|
68
|
+
// task's graph invocation state.
|
69
|
+
message InvocationStateResponse {
|
70
|
+
// The id of the request this response is for.
|
71
|
+
optional string request_id = 1;
|
72
|
+
optional bool success = 2;
|
73
|
+
oneof response {
|
74
|
+
SetInvocationStateResponse set = 3;
|
75
|
+
GetInvocationStateResponse get = 4;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
36
79
|
message FunctionOutput {
|
37
80
|
repeated SerializedObject outputs = 1;
|
38
81
|
}
|
@@ -64,6 +107,10 @@ service FunctionExecutor {
|
|
64
107
|
// once per Function Executor as it can only run a single function.
|
65
108
|
// It should be called before calling RunTask for the function.
|
66
109
|
rpc initialize(InitializeRequest) returns (InitializeResponse);
|
110
|
+
// Initializes a server that sends requests to the client to perform actions on
|
111
|
+
// a task's graph invocation state. This method is called only once per Function Executor
|
112
|
+
// It should be called before calling RunTask for the function.
|
113
|
+
rpc initialize_invocation_state_server(stream InvocationStateResponse) returns (stream InvocationStateRequest);
|
67
114
|
// Executes the task defined in the request.
|
68
115
|
// Multiple tasks can be running in parallel.
|
69
116
|
rpc run_task(RunTaskRequest) returns (RunTaskResponse);
|
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
|
|
24
24
|
|
25
25
|
|
26
26
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
27
|
-
b'\n8indexify/function_executor/proto/function_executor.proto\x12\x19\x66unction_executor_service"i\n\x10SerializedObject\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12\x10\n\x06string\x18\x02 \x01(\tH\x00\x12\x19\n\x0c\x63ontent_type\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x06\n\x04\x64\x61taB\x0f\n\r_content_type"\x88\x02\n\x11InitializeRequest\x12\x16\n\tnamespace\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ngraph_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x1a\n\rgraph_version\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x1a\n\rfunction_name\x18\x05 \x01(\tH\x03\x88\x01\x01\x12?\n\x05graph\x18\x07 \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x04\x88\x01\x01\x42\x0c\n\n_namespaceB\r\n\x0b_graph_nameB\x10\n\x0e_graph_versionB\x10\n\x0e_function_nameB\x08\n\x06_graph"6\n\x12InitializeResponse\x12\x14\n\x07success\x18\x01 \x01(\x08H\x00\x88\x01\x01\x42\n\n\x08_success"N\n\x0e\x46unctionOutput\x12<\n\x07outputs\x18\x01 \x03(\x0b\x32+.function_executor_service.SerializedObject"\x1d\n\x0cRouterOutput\x12\r\n\x05\x65\x64ges\x18\x01 \x03(\t"\xb0\x02\n\x0eRunTaskRequest\x12 \n\x13graph_invocation_id\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07task_id\x18\x06 \x01(\tH\x01\x88\x01\x01\x12H\n\x0e\x66unction_input\x18\t \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x02\x88\x01\x01\x12M\n\x13\x66unction_init_value\x18\n \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x03\x88\x01\x01\x42\x16\n\x14_graph_invocation_idB\n\n\x08_task_idB\x11\n\x0f_function_inputB\x16\n\x14_function_init_value"\xf1\x02\n\x0fRunTaskResponse\x12\x14\n\x07task_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12G\n\x0f\x66unction_output\x18\x02 \x01(\x0b\x32).function_executor_service.FunctionOutputH\x01\x88\x01\x01\x12\x43\n\rrouter_output\x18\x03 \x01(\x0b\x32\'.function_executor_service.RouterOutputH\x02\x88\x01\x01\x12\x13\n\x06stdout\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x13\n\x06stderr\x18\x05 \x01(\tH\x04\x88\x01\x01\x12\x17\n\nis_reducer\x18\x06 \x01(\x08H\x05\x88\x01\x01\x12\x14\n\x07success\x18\x07 \x01(\x08H\x06\x88\x01\x01\x42\n\n\x08_task_idB\x12\n\x10_function_outputB\x10\n\x0e_router_outputB\t\n\x07_stdoutB\t\n\x07_stderrB\r\n\x0b_is_reducerB\n\n\x08_success2\
|
27
|
+
b'\n8indexify/function_executor/proto/function_executor.proto\x12\x19\x66unction_executor_service"i\n\x10SerializedObject\x12\x0f\n\x05\x62ytes\x18\x01 \x01(\x0cH\x00\x12\x10\n\x06string\x18\x02 \x01(\tH\x00\x12\x19\n\x0c\x63ontent_type\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x06\n\x04\x64\x61taB\x0f\n\r_content_type"\x88\x02\n\x11InitializeRequest\x12\x16\n\tnamespace\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ngraph_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x1a\n\rgraph_version\x18\x03 \x01(\x05H\x02\x88\x01\x01\x12\x1a\n\rfunction_name\x18\x05 \x01(\tH\x03\x88\x01\x01\x12?\n\x05graph\x18\x07 \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x04\x88\x01\x01\x42\x0c\n\n_namespaceB\r\n\x0b_graph_nameB\x10\n\x0e_graph_versionB\x10\n\x0e_function_nameB\x08\n\x06_graph"6\n\x12InitializeResponse\x12\x14\n\x07success\x18\x01 \x01(\x08H\x00\x88\x01\x01\x42\n\n\x08_success"\x80\x01\n\x19SetInvocationStateRequest\x12\x10\n\x03key\x18\x01 \x01(\tH\x00\x88\x01\x01\x12?\n\x05value\x18\x02 \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x01\x88\x01\x01\x42\x06\n\x04_keyB\x08\n\x06_value"\x1c\n\x1aSetInvocationStateResponse"5\n\x19GetInvocationStateRequest\x12\x10\n\x03key\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_key"\x81\x01\n\x1aGetInvocationStateResponse\x12\x10\n\x03key\x18\x01 \x01(\tH\x00\x88\x01\x01\x12?\n\x05value\x18\x02 \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x01\x88\x01\x01\x42\x06\n\x04_keyB\x08\n\x06_value"\xf7\x01\n\x16InvocationStateRequest\x12\x17\n\nrequest_id\x18\x01 \x01(\tH\x01\x88\x01\x01\x12\x14\n\x07task_id\x18\x02 \x01(\tH\x02\x88\x01\x01\x12\x43\n\x03set\x18\x03 \x01(\x0b\x32\x34.function_executor_service.SetInvocationStateRequestH\x00\x12\x43\n\x03get\x18\x04 \x01(\x0b\x32\x34.function_executor_service.GetInvocationStateRequestH\x00\x42\t\n\x07requestB\r\n\x0b_request_idB\n\n\x08_task_id"\xfb\x01\n\x17InvocationStateResponse\x12\x17\n\nrequest_id\x18\x01 \x01(\tH\x01\x88\x01\x01\x12\x14\n\x07success\x18\x02 \x01(\x08H\x02\x88\x01\x01\x12\x44\n\x03set\x18\x03 \x01(\x0b\x32\x35.function_executor_service.SetInvocationStateResponseH\x00\x12\x44\n\x03get\x18\x04 \x01(\x0b\x32\x35.function_executor_service.GetInvocationStateResponseH\x00\x42\n\n\x08responseB\r\n\x0b_request_idB\n\n\x08_success"N\n\x0e\x46unctionOutput\x12<\n\x07outputs\x18\x01 \x03(\x0b\x32+.function_executor_service.SerializedObject"\x1d\n\x0cRouterOutput\x12\r\n\x05\x65\x64ges\x18\x01 \x03(\t"\xb0\x02\n\x0eRunTaskRequest\x12 \n\x13graph_invocation_id\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07task_id\x18\x06 \x01(\tH\x01\x88\x01\x01\x12H\n\x0e\x66unction_input\x18\t \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x02\x88\x01\x01\x12M\n\x13\x66unction_init_value\x18\n \x01(\x0b\x32+.function_executor_service.SerializedObjectH\x03\x88\x01\x01\x42\x16\n\x14_graph_invocation_idB\n\n\x08_task_idB\x11\n\x0f_function_inputB\x16\n\x14_function_init_value"\xf1\x02\n\x0fRunTaskResponse\x12\x14\n\x07task_id\x18\x01 \x01(\tH\x00\x88\x01\x01\x12G\n\x0f\x66unction_output\x18\x02 \x01(\x0b\x32).function_executor_service.FunctionOutputH\x01\x88\x01\x01\x12\x43\n\rrouter_output\x18\x03 \x01(\x0b\x32\'.function_executor_service.RouterOutputH\x02\x88\x01\x01\x12\x13\n\x06stdout\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x13\n\x06stderr\x18\x05 \x01(\tH\x04\x88\x01\x01\x12\x17\n\nis_reducer\x18\x06 \x01(\x08H\x05\x88\x01\x01\x12\x14\n\x07success\x18\x07 \x01(\x08H\x06\x88\x01\x01\x42\n\n\x08_task_idB\x12\n\x10_function_outputB\x10\n\x0e_router_outputB\t\n\x07_stdoutB\t\n\x07_stderrB\r\n\x0b_is_reducerB\n\n\x08_success2\xf2\x02\n\x10\x46unctionExecutor\x12i\n\ninitialize\x12,.function_executor_service.InitializeRequest\x1a-.function_executor_service.InitializeResponse\x12\x8f\x01\n"initialize_invocation_state_server\x12\x32.function_executor_service.InvocationStateResponse\x1a\x31.function_executor_service.InvocationStateRequest(\x01\x30\x01\x12\x61\n\x08run_task\x12).function_executor_service.RunTaskRequest\x1a*.function_executor_service.RunTaskResponseb\x06proto3'
|
28
28
|
)
|
29
29
|
|
30
30
|
_globals = globals()
|
@@ -40,14 +40,26 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|
40
40
|
_globals["_INITIALIZEREQUEST"]._serialized_end = 459
|
41
41
|
_globals["_INITIALIZERESPONSE"]._serialized_start = 461
|
42
42
|
_globals["_INITIALIZERESPONSE"]._serialized_end = 515
|
43
|
-
_globals["
|
44
|
-
_globals["
|
45
|
-
_globals["
|
46
|
-
_globals["
|
47
|
-
_globals["
|
48
|
-
_globals["
|
49
|
-
_globals["
|
50
|
-
_globals["
|
51
|
-
_globals["
|
52
|
-
_globals["
|
43
|
+
_globals["_SETINVOCATIONSTATEREQUEST"]._serialized_start = 518
|
44
|
+
_globals["_SETINVOCATIONSTATEREQUEST"]._serialized_end = 646
|
45
|
+
_globals["_SETINVOCATIONSTATERESPONSE"]._serialized_start = 648
|
46
|
+
_globals["_SETINVOCATIONSTATERESPONSE"]._serialized_end = 676
|
47
|
+
_globals["_GETINVOCATIONSTATEREQUEST"]._serialized_start = 678
|
48
|
+
_globals["_GETINVOCATIONSTATEREQUEST"]._serialized_end = 731
|
49
|
+
_globals["_GETINVOCATIONSTATERESPONSE"]._serialized_start = 734
|
50
|
+
_globals["_GETINVOCATIONSTATERESPONSE"]._serialized_end = 863
|
51
|
+
_globals["_INVOCATIONSTATEREQUEST"]._serialized_start = 866
|
52
|
+
_globals["_INVOCATIONSTATEREQUEST"]._serialized_end = 1113
|
53
|
+
_globals["_INVOCATIONSTATERESPONSE"]._serialized_start = 1116
|
54
|
+
_globals["_INVOCATIONSTATERESPONSE"]._serialized_end = 1367
|
55
|
+
_globals["_FUNCTIONOUTPUT"]._serialized_start = 1369
|
56
|
+
_globals["_FUNCTIONOUTPUT"]._serialized_end = 1447
|
57
|
+
_globals["_ROUTEROUTPUT"]._serialized_start = 1449
|
58
|
+
_globals["_ROUTEROUTPUT"]._serialized_end = 1478
|
59
|
+
_globals["_RUNTASKREQUEST"]._serialized_start = 1481
|
60
|
+
_globals["_RUNTASKREQUEST"]._serialized_end = 1785
|
61
|
+
_globals["_RUNTASKRESPONSE"]._serialized_start = 1788
|
62
|
+
_globals["_RUNTASKRESPONSE"]._serialized_end = 2157
|
63
|
+
_globals["_FUNCTIONEXECUTOR"]._serialized_start = 2160
|
64
|
+
_globals["_FUNCTIONEXECUTOR"]._serialized_end = 2530
|
53
65
|
# @@protoc_insertion_point(module_scope)
|