indexify 0.2.48__py3-none-any.whl → 0.3.0__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/{cli.py → cli/cli.py} +75 -82
- indexify/executor/README.md +35 -0
- indexify/executor/api_objects.py +9 -3
- indexify/executor/downloader.py +5 -5
- indexify/executor/executor.py +35 -22
- indexify/executor/function_executor/function_executor.py +14 -3
- indexify/executor/function_executor/function_executor_state.py +13 -10
- indexify/executor/function_executor/invocation_state_client.py +2 -1
- indexify/executor/function_executor/server/subprocess_function_executor_server_factory.py +22 -10
- indexify/executor/function_executor/single_task_runner.py +43 -26
- indexify/executor/function_executor/task_input.py +1 -3
- indexify/executor/task_fetcher.py +5 -7
- indexify/executor/task_reporter.py +3 -5
- indexify/executor/task_runner.py +31 -24
- indexify/function_executor/README.md +18 -0
- indexify/function_executor/handlers/run_function/function_inputs_loader.py +13 -14
- indexify/function_executor/handlers/run_function/handler.py +16 -40
- indexify/function_executor/handlers/run_function/request_validator.py +7 -5
- indexify/function_executor/handlers/run_function/response_helper.py +6 -8
- indexify/function_executor/initialize_request_validator.py +1 -2
- indexify/function_executor/invocation_state/invocation_state_proxy_server.py +1 -1
- indexify/function_executor/invocation_state/proxied_invocation_state.py +1 -3
- indexify/function_executor/main.py +50 -0
- indexify/function_executor/proto/configuration.py +8 -0
- indexify/function_executor/proto/function_executor.proto +9 -4
- indexify/function_executor/proto/function_executor_pb2.py +24 -24
- indexify/function_executor/proto/function_executor_pb2.pyi +24 -4
- indexify/function_executor/server.py +4 -6
- indexify/function_executor/{function_executor_service.py → service.py} +35 -24
- indexify/utils/README.md +3 -0
- indexify/{common_util.py → utils/http_client.py} +2 -2
- indexify/{logging.py → utils/logging.py} +36 -2
- indexify-0.3.0.dist-info/METADATA +38 -0
- indexify-0.3.0.dist-info/RECORD +44 -0
- {indexify-0.2.48.dist-info → indexify-0.3.0.dist-info}/WHEEL +1 -1
- indexify-0.3.0.dist-info/entry_points.txt +4 -0
- indexify/__init__.py +0 -31
- indexify/data_loaders/__init__.py +0 -58
- indexify/data_loaders/local_directory_loader.py +0 -37
- indexify/data_loaders/url_loader.py +0 -52
- indexify/error.py +0 -8
- indexify/functions_sdk/data_objects.py +0 -27
- indexify/functions_sdk/graph.py +0 -364
- indexify/functions_sdk/graph_definition.py +0 -63
- indexify/functions_sdk/graph_validation.py +0 -70
- indexify/functions_sdk/image.py +0 -222
- indexify/functions_sdk/indexify_functions.py +0 -354
- indexify/functions_sdk/invocation_state/invocation_state.py +0 -22
- indexify/functions_sdk/invocation_state/local_invocation_state.py +0 -30
- indexify/functions_sdk/object_serializer.py +0 -68
- indexify/functions_sdk/pipeline.py +0 -33
- indexify/http_client.py +0 -379
- indexify/remote_graph.py +0 -138
- indexify/remote_pipeline.py +0 -25
- indexify/settings.py +0 -1
- indexify-0.2.48.dist-info/LICENSE.txt +0 -201
- indexify-0.2.48.dist-info/METADATA +0 -154
- indexify-0.2.48.dist-info/RECORD +0 -60
- indexify-0.2.48.dist-info/entry_points.txt +0 -3
indexify/{cli.py → cli/cli.py}
RENAMED
@@ -1,8 +1,11 @@
|
|
1
|
-
from .logging import
|
1
|
+
from indexify.utils.logging import (
|
2
|
+
configure_development_mode_logging,
|
3
|
+
configure_logging_early,
|
4
|
+
configure_production_mode_logging,
|
5
|
+
)
|
2
6
|
|
3
7
|
configure_logging_early()
|
4
8
|
|
5
|
-
import asyncio
|
6
9
|
import os
|
7
10
|
import shutil
|
8
11
|
import signal
|
@@ -11,25 +14,22 @@ import sys
|
|
11
14
|
import threading
|
12
15
|
import time
|
13
16
|
from importlib.metadata import version
|
14
|
-
from
|
17
|
+
from pathlib import Path
|
18
|
+
from typing import Annotated, List, Optional, Tuple
|
15
19
|
|
16
20
|
import nanoid
|
17
21
|
import structlog
|
18
22
|
import typer
|
19
23
|
from rich.console import Console
|
20
|
-
from rich.panel import Panel
|
21
24
|
from rich.text import Text
|
22
25
|
from rich.theme import Theme
|
26
|
+
from tensorlake.functions_sdk.image import GetDefaultPythonImage, Image
|
23
27
|
|
28
|
+
from indexify.executor.api_objects import FunctionURI
|
24
29
|
from indexify.executor.executor import Executor
|
25
30
|
from indexify.executor.function_executor.server.subprocess_function_executor_server_factory import (
|
26
31
|
SubprocessFunctionExecutorServerFactory,
|
27
32
|
)
|
28
|
-
from indexify.function_executor.function_executor_service import (
|
29
|
-
FunctionExecutorService,
|
30
|
-
)
|
31
|
-
from indexify.function_executor.server import Server as FunctionExecutorServer
|
32
|
-
from indexify.functions_sdk.image import Build, GetDefaultPythonImage, Image
|
33
33
|
|
34
34
|
logger = structlog.get_logger(module=__name__)
|
35
35
|
|
@@ -71,8 +71,10 @@ def server_dev_mode():
|
|
71
71
|
print("starting indexify server and executor in dev mode...")
|
72
72
|
print("press Ctrl+C to stop the server and executor.")
|
73
73
|
print(f"server binary path: {indexify_server_path}")
|
74
|
-
commands = [
|
75
|
-
|
74
|
+
commands: List[List[str]] = [
|
75
|
+
[indexify_server_path, "--dev"],
|
76
|
+
["indexify-cli", "executor", "--dev"],
|
77
|
+
]
|
76
78
|
processes = []
|
77
79
|
stop_event = threading.Event()
|
78
80
|
|
@@ -103,7 +105,7 @@ def server_dev_mode():
|
|
103
105
|
|
104
106
|
for cmd in commands:
|
105
107
|
process = subprocess.Popen(
|
106
|
-
cmd
|
108
|
+
cmd,
|
107
109
|
stdout=subprocess.PIPE,
|
108
110
|
stderr=subprocess.STDOUT,
|
109
111
|
bufsize=1,
|
@@ -155,32 +157,6 @@ def build_image(
|
|
155
157
|
_create_image(obj, python_sdk_path)
|
156
158
|
|
157
159
|
|
158
|
-
@app.command(help="Build platform images for function names")
|
159
|
-
def build_platform_image(
|
160
|
-
workflow_file_path: Annotated[str, typer.Argument()],
|
161
|
-
image_names: Optional[List[str]] = None,
|
162
|
-
build_service="https://api.tensorlake.ai/images/v1",
|
163
|
-
):
|
164
|
-
|
165
|
-
globals_dict = {}
|
166
|
-
|
167
|
-
# Add the folder in the workflow file path to the current Python path
|
168
|
-
folder_path = os.path.dirname(workflow_file_path)
|
169
|
-
if folder_path not in sys.path:
|
170
|
-
sys.path.append(folder_path)
|
171
|
-
|
172
|
-
try:
|
173
|
-
exec(open(workflow_file_path).read(), globals_dict)
|
174
|
-
except FileNotFoundError as e:
|
175
|
-
raise Exception(
|
176
|
-
f"Could not find workflow file to execute at: " f"`{workflow_file_path}`"
|
177
|
-
)
|
178
|
-
for _, obj in globals_dict.items():
|
179
|
-
if type(obj) and isinstance(obj, Image):
|
180
|
-
if image_names is None or obj._image_name in image_names:
|
181
|
-
_create_platform_image(obj, build_service)
|
182
|
-
|
183
|
-
|
184
160
|
@app.command(help="Build default image for indexify")
|
185
161
|
def build_default_image(
|
186
162
|
python_version: Optional[str] = typer.Option(
|
@@ -200,87 +176,104 @@ def build_default_image(
|
|
200
176
|
)
|
201
177
|
|
202
178
|
|
203
|
-
@app.command(
|
179
|
+
@app.command(
|
180
|
+
help="Runs Executor that connects to the Indexify server and starts running its tasks"
|
181
|
+
)
|
204
182
|
def executor(
|
205
183
|
server_addr: str = "localhost:8900",
|
206
184
|
dev: Annotated[
|
207
185
|
bool, typer.Option("--dev", "-d", help="Run the executor in development mode")
|
208
186
|
] = False,
|
187
|
+
function_uris: Annotated[
|
188
|
+
Optional[List[str]],
|
189
|
+
typer.Option(
|
190
|
+
"--function",
|
191
|
+
"-f",
|
192
|
+
help="Function that the executor will run "
|
193
|
+
"specified as <namespace>:<workflow>:<function>:<version>",
|
194
|
+
),
|
195
|
+
] = None,
|
209
196
|
config_path: Optional[str] = typer.Option(
|
210
197
|
None, help="Path to the TLS configuration file"
|
211
198
|
),
|
212
199
|
executor_cache: Optional[str] = typer.Option(
|
213
200
|
"~/.indexify/executor_cache", help="Path to the executor cache directory"
|
214
201
|
),
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
image_hash: Optional[str] = typer.Option(
|
219
|
-
None, help="Image hash override for the executor"
|
202
|
+
# Registred ports range ends at 49151.
|
203
|
+
ports: Tuple[int, int] = typer.Option(
|
204
|
+
(50000, 51000), help="Range of localhost TCP ports to be used by the executor"
|
220
205
|
),
|
221
206
|
):
|
222
|
-
if
|
223
|
-
|
207
|
+
if dev:
|
208
|
+
configure_development_mode_logging()
|
209
|
+
else:
|
210
|
+
configure_production_mode_logging()
|
211
|
+
if function_uris is None:
|
212
|
+
raise typer.BadParameter(
|
213
|
+
"At least one function must be specified when not running in development mode"
|
214
|
+
)
|
224
215
|
|
225
216
|
id = nanoid.generate()
|
226
|
-
executor_version = version("indexify")
|
227
217
|
logger.info(
|
228
|
-
"executor
|
218
|
+
"starting executor",
|
229
219
|
server_addr=server_addr,
|
230
220
|
config_path=config_path,
|
231
221
|
executor_id=id,
|
232
|
-
executor_version=
|
222
|
+
executor_version=version("indexify"),
|
233
223
|
executor_cache=executor_cache,
|
234
|
-
|
235
|
-
|
224
|
+
ports=ports,
|
225
|
+
functions=function_uris,
|
236
226
|
dev_mode=dev,
|
237
227
|
)
|
238
228
|
|
239
|
-
from pathlib import Path
|
240
|
-
|
241
229
|
executor_cache = Path(executor_cache).expanduser().absolute()
|
242
230
|
if os.path.exists(executor_cache):
|
243
231
|
shutil.rmtree(executor_cache)
|
244
232
|
Path(executor_cache).mkdir(parents=True, exist_ok=True)
|
245
233
|
|
246
|
-
|
234
|
+
start_port: int = ports[0]
|
235
|
+
end_port: int = ports[1]
|
236
|
+
if start_port >= end_port:
|
237
|
+
console.print(
|
238
|
+
Text(
|
239
|
+
f"start port {start_port} should be less than {end_port}", style="red"
|
240
|
+
),
|
241
|
+
)
|
242
|
+
exit(1)
|
243
|
+
|
244
|
+
Executor(
|
247
245
|
id,
|
248
246
|
server_addr=server_addr,
|
249
247
|
config_path=config_path,
|
250
248
|
code_path=executor_cache,
|
251
|
-
|
252
|
-
image_hash=image_hash,
|
249
|
+
function_allowlist=_parse_function_uris(function_uris),
|
253
250
|
function_executor_server_factory=SubprocessFunctionExecutorServerFactory(
|
254
|
-
development_mode=dev
|
251
|
+
development_mode=dev,
|
252
|
+
server_ports=range(ports[0], ports[1]),
|
255
253
|
),
|
256
|
-
)
|
257
|
-
try:
|
258
|
-
asyncio.get_event_loop().run_until_complete(executor.run())
|
259
|
-
except asyncio.CancelledError:
|
260
|
-
logger.info("graceful shutdown")
|
254
|
+
).run()
|
261
255
|
|
262
256
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
help="Function Executor server address"
|
267
|
-
),
|
268
|
-
dev: Annotated[
|
269
|
-
bool, typer.Option("--dev", "-d", help="Run the executor in development mode")
|
270
|
-
] = False,
|
271
|
-
):
|
272
|
-
if not dev:
|
273
|
-
configure_production_logging()
|
257
|
+
def _parse_function_uris(uri_strs: Optional[List[str]]) -> Optional[List[FunctionURI]]:
|
258
|
+
if uri_strs is None:
|
259
|
+
return None
|
274
260
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
261
|
+
uris: List[FunctionURI] = []
|
262
|
+
for uri_str in uri_strs:
|
263
|
+
tokens = uri_str.split(":")
|
264
|
+
if len(tokens) != 4:
|
265
|
+
raise typer.BadParameter(
|
266
|
+
"Function should be specified as <namespace>:<workflow>:<function>:<version>"
|
267
|
+
)
|
268
|
+
uris.append(
|
269
|
+
FunctionURI(
|
270
|
+
namespace=tokens[0],
|
271
|
+
compute_graph=tokens[1],
|
272
|
+
compute_fn=tokens[2],
|
273
|
+
version=tokens[3],
|
274
|
+
)
|
275
|
+
)
|
276
|
+
return uris
|
284
277
|
|
285
278
|
|
286
279
|
def _create_image(image: Image, python_sdk_path):
|
@@ -0,0 +1,35 @@
|
|
1
|
+
## Overview
|
2
|
+
|
3
|
+
Executor registers at Indexify Server and continuously pulls tasks assigned to it from the Indexify Server
|
4
|
+
and executes them. While registering it shares its capabilities like available hardware with the Indexify
|
5
|
+
Server and periodically updates the Server about its current state. Executor spins up Function Executors
|
6
|
+
to run customer functions. Executor should never link with Tensorlake Python-SDK. It should not know anything
|
7
|
+
about programming languages and runtime environments used by Tensorlake Functions. Function Executor is
|
8
|
+
responsible for this.
|
9
|
+
|
10
|
+
This subpackage doesn't provide an executable entry point that runs an Executor. This is intentional
|
11
|
+
as Executor has many configurable sub-components. indexify cli subpackage provides `executor`
|
12
|
+
command that runs Executor with functionality available in Open Source offering.
|
13
|
+
|
14
|
+
## Deployment
|
15
|
+
|
16
|
+
### Production setup
|
17
|
+
|
18
|
+
A single Executor runs in a Virtual Machine, container or a in bare metal host. An Indexify cluster
|
19
|
+
is scaled by adding more Executor hosts. Open Source users manage and scale the hosts themselves e.g.
|
20
|
+
using Kubernetes, any other orchestrator or even manually. E.g. the users provision secrets,
|
21
|
+
persistent volumes to each host using the orchestrator or manually. Each Executor runs a single function.
|
22
|
+
The function name and other qualifiers are defined in Executor arguments.
|
23
|
+
|
24
|
+
### Development setup
|
25
|
+
|
26
|
+
To make Indexify development and testing easier an Executor in development mode can run any function.
|
27
|
+
Running multiple Executors on the same host is supported too. In this case each Executor requires a
|
28
|
+
unique port range passed to it in its arguments.
|
29
|
+
|
30
|
+
## Threat model
|
31
|
+
|
32
|
+
A VM/container/bare metal host where an Executor is running is fully trusted. This works well for single
|
33
|
+
tenant deployments where customer functions' code is fully trusted. If this is not the case then Function
|
34
|
+
Executors that run customer functions need to get isolated from Executor using e.g. Virtual Machines.
|
35
|
+
This functionality is not included into the Open Source offering.
|
indexify/executor/api_objects.py
CHANGED
@@ -11,17 +11,23 @@ class Task(BaseModel):
|
|
11
11
|
invocation_id: str
|
12
12
|
input_key: str
|
13
13
|
reducer_output_id: Optional[str] = None
|
14
|
-
graph_version:
|
14
|
+
graph_version: str
|
15
15
|
image_uri: Optional[str] = None
|
16
16
|
"image_uri defines the URI of the image of this task. Optional since some executors do not require it."
|
17
17
|
|
18
18
|
|
19
|
+
class FunctionURI(BaseModel):
|
20
|
+
namespace: str
|
21
|
+
compute_graph: str
|
22
|
+
compute_fn: str
|
23
|
+
version: str
|
24
|
+
|
25
|
+
|
19
26
|
class ExecutorMetadata(BaseModel):
|
20
27
|
id: str
|
21
28
|
executor_version: str
|
22
29
|
addr: str
|
23
|
-
|
24
|
-
image_hash: str
|
30
|
+
function_allowlist: Optional[List[FunctionURI]] = None
|
25
31
|
labels: Dict[str, Any]
|
26
32
|
|
27
33
|
|
indexify/executor/downloader.py
CHANGED
@@ -5,11 +5,9 @@ from typing import Any, Optional
|
|
5
5
|
import httpx
|
6
6
|
import structlog
|
7
7
|
|
8
|
-
from indexify.function_executor.proto.function_executor_pb2 import
|
9
|
-
|
10
|
-
)
|
8
|
+
from indexify.function_executor.proto.function_executor_pb2 import SerializedObject
|
9
|
+
from indexify.utils.http_client import get_httpx_client
|
11
10
|
|
12
|
-
from ..common_util import get_httpx_client
|
13
11
|
from .api_objects import Task
|
14
12
|
|
15
13
|
|
@@ -27,7 +25,8 @@ class Downloader:
|
|
27
25
|
self.code_path,
|
28
26
|
"graph_cache",
|
29
27
|
task.namespace,
|
30
|
-
|
28
|
+
task.compute_graph,
|
29
|
+
task.graph_version,
|
31
30
|
)
|
32
31
|
# Filesystem operations are synchronous.
|
33
32
|
# Run in a separate thread to not block the main event loop.
|
@@ -70,6 +69,7 @@ class Downloader:
|
|
70
69
|
# Atomically rename the fully written file at tmp path.
|
71
70
|
# This allows us to not use any locking because file link/unlink
|
72
71
|
# are atomic operations at filesystem level.
|
72
|
+
# This also allows to share the same cache between multiple Executors.
|
73
73
|
os.replace(tmp_path, path)
|
74
74
|
|
75
75
|
async def download_input(self, task: Task) -> SerializedObject:
|
indexify/executor/executor.py
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
import asyncio
|
2
2
|
import signal
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import Any, Optional
|
4
|
+
from typing import Any, List, Optional
|
5
5
|
|
6
6
|
import structlog
|
7
7
|
|
8
|
-
from indexify.function_executor.proto.function_executor_pb2 import
|
9
|
-
|
10
|
-
)
|
8
|
+
from indexify.function_executor.proto.function_executor_pb2 import SerializedObject
|
9
|
+
from indexify.utils.logging import suppress as suppress_logging
|
11
10
|
|
12
|
-
from .api_objects import Task
|
11
|
+
from .api_objects import FunctionURI, Task
|
13
12
|
from .downloader import Downloader
|
14
13
|
from .function_executor.server.function_executor_server_factory import (
|
15
14
|
FunctionExecutorServerFactory,
|
@@ -24,14 +23,13 @@ class Executor:
|
|
24
23
|
self,
|
25
24
|
executor_id: str,
|
26
25
|
code_path: Path,
|
26
|
+
function_allowlist: Optional[List[FunctionURI]],
|
27
27
|
function_executor_server_factory: FunctionExecutorServerFactory,
|
28
28
|
server_addr: str = "localhost:8900",
|
29
29
|
config_path: Optional[str] = None,
|
30
|
-
name_alias: Optional[str] = None,
|
31
|
-
image_hash: Optional[str] = None,
|
32
30
|
):
|
33
31
|
self._logger = structlog.get_logger(module=__name__)
|
34
|
-
self.
|
32
|
+
self._is_shutdown: bool = False
|
35
33
|
self._config_path = config_path
|
36
34
|
protocol: str = "http"
|
37
35
|
if config_path:
|
@@ -41,7 +39,7 @@ class Executor:
|
|
41
39
|
self._server_addr = server_addr
|
42
40
|
self._base_url = f"{protocol}://{self._server_addr}"
|
43
41
|
self._code_path = code_path
|
44
|
-
self.
|
42
|
+
self._task_runner = TaskRunner(
|
45
43
|
function_executor_server_factory=function_executor_server_factory,
|
46
44
|
base_url=self._base_url,
|
47
45
|
config_path=config_path,
|
@@ -53,8 +51,7 @@ class Executor:
|
|
53
51
|
protocol=protocol,
|
54
52
|
indexify_server_addr=self._server_addr,
|
55
53
|
executor_id=executor_id,
|
56
|
-
|
57
|
-
image_hash=image_hash,
|
54
|
+
function_allowlist=function_allowlist,
|
58
55
|
config_path=config_path,
|
59
56
|
)
|
60
57
|
self._task_reporter = TaskReporter(
|
@@ -63,15 +60,25 @@ class Executor:
|
|
63
60
|
config_path=self._config_path,
|
64
61
|
)
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
signal.
|
69
|
-
|
70
|
-
|
71
|
-
signal.
|
72
|
-
|
63
|
+
def run(self):
|
64
|
+
for signum in [
|
65
|
+
signal.SIGABRT,
|
66
|
+
signal.SIGINT,
|
67
|
+
signal.SIGTERM,
|
68
|
+
signal.SIGQUIT,
|
69
|
+
signal.SIGHUP,
|
70
|
+
]:
|
71
|
+
asyncio.get_event_loop().add_signal_handler(
|
72
|
+
signum, self.shutdown, asyncio.get_event_loop()
|
73
|
+
)
|
74
|
+
|
75
|
+
try:
|
76
|
+
asyncio.get_event_loop().run_until_complete(self._run_async())
|
77
|
+
except asyncio.CancelledError:
|
78
|
+
pass # Suppress this expected exception and return without error (normally).
|
73
79
|
|
74
|
-
|
80
|
+
async def _run_async(self):
|
81
|
+
while not self._is_shutdown:
|
75
82
|
try:
|
76
83
|
async for task in self._task_fetcher.run():
|
77
84
|
asyncio.create_task(self._run_task(task))
|
@@ -95,7 +102,7 @@ class Executor:
|
|
95
102
|
await self._downloader.download_init_value(task)
|
96
103
|
)
|
97
104
|
logger.info("task_execution_started")
|
98
|
-
output: TaskOutput = await self.
|
105
|
+
output: TaskOutput = await self._task_runner.run(
|
99
106
|
TaskInput(
|
100
107
|
task=task,
|
101
108
|
graph=graph,
|
@@ -130,8 +137,14 @@ class Executor:
|
|
130
137
|
|
131
138
|
async def _shutdown(self, loop):
|
132
139
|
self._logger.info("shutting_down")
|
133
|
-
|
134
|
-
|
140
|
+
# There will be lots of task cancellation exceptions and "X is shutting down"
|
141
|
+
# exceptions logged during Executor shutdown. Suppress their logs as they are
|
142
|
+
# expected and are confusing for users.
|
143
|
+
suppress_logging()
|
144
|
+
|
145
|
+
self._is_shutdown = True
|
146
|
+
await self._task_runner.shutdown()
|
147
|
+
# We mainly need to cancel the task that runs _run_async() loop.
|
135
148
|
for task in asyncio.all_tasks(loop):
|
136
149
|
task.cancel()
|
137
150
|
|
@@ -3,7 +3,6 @@ from typing import Any, Optional
|
|
3
3
|
|
4
4
|
import grpc
|
5
5
|
|
6
|
-
from indexify.common_util import get_httpx_client
|
7
6
|
from indexify.function_executor.proto.function_executor_pb2 import (
|
8
7
|
InitializeRequest,
|
9
8
|
InitializeResponse,
|
@@ -11,6 +10,7 @@ from indexify.function_executor.proto.function_executor_pb2 import (
|
|
11
10
|
from indexify.function_executor.proto.function_executor_pb2_grpc import (
|
12
11
|
FunctionExecutorStub,
|
13
12
|
)
|
13
|
+
from indexify.utils.http_client import get_httpx_client
|
14
14
|
|
15
15
|
from .invocation_state_client import InvocationStateClient
|
16
16
|
from .server.function_executor_server import (
|
@@ -23,6 +23,10 @@ from .server.function_executor_server_factory import (
|
|
23
23
|
)
|
24
24
|
|
25
25
|
|
26
|
+
class CustomerError(RuntimeError):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
26
30
|
class FunctionExecutor:
|
27
31
|
"""Executor side class supporting a running FunctionExecutorServer.
|
28
32
|
|
@@ -50,7 +54,10 @@ class FunctionExecutor:
|
|
50
54
|
base_url: str,
|
51
55
|
config_path: Optional[str],
|
52
56
|
):
|
53
|
-
"""Creates and initializes a FunctionExecutorServer and all resources associated with it.
|
57
|
+
"""Creates and initializes a FunctionExecutorServer and all resources associated with it.
|
58
|
+
|
59
|
+
Raises CustomerError if the server failed to initialize due to an error in customer owned code or data.
|
60
|
+
Raises an Exception if an internal error occured."""
|
54
61
|
try:
|
55
62
|
self._server = await self._server_factory.create(
|
56
63
|
config=config, logger=self._logger
|
@@ -129,5 +136,9 @@ async def _initialize_server(
|
|
129
136
|
stub: FunctionExecutorStub, initialize_request: InitializeRequest
|
130
137
|
):
|
131
138
|
initialize_response: InitializeResponse = await stub.initialize(initialize_request)
|
132
|
-
if
|
139
|
+
if initialize_response.success:
|
140
|
+
return
|
141
|
+
if initialize_response.HasField("customer_error"):
|
142
|
+
raise CustomerError(initialize_response.customer_error)
|
143
|
+
else:
|
133
144
|
raise Exception("initialize RPC failed at function executor server")
|
@@ -15,9 +15,11 @@ class FunctionExecutorState:
|
|
15
15
|
def __init__(self, function_id_with_version: str, function_id_without_version: str):
|
16
16
|
self.function_id_with_version: str = function_id_with_version
|
17
17
|
self.function_id_without_version: str = function_id_without_version
|
18
|
+
# All the fields below are protected by the lock.
|
19
|
+
self.lock: asyncio.Lock = asyncio.Lock()
|
20
|
+
self.is_shutdown: bool = False
|
18
21
|
self.function_executor: Optional[FunctionExecutor] = None
|
19
22
|
self.running_tasks: int = 0
|
20
|
-
self.lock: asyncio.Lock = asyncio.Lock()
|
21
23
|
self.running_tasks_change_notifier: asyncio.Condition = asyncio.Condition(
|
22
24
|
lock=self.lock
|
23
25
|
)
|
@@ -58,16 +60,17 @@ class FunctionExecutorState:
|
|
58
60
|
await self.function_executor.destroy()
|
59
61
|
self.function_executor = None
|
60
62
|
|
61
|
-
async def
|
62
|
-
"""
|
63
|
+
async def shutdown(self) -> None:
|
64
|
+
"""Shuts down the state.
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
66
|
+
Called only during Executor shutdown so it's okay to fail all running and pending
|
67
|
+
Function Executor tasks. The state is not valid anymore after this call.
|
68
|
+
The caller must hold the lock.
|
69
|
+
"""
|
70
|
+
self.check_locked()
|
71
|
+
# Pending tasks will not create a new Function Executor and won't run.
|
72
|
+
self.is_shutdown = True
|
73
|
+
await self.destroy_function_executor()
|
71
74
|
|
72
75
|
def check_locked(self) -> None:
|
73
76
|
"""Raises an exception if the lock is not held."""
|
@@ -4,7 +4,6 @@ from typing import Any, AsyncGenerator, Optional, Union
|
|
4
4
|
import grpc
|
5
5
|
import httpx
|
6
6
|
|
7
|
-
from indexify.executor.downloader import serialized_object_from_http_response
|
8
7
|
from indexify.function_executor.proto.function_executor_pb2 import (
|
9
8
|
GetInvocationStateResponse,
|
10
9
|
InvocationStateRequest,
|
@@ -17,6 +16,8 @@ from indexify.function_executor.proto.function_executor_pb2_grpc import (
|
|
17
16
|
)
|
18
17
|
from indexify.function_executor.proto.message_validator import MessageValidator
|
19
18
|
|
19
|
+
from ..downloader import serialized_object_from_http_response
|
20
|
+
|
20
21
|
|
21
22
|
class InvocationStateClient:
|
22
23
|
"""InvocationStateClient is a client for the invocation state server of a Function Executor.
|
@@ -1,23 +1,23 @@
|
|
1
1
|
import asyncio
|
2
|
-
|
2
|
+
import os
|
3
|
+
import signal
|
4
|
+
from typing import Any, List, Optional
|
3
5
|
|
4
6
|
from .function_executor_server_factory import (
|
5
7
|
FunctionExecutorServerConfiguration,
|
6
8
|
FunctionExecutorServerFactory,
|
7
9
|
)
|
8
|
-
from .subprocess_function_executor_server import
|
9
|
-
SubprocessFunctionExecutorServer,
|
10
|
-
)
|
10
|
+
from .subprocess_function_executor_server import SubprocessFunctionExecutorServer
|
11
11
|
|
12
12
|
|
13
13
|
class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
|
14
14
|
def __init__(
|
15
15
|
self,
|
16
16
|
development_mode: bool,
|
17
|
+
server_ports: range,
|
17
18
|
):
|
18
19
|
self._development_mode: bool = development_mode
|
19
|
-
|
20
|
-
self._free_ports = set(range(50000, 51000))
|
20
|
+
self._free_ports: List[int] = list(reversed(server_ports))
|
21
21
|
|
22
22
|
async def create(
|
23
23
|
self, config: FunctionExecutorServerConfiguration, logger: Any
|
@@ -33,8 +33,7 @@ class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
|
|
33
33
|
try:
|
34
34
|
port = self._allocate_port()
|
35
35
|
args = [
|
36
|
-
"
|
37
|
-
"--function-executor-server-address",
|
36
|
+
"--address",
|
38
37
|
_server_address(port),
|
39
38
|
]
|
40
39
|
if self._development_mode:
|
@@ -44,8 +43,10 @@ class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
|
|
44
43
|
# so we won't see it in our process outputs. This is the right behavior as customer function stdout and stderr
|
45
44
|
# contains private customer data.
|
46
45
|
proc: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
|
47
|
-
"
|
46
|
+
"function-executor",
|
48
47
|
*args,
|
48
|
+
# TODO: pass `process_group=0` instead of the depricated `preexec_fn` once we only support Python 3.11+.
|
49
|
+
preexec_fn=_new_process_group,
|
49
50
|
)
|
50
51
|
return SubprocessFunctionExecutorServer(
|
51
52
|
process=proc,
|
@@ -77,6 +78,10 @@ class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
|
|
77
78
|
# The process already exited and was waited() sucessfully.
|
78
79
|
return
|
79
80
|
|
81
|
+
if os.name == "posix":
|
82
|
+
# On POSIX systems, we can kill the whole process group so processes forked by customer code are also killed.
|
83
|
+
# This should be done before proc.kill because PG processes get their own PG when their PG leader dies.
|
84
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
80
85
|
proc.kill()
|
81
86
|
await proc.wait()
|
82
87
|
except Exception as e:
|
@@ -95,8 +100,15 @@ class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
|
|
95
100
|
def _release_port(self, port: int) -> None:
|
96
101
|
# No asyncio.Lock is required here because this operation never awaits
|
97
102
|
# and it is always called from the same thread where the event loop is running.
|
98
|
-
|
103
|
+
#
|
104
|
+
# Prefer port reuse to repro as many possible issues deterministically as possible.
|
105
|
+
self._free_ports.append(port)
|
99
106
|
|
100
107
|
|
101
108
|
def _server_address(port: int) -> str:
|
102
109
|
return f"localhost:{port}"
|
110
|
+
|
111
|
+
|
112
|
+
def _new_process_group() -> None:
|
113
|
+
"""Creates a new process group with ID equal to the current process PID. POSIX only."""
|
114
|
+
os.setpgid(0, 0)
|