indexify 0.2.40__py3-none-any.whl → 0.2.41__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 +92 -52
- indexify/executor/agent.py +99 -187
- indexify/executor/api_objects.py +2 -8
- indexify/executor/downloader.py +129 -90
- indexify/executor/executor_tasks.py +15 -30
- indexify/executor/function_executor/function_executor.py +32 -0
- indexify/executor/function_executor/function_executor_factory.py +26 -0
- indexify/executor/function_executor/function_executor_map.py +91 -0
- indexify/executor/function_executor/process_function_executor.py +64 -0
- indexify/executor/function_executor/process_function_executor_factory.py +102 -0
- indexify/executor/function_worker.py +227 -184
- indexify/executor/runtime_probes.py +9 -8
- indexify/executor/task_fetcher.py +80 -0
- indexify/executor/task_reporter.py +18 -25
- indexify/executor/task_store.py +35 -16
- indexify/function_executor/function_executor_service.py +86 -0
- indexify/function_executor/handlers/run_function/function_inputs_loader.py +54 -0
- indexify/function_executor/handlers/run_function/handler.py +149 -0
- indexify/function_executor/handlers/run_function/request_validator.py +24 -0
- indexify/function_executor/handlers/run_function/response_helper.py +98 -0
- indexify/function_executor/initialize_request_validator.py +22 -0
- indexify/function_executor/proto/configuration.py +13 -0
- indexify/function_executor/proto/function_executor.proto +70 -0
- indexify/function_executor/proto/function_executor_pb2.py +53 -0
- indexify/function_executor/proto/function_executor_pb2.pyi +125 -0
- indexify/function_executor/proto/function_executor_pb2_grpc.py +163 -0
- indexify/function_executor/proto/message_validator.py +38 -0
- indexify/function_executor/server.py +31 -0
- indexify/functions_sdk/data_objects.py +0 -9
- indexify/functions_sdk/graph.py +10 -11
- indexify/functions_sdk/graph_definition.py +2 -2
- indexify/functions_sdk/image.py +35 -30
- indexify/functions_sdk/indexify_functions.py +5 -5
- indexify/http_client.py +15 -23
- indexify/logging.py +32 -0
- {indexify-0.2.40.dist-info → indexify-0.2.41.dist-info}/METADATA +3 -1
- indexify-0.2.41.dist-info/RECORD +53 -0
- indexify/executor/indexify_executor.py +0 -32
- indexify-0.2.40.dist-info/RECORD +0 -34
- {indexify-0.2.40.dist-info → indexify-0.2.41.dist-info}/LICENSE.txt +0 -0
- {indexify-0.2.40.dist-info → indexify-0.2.41.dist-info}/WHEEL +0 -0
- {indexify-0.2.40.dist-info → indexify-0.2.41.dist-info}/entry_points.txt +0 -0
indexify/cli.py
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
from .logging import configure_logging_early, configure_production_logging
|
2
|
+
|
3
|
+
configure_logging_early()
|
4
|
+
|
5
|
+
|
1
6
|
import asyncio
|
2
7
|
import os
|
3
8
|
import shutil
|
@@ -18,12 +23,18 @@ from rich.text import Text
|
|
18
23
|
from rich.theme import Theme
|
19
24
|
|
20
25
|
from indexify.executor.agent import ExtractorAgent
|
21
|
-
from indexify.
|
26
|
+
from indexify.function_executor.function_executor_service import (
|
27
|
+
FunctionExecutorService,
|
28
|
+
)
|
29
|
+
from indexify.function_executor.server import Server as FunctionExecutorServer
|
22
30
|
from indexify.functions_sdk.image import (
|
23
|
-
|
24
|
-
|
31
|
+
LOCAL_PYTHON_VERSION,
|
32
|
+
GetDefaultPythonImage,
|
25
33
|
Image,
|
26
34
|
)
|
35
|
+
from indexify.http_client import IndexifyClient
|
36
|
+
|
37
|
+
logger = structlog.get_logger(module=__name__)
|
27
38
|
|
28
39
|
custom_theme = Theme(
|
29
40
|
{
|
@@ -34,10 +45,12 @@ custom_theme = Theme(
|
|
34
45
|
}
|
35
46
|
)
|
36
47
|
|
37
|
-
logging = structlog.get_logger(module=__name__)
|
38
48
|
console = Console(theme=custom_theme)
|
39
49
|
|
40
50
|
app = typer.Typer(pretty_exceptions_enable=False, no_args_is_help=True)
|
51
|
+
config_path_option: Optional[str] = typer.Option(
|
52
|
+
None, help="Path to the TLS configuration file"
|
53
|
+
)
|
41
54
|
|
42
55
|
|
43
56
|
@app.command(
|
@@ -149,12 +162,21 @@ def build_image(
|
|
149
162
|
|
150
163
|
|
151
164
|
@app.command(help="Build default image for indexify")
|
152
|
-
def build_default_image(
|
153
|
-
|
154
|
-
|
165
|
+
def build_default_image(
|
166
|
+
python_version: Optional[str] = typer.Option(
|
167
|
+
f"{sys.version_info.major}.{sys.version_info.minor}",
|
168
|
+
help="Python version to use in the base image",
|
169
|
+
)
|
170
|
+
):
|
171
|
+
image = GetDefaultPythonImage(python_version)
|
172
|
+
|
173
|
+
_build_image(image=image)
|
155
174
|
|
156
175
|
console.print(
|
157
|
-
Text(f"Built default indexify image", style="cyan"),
|
176
|
+
Text(f"Built default indexify image with hash {image.hash()}\n", style="cyan"),
|
177
|
+
Text(
|
178
|
+
f"Don't forget to update your executors to run this image!", style="yellow"
|
179
|
+
),
|
158
180
|
)
|
159
181
|
|
160
182
|
|
@@ -164,42 +186,32 @@ def executor(
|
|
164
186
|
dev: Annotated[
|
165
187
|
bool, typer.Option("--dev", "-d", help="Run the executor in development mode")
|
166
188
|
] = False,
|
167
|
-
|
168
|
-
int, typer.Option(help="number of worker processes for extraction")
|
169
|
-
] = 1,
|
170
|
-
config_path: Optional[str] = typer.Option(
|
171
|
-
None, help="Path to the TLS configuration file"
|
172
|
-
),
|
189
|
+
config_path: Optional[str] = config_path_option,
|
173
190
|
executor_cache: Optional[str] = typer.Option(
|
174
191
|
"~/.indexify/executor_cache", help="Path to the executor cache directory"
|
175
192
|
),
|
176
193
|
name_alias: Optional[str] = typer.Option(
|
177
|
-
None, help="
|
194
|
+
None, help="Image name override for the executor"
|
178
195
|
),
|
179
|
-
|
180
|
-
|
196
|
+
image_hash: Optional[str] = typer.Option(
|
197
|
+
None, help="Image hash override for the executor"
|
181
198
|
),
|
182
199
|
):
|
183
|
-
# configure structured logging
|
184
200
|
if not dev:
|
185
|
-
|
186
|
-
structlog.processors.dict_tracebacks,
|
187
|
-
structlog.processors.JSONRenderer(),
|
188
|
-
]
|
189
|
-
structlog.configure(processors=processors)
|
201
|
+
configure_production_logging()
|
190
202
|
|
191
203
|
id = nanoid.generate()
|
192
204
|
executor_version = version("indexify")
|
193
|
-
|
205
|
+
logger.info(
|
194
206
|
"executor started",
|
195
|
-
workers=workers,
|
196
207
|
server_addr=server_addr,
|
197
208
|
config_path=config_path,
|
198
209
|
executor_id=id,
|
199
210
|
executor_version=executor_version,
|
200
211
|
executor_cache=executor_cache,
|
201
212
|
name_alias=name_alias,
|
202
|
-
|
213
|
+
image_hash=image_hash,
|
214
|
+
dev_mode=dev,
|
203
215
|
)
|
204
216
|
|
205
217
|
from pathlib import Path
|
@@ -211,18 +223,47 @@ def executor(
|
|
211
223
|
|
212
224
|
agent = ExtractorAgent(
|
213
225
|
id,
|
214
|
-
num_workers=workers,
|
215
226
|
server_addr=server_addr,
|
216
227
|
config_path=config_path,
|
217
228
|
code_path=executor_cache,
|
218
229
|
name_alias=name_alias,
|
219
|
-
|
230
|
+
image_hash=image_hash,
|
231
|
+
development_mode=dev,
|
220
232
|
)
|
221
233
|
|
222
234
|
try:
|
223
235
|
asyncio.get_event_loop().run_until_complete(agent.run())
|
224
236
|
except asyncio.CancelledError:
|
225
|
-
|
237
|
+
logger.info("graceful shutdown")
|
238
|
+
|
239
|
+
|
240
|
+
@app.command(help="Runs a Function Executor server")
|
241
|
+
def function_executor(
|
242
|
+
function_executor_server_address: str = typer.Option(
|
243
|
+
help="Function Executor server address"
|
244
|
+
),
|
245
|
+
indexify_server_address: str = typer.Option(help="Indexify server address"),
|
246
|
+
dev: Annotated[
|
247
|
+
bool, typer.Option("--dev", "-d", help="Run the executor in development mode")
|
248
|
+
] = False,
|
249
|
+
config_path: Optional[str] = config_path_option,
|
250
|
+
):
|
251
|
+
if not dev:
|
252
|
+
configure_production_logging()
|
253
|
+
|
254
|
+
logger.info(
|
255
|
+
"starting function executor server",
|
256
|
+
function_executor_server_address=function_executor_server_address,
|
257
|
+
indexify_server_address=indexify_server_address,
|
258
|
+
config_path=config_path,
|
259
|
+
)
|
260
|
+
|
261
|
+
FunctionExecutorServer(
|
262
|
+
server_address=function_executor_server_address,
|
263
|
+
service=FunctionExecutorService(
|
264
|
+
indexify_server_address=indexify_server_address, config_path=config_path
|
265
|
+
),
|
266
|
+
).run()
|
226
267
|
|
227
268
|
|
228
269
|
def _create_image(image: Image, python_sdk_path):
|
@@ -234,6 +275,7 @@ def _create_image(image: Image, python_sdk_path):
|
|
234
275
|
|
235
276
|
|
236
277
|
def _build_image(image: Image, python_sdk_path: Optional[str] = None):
|
278
|
+
|
237
279
|
try:
|
238
280
|
import docker
|
239
281
|
|
@@ -246,24 +288,31 @@ def _build_image(image: Image, python_sdk_path: Optional[str] = None):
|
|
246
288
|
)
|
247
289
|
exit(-1)
|
248
290
|
|
249
|
-
|
250
|
-
FROM {image._base_image}
|
251
|
-
|
252
|
-
RUN
|
291
|
+
docker_contents = [
|
292
|
+
f"FROM {image._base_image}",
|
293
|
+
"RUN mkdir -p ~/.indexify",
|
294
|
+
"RUN touch ~/.indexify/image_name",
|
295
|
+
f"RUN echo {image._image_name} > ~/.indexify/image_name",
|
296
|
+
f"RUN echo {image.hash()} > ~/.indexify/image_hash",
|
297
|
+
"WORKDIR /app",
|
298
|
+
]
|
253
299
|
|
254
|
-
RUN
|
300
|
+
docker_contents.extend(["RUN " + i for i in image._run_strs])
|
255
301
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
302
|
+
if python_sdk_path is not None:
|
303
|
+
logging.info(
|
304
|
+
f"Building image {image._image_name} with local version of the SDK"
|
305
|
+
)
|
306
|
+
if not os.path.exists(python_sdk_path):
|
307
|
+
print(f"error: {python_sdk_path} does not exist")
|
308
|
+
os.exit(1)
|
309
|
+
docker_contents.append(f"COPY {python_sdk_path} /app/python-sdk")
|
310
|
+
docker_contents.append("RUN (cd /app/python-sdk && pip install .)")
|
311
|
+
else:
|
312
|
+
docker_contents.append(f"RUN pip install indexify=={image._sdk_version}")
|
261
313
|
|
262
|
-
|
314
|
+
docker_file = "\n".join(docker_contents)
|
263
315
|
|
264
|
-
docker_file += "\n".join(run_strs)
|
265
|
-
print(os.getcwd())
|
266
|
-
import docker
|
267
316
|
import docker.api.build
|
268
317
|
|
269
318
|
docker.api.build.process_dockerfile = lambda dockerfile, path: (
|
@@ -271,15 +320,6 @@ WORKDIR /app
|
|
271
320
|
dockerfile,
|
272
321
|
)
|
273
322
|
|
274
|
-
if python_sdk_path is not None:
|
275
|
-
if not os.path.exists(python_sdk_path):
|
276
|
-
print(f"error: {python_sdk_path} does not exist")
|
277
|
-
os.exit(1)
|
278
|
-
docker_file += f"\nCOPY {python_sdk_path} /app/python-sdk"
|
279
|
-
docker_file += f"\nRUN (cd /app/python-sdk && pip install .)"
|
280
|
-
else:
|
281
|
-
docker_file += f"\nRUN pip install indexify"
|
282
|
-
|
283
323
|
console.print("Creating image using Dockerfile contents:", style="cyan bold")
|
284
324
|
print(f"{docker_file}")
|
285
325
|
|
indexify/executor/agent.py
CHANGED
@@ -1,109 +1,87 @@
|
|
1
1
|
import asyncio
|
2
|
-
import json
|
3
|
-
from concurrent.futures.process import BrokenProcessPool
|
4
|
-
from importlib.metadata import version
|
5
2
|
from pathlib import Path
|
6
3
|
from typing import Dict, List, Optional
|
7
4
|
|
8
5
|
import structlog
|
9
|
-
from httpx_sse import aconnect_sse
|
10
|
-
from pydantic import BaseModel
|
11
6
|
|
12
|
-
from
|
13
|
-
from
|
7
|
+
from .downloader import Downloader
|
8
|
+
from .executor_tasks import DownloadGraphTask, DownloadInputsTask, RunTask
|
9
|
+
from .function_executor.process_function_executor_factory import (
|
10
|
+
ProcessFunctionExecutorFactory,
|
11
|
+
)
|
12
|
+
from .function_worker import (
|
13
|
+
FunctionWorker,
|
14
|
+
FunctionWorkerInput,
|
14
15
|
FunctionWorkerOutput,
|
15
|
-
IndexifyData,
|
16
16
|
)
|
17
|
-
from
|
18
|
-
|
19
|
-
from .api_objects import ExecutorMetadata, Task
|
20
|
-
from .downloader import DownloadedInputs, Downloader
|
21
|
-
from .executor_tasks import DownloadGraphTask, DownloadInputTask, ExtractTask
|
22
|
-
from .function_worker import FunctionWorker
|
23
|
-
from .runtime_probes import ProbeInfo, RuntimeProbes
|
17
|
+
from .task_fetcher import TaskFetcher
|
24
18
|
from .task_reporter import TaskReporter
|
25
19
|
from .task_store import CompletedTask, TaskStore
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
class FunctionInput(BaseModel):
|
31
|
-
task_id: str
|
32
|
-
namespace: str
|
33
|
-
compute_graph: str
|
34
|
-
function: str
|
35
|
-
input: IndexifyData
|
36
|
-
init_value: Optional[IndexifyData] = None
|
21
|
+
logger = structlog.get_logger(module=__name__)
|
37
22
|
|
38
23
|
|
39
24
|
class ExtractorAgent:
|
40
25
|
def __init__(
|
41
26
|
self,
|
42
27
|
executor_id: str,
|
43
|
-
num_workers,
|
44
28
|
code_path: Path,
|
45
29
|
server_addr: str = "localhost:8900",
|
30
|
+
development_mode: bool = False,
|
46
31
|
config_path: Optional[str] = None,
|
47
32
|
name_alias: Optional[str] = None,
|
48
|
-
|
33
|
+
image_hash: Optional[str] = None,
|
49
34
|
):
|
50
|
-
self.name_alias = name_alias
|
51
|
-
self.image_version = image_version
|
52
35
|
self._config_path = config_path
|
53
|
-
|
54
|
-
|
55
|
-
self.num_workers = num_workers
|
36
|
+
protocol: str = "http"
|
56
37
|
if config_path:
|
57
|
-
|
58
|
-
|
59
|
-
else:
|
60
|
-
self._protocol = "http"
|
38
|
+
logger.info("running the extractor with TLS enabled")
|
39
|
+
protocol = "https"
|
61
40
|
|
62
41
|
self._task_store: TaskStore = TaskStore()
|
63
|
-
self._executor_id = executor_id
|
64
42
|
self._function_worker = FunctionWorker(
|
65
|
-
|
66
|
-
|
67
|
-
|
43
|
+
function_executor_factory=ProcessFunctionExecutorFactory(
|
44
|
+
indexify_server_address=server_addr,
|
45
|
+
development_mode=development_mode,
|
68
46
|
config_path=config_path,
|
69
|
-
)
|
47
|
+
)
|
70
48
|
)
|
71
49
|
self._has_registered = False
|
72
50
|
self._server_addr = server_addr
|
73
|
-
self._base_url = f"{
|
51
|
+
self._base_url = f"{protocol}://{self._server_addr}"
|
74
52
|
self._code_path = code_path
|
75
53
|
self._downloader = Downloader(
|
76
|
-
code_path=code_path, base_url=self._base_url, config_path=
|
54
|
+
code_path=code_path, base_url=self._base_url, config_path=config_path
|
55
|
+
)
|
56
|
+
self._task_fetcher = TaskFetcher(
|
57
|
+
protocol=protocol,
|
58
|
+
indexify_server_addr=self._server_addr,
|
59
|
+
executor_id=executor_id,
|
60
|
+
name_alias=name_alias,
|
61
|
+
image_hash=image_hash,
|
62
|
+
config_path=config_path,
|
77
63
|
)
|
78
|
-
self._max_queued_tasks = 10
|
79
64
|
self._task_reporter = TaskReporter(
|
80
65
|
base_url=self._base_url,
|
81
|
-
executor_id=
|
66
|
+
executor_id=executor_id,
|
82
67
|
config_path=self._config_path,
|
83
68
|
)
|
84
69
|
|
85
70
|
async def task_completion_reporter(self):
|
86
|
-
|
71
|
+
logger.info("starting task completion reporter")
|
87
72
|
# We should copy only the keys and not the values
|
88
73
|
while True:
|
89
74
|
outcomes = await self._task_store.task_outcomes()
|
90
75
|
for task_outcome in outcomes:
|
91
|
-
|
92
|
-
f"\nRetries: {task_outcome.reporting_retries}"
|
93
|
-
if task_outcome.reporting_retries > 0
|
94
|
-
else ""
|
95
|
-
)
|
96
|
-
outcome = task_outcome.task_outcome
|
97
|
-
style_outcome = (
|
98
|
-
f"[bold red] {outcome} [/]"
|
99
|
-
if "fail" in outcome
|
100
|
-
else f"[bold green] {outcome} [/]"
|
101
|
-
)
|
102
|
-
logging.info(
|
76
|
+
logger.info(
|
103
77
|
"reporting_task_outcome",
|
104
78
|
task_id=task_outcome.task.id,
|
105
79
|
fn_name=task_outcome.task.compute_fn,
|
106
|
-
num_outputs=
|
80
|
+
num_outputs=(
|
81
|
+
len(task_outcome.function_output.outputs)
|
82
|
+
if task_outcome.function_output is not None
|
83
|
+
else 0
|
84
|
+
),
|
107
85
|
router_output=task_outcome.router_output,
|
108
86
|
outcome=task_outcome.task_outcome,
|
109
87
|
retries=task_outcome.reporting_retries,
|
@@ -114,10 +92,10 @@ class ExtractorAgent:
|
|
114
92
|
self._task_reporter.report_task_outcome(completed_task=task_outcome)
|
115
93
|
except Exception as e:
|
116
94
|
# The connection was dropped in the middle of the reporting, process, retry
|
117
|
-
|
95
|
+
logger.error(
|
118
96
|
"failed_to_report_task",
|
119
97
|
task_id=task_outcome.task.id,
|
120
|
-
|
98
|
+
exc_info=e,
|
121
99
|
retries=task_outcome.reporting_retries,
|
122
100
|
)
|
123
101
|
task_outcome.reporting_retries += 1
|
@@ -127,30 +105,13 @@ class ExtractorAgent:
|
|
127
105
|
self._task_store.mark_reported(task_id=task_outcome.task.id)
|
128
106
|
|
129
107
|
async def task_launcher(self):
|
130
|
-
async_tasks: List[asyncio.Task] = [
|
131
|
-
fn_queue: List[FunctionInput] = []
|
132
|
-
|
133
|
-
async_tasks.append(
|
108
|
+
async_tasks: List[asyncio.Task] = [
|
134
109
|
asyncio.create_task(
|
135
110
|
self._task_store.get_runnable_tasks(), name="get_runnable_tasks"
|
136
111
|
)
|
137
|
-
|
112
|
+
]
|
138
113
|
|
139
114
|
while True:
|
140
|
-
fn: FunctionInput
|
141
|
-
for fn in fn_queue:
|
142
|
-
task: Task = self._task_store.get_task(fn.task_id)
|
143
|
-
async_tasks.append(
|
144
|
-
ExtractTask(
|
145
|
-
function_worker=self._function_worker,
|
146
|
-
task=task,
|
147
|
-
input=fn.input,
|
148
|
-
code_path=f"{self._code_path}/{task.namespace}/{task.compute_graph}.{task.graph_version}",
|
149
|
-
init_value=fn.init_value,
|
150
|
-
)
|
151
|
-
)
|
152
|
-
|
153
|
-
fn_queue = []
|
154
115
|
done, pending = await asyncio.wait(
|
155
116
|
async_tasks, return_when=asyncio.FIRST_COMPLETED
|
156
117
|
)
|
@@ -159,16 +120,19 @@ class ExtractorAgent:
|
|
159
120
|
for async_task in done:
|
160
121
|
if async_task.get_name() == "get_runnable_tasks":
|
161
122
|
if async_task.exception():
|
162
|
-
|
123
|
+
logger.error(
|
163
124
|
"task_launcher_error, failed to get runnable tasks",
|
164
|
-
|
125
|
+
exc_info=async_task.exception(),
|
165
126
|
)
|
166
127
|
continue
|
167
128
|
result: Dict[str, Task] = await async_task
|
168
129
|
task: Task
|
169
130
|
for _, task in result.items():
|
170
131
|
async_tasks.append(
|
171
|
-
DownloadGraphTask(
|
132
|
+
DownloadGraphTask(
|
133
|
+
function_worker_input=FunctionWorkerInput(task=task),
|
134
|
+
downloader=self._downloader,
|
135
|
+
)
|
172
136
|
)
|
173
137
|
async_tasks.append(
|
174
138
|
asyncio.create_task(
|
@@ -178,58 +142,60 @@ class ExtractorAgent:
|
|
178
142
|
)
|
179
143
|
elif async_task.get_name() == "download_graph":
|
180
144
|
if async_task.exception():
|
181
|
-
|
145
|
+
logger.error(
|
182
146
|
"task_launcher_error, failed to download graph",
|
183
|
-
|
147
|
+
exc_info=async_task.exception(),
|
184
148
|
)
|
185
149
|
completed_task = CompletedTask(
|
186
|
-
task=async_task.task,
|
187
|
-
outputs=[],
|
150
|
+
task=async_task.function_worker_input.task,
|
188
151
|
task_outcome="failure",
|
189
152
|
)
|
190
153
|
self._task_store.complete(outcome=completed_task)
|
191
154
|
continue
|
155
|
+
async_task: DownloadGraphTask
|
156
|
+
function_worker_input: FunctionWorkerInput = (
|
157
|
+
async_task.function_worker_input
|
158
|
+
)
|
159
|
+
function_worker_input.graph = await async_task
|
192
160
|
async_tasks.append(
|
193
|
-
|
194
|
-
|
161
|
+
DownloadInputsTask(
|
162
|
+
function_worker_input=function_worker_input,
|
163
|
+
downloader=self._downloader,
|
195
164
|
)
|
196
165
|
)
|
197
|
-
elif async_task.get_name() == "
|
166
|
+
elif async_task.get_name() == "download_inputs":
|
198
167
|
if async_task.exception():
|
199
|
-
|
200
|
-
"task_launcher_error, failed to download
|
201
|
-
|
168
|
+
logger.error(
|
169
|
+
"task_launcher_error, failed to download inputs",
|
170
|
+
exc_info=async_task.exception(),
|
202
171
|
)
|
203
172
|
completed_task = CompletedTask(
|
204
|
-
task=async_task.task,
|
205
|
-
outputs=[],
|
173
|
+
task=async_task.function_worker_input.task,
|
206
174
|
task_outcome="failure",
|
207
175
|
)
|
208
176
|
self._task_store.complete(outcome=completed_task)
|
209
177
|
continue
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
init_value=downloaded_inputs.init_value,
|
178
|
+
async_task: DownloadInputsTask
|
179
|
+
function_worker_input: FunctionWorkerInput = (
|
180
|
+
async_task.function_worker_input
|
181
|
+
)
|
182
|
+
function_worker_input.function_input = await async_task
|
183
|
+
async_tasks.append(
|
184
|
+
RunTask(
|
185
|
+
function_worker=self._function_worker,
|
186
|
+
function_worker_input=function_worker_input,
|
220
187
|
)
|
221
188
|
)
|
222
|
-
elif async_task.get_name() == "
|
189
|
+
elif async_task.get_name() == "run_task":
|
223
190
|
if async_task.exception():
|
224
191
|
completed_task = CompletedTask(
|
225
|
-
task=async_task.task,
|
192
|
+
task=async_task.function_worker_input.task,
|
226
193
|
task_outcome="failure",
|
227
|
-
outputs=[],
|
228
194
|
stderr=str(async_task.exception()),
|
229
195
|
)
|
230
196
|
self._task_store.complete(outcome=completed_task)
|
231
197
|
continue
|
232
|
-
async_task:
|
198
|
+
async_task: RunTask
|
233
199
|
try:
|
234
200
|
outputs: FunctionWorkerOutput = await async_task
|
235
201
|
if not outputs.success:
|
@@ -238,113 +204,59 @@ class ExtractorAgent:
|
|
238
204
|
task_outcome = "success"
|
239
205
|
|
240
206
|
completed_task = CompletedTask(
|
241
|
-
task=async_task.task,
|
207
|
+
task=async_task.function_worker_input.task,
|
242
208
|
task_outcome=task_outcome,
|
243
|
-
|
209
|
+
function_output=outputs.function_output,
|
244
210
|
router_output=outputs.router_output,
|
245
211
|
stdout=outputs.stdout,
|
246
212
|
stderr=outputs.stderr,
|
247
213
|
reducer=outputs.reducer,
|
248
214
|
)
|
249
215
|
self._task_store.complete(outcome=completed_task)
|
250
|
-
except BrokenProcessPool:
|
251
|
-
self._task_store.retriable_failure(async_task.task.id)
|
252
|
-
continue
|
253
216
|
except Exception as e:
|
254
|
-
|
217
|
+
logger.error(
|
255
218
|
"failed to execute task",
|
256
|
-
task_id=async_task.task.id,
|
257
|
-
|
219
|
+
task_id=async_task.function_worker_input.task.id,
|
220
|
+
exc_info=e,
|
258
221
|
)
|
259
222
|
completed_task = CompletedTask(
|
260
|
-
task=async_task.task,
|
223
|
+
task=async_task.function_worker_input.task,
|
261
224
|
task_outcome="failure",
|
262
|
-
outputs=[],
|
263
225
|
)
|
264
226
|
self._task_store.complete(outcome=completed_task)
|
265
227
|
continue
|
266
228
|
|
229
|
+
async def _main_loop(self):
|
230
|
+
"""Fetches incoming tasks from the server and starts their processing."""
|
231
|
+
self._should_run = True
|
232
|
+
while self._should_run:
|
233
|
+
try:
|
234
|
+
async for task in self._task_fetcher.run():
|
235
|
+
self._task_store.add_tasks([task])
|
236
|
+
except Exception as e:
|
237
|
+
logger.error("failed fetching tasks, retrying in 5 seconds", exc_info=e)
|
238
|
+
await asyncio.sleep(5)
|
239
|
+
continue
|
240
|
+
|
267
241
|
async def run(self):
|
268
242
|
import signal
|
269
243
|
|
270
244
|
asyncio.get_event_loop().add_signal_handler(
|
271
245
|
signal.SIGINT, self.shutdown, asyncio.get_event_loop()
|
272
246
|
)
|
247
|
+
asyncio.get_event_loop().add_signal_handler(
|
248
|
+
signal.SIGTERM, self.shutdown, asyncio.get_event_loop()
|
249
|
+
)
|
273
250
|
asyncio.create_task(self.task_launcher())
|
274
251
|
asyncio.create_task(self.task_completion_reporter())
|
275
|
-
self.
|
276
|
-
while self._should_run:
|
277
|
-
url = f"{self._protocol}://{self._server_addr}/internal/executors/{self._executor_id}/tasks"
|
278
|
-
runtime_probe: ProbeInfo = self._probe.probe()
|
279
|
-
|
280
|
-
executor_version = version("indexify")
|
281
|
-
|
282
|
-
image_name = (
|
283
|
-
self.name_alias
|
284
|
-
if self.name_alias is not None
|
285
|
-
else runtime_probe.image_name
|
286
|
-
)
|
287
|
-
|
288
|
-
image_version: int = (
|
289
|
-
self.image_version
|
290
|
-
if self.image_version is not None
|
291
|
-
else runtime_probe.image_version
|
292
|
-
)
|
293
|
-
|
294
|
-
data = ExecutorMetadata(
|
295
|
-
id=self._executor_id,
|
296
|
-
executor_version=executor_version,
|
297
|
-
addr="",
|
298
|
-
image_name=image_name,
|
299
|
-
image_version=image_version,
|
300
|
-
labels=runtime_probe.labels,
|
301
|
-
).model_dump()
|
302
|
-
logging.info(
|
303
|
-
"registering_executor",
|
304
|
-
executor_id=self._executor_id,
|
305
|
-
url=url,
|
306
|
-
executor_version=executor_version,
|
307
|
-
)
|
308
|
-
try:
|
309
|
-
async with get_httpx_client(self._config_path, True) as client:
|
310
|
-
async with aconnect_sse(
|
311
|
-
client,
|
312
|
-
"POST",
|
313
|
-
url,
|
314
|
-
json=data,
|
315
|
-
headers={"Content-Type": "application/json"},
|
316
|
-
) as event_source:
|
317
|
-
if not event_source.response.is_success:
|
318
|
-
resp = await event_source.response.aread()
|
319
|
-
logging.error(
|
320
|
-
f"failed to register",
|
321
|
-
resp=str(resp),
|
322
|
-
status_code=event_source.response.status_code,
|
323
|
-
)
|
324
|
-
await asyncio.sleep(5)
|
325
|
-
continue
|
326
|
-
logging.info(
|
327
|
-
"executor_registered", executor_id=self._executor_id
|
328
|
-
)
|
329
|
-
async for sse in event_source.aiter_sse():
|
330
|
-
data = json.loads(sse.data)
|
331
|
-
tasks = []
|
332
|
-
for task_dict in data:
|
333
|
-
tasks.append(
|
334
|
-
Task.model_validate(task_dict, strict=False)
|
335
|
-
)
|
336
|
-
self._task_store.add_tasks(tasks)
|
337
|
-
except Exception as e:
|
338
|
-
logging.error(f"failed to register: {e}")
|
339
|
-
await asyncio.sleep(5)
|
340
|
-
continue
|
252
|
+
await self._main_loop()
|
341
253
|
|
342
254
|
async def _shutdown(self, loop):
|
343
|
-
|
255
|
+
logger.info("shutting_down")
|
344
256
|
self._should_run = False
|
257
|
+
await self._function_worker.shutdown()
|
345
258
|
for task in asyncio.all_tasks(loop):
|
346
259
|
task.cancel()
|
347
260
|
|
348
261
|
def shutdown(self, loop):
|
349
|
-
self._function_worker.shutdown()
|
350
262
|
loop.create_task(self._shutdown(loop))
|