indexify 0.2.39__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.
Files changed (42) hide show
  1. indexify/cli.py +92 -52
  2. indexify/executor/agent.py +99 -187
  3. indexify/executor/api_objects.py +2 -8
  4. indexify/executor/downloader.py +129 -90
  5. indexify/executor/executor_tasks.py +15 -30
  6. indexify/executor/function_executor/function_executor.py +32 -0
  7. indexify/executor/function_executor/function_executor_factory.py +26 -0
  8. indexify/executor/function_executor/function_executor_map.py +91 -0
  9. indexify/executor/function_executor/process_function_executor.py +64 -0
  10. indexify/executor/function_executor/process_function_executor_factory.py +102 -0
  11. indexify/executor/function_worker.py +227 -184
  12. indexify/executor/runtime_probes.py +9 -8
  13. indexify/executor/task_fetcher.py +80 -0
  14. indexify/executor/task_reporter.py +18 -25
  15. indexify/executor/task_store.py +35 -16
  16. indexify/function_executor/function_executor_service.py +86 -0
  17. indexify/function_executor/handlers/run_function/function_inputs_loader.py +54 -0
  18. indexify/function_executor/handlers/run_function/handler.py +149 -0
  19. indexify/function_executor/handlers/run_function/request_validator.py +24 -0
  20. indexify/function_executor/handlers/run_function/response_helper.py +98 -0
  21. indexify/function_executor/initialize_request_validator.py +22 -0
  22. indexify/function_executor/proto/configuration.py +13 -0
  23. indexify/function_executor/proto/function_executor.proto +70 -0
  24. indexify/function_executor/proto/function_executor_pb2.py +53 -0
  25. indexify/function_executor/proto/function_executor_pb2.pyi +125 -0
  26. indexify/function_executor/proto/function_executor_pb2_grpc.py +163 -0
  27. indexify/function_executor/proto/message_validator.py +38 -0
  28. indexify/function_executor/server.py +31 -0
  29. indexify/functions_sdk/data_objects.py +0 -9
  30. indexify/functions_sdk/graph.py +17 -10
  31. indexify/functions_sdk/graph_definition.py +3 -2
  32. indexify/functions_sdk/image.py +35 -30
  33. indexify/functions_sdk/indexify_functions.py +5 -5
  34. indexify/http_client.py +15 -23
  35. indexify/logging.py +32 -0
  36. {indexify-0.2.39.dist-info → indexify-0.2.41.dist-info}/METADATA +3 -1
  37. indexify-0.2.41.dist-info/RECORD +53 -0
  38. indexify/executor/indexify_executor.py +0 -32
  39. indexify-0.2.39.dist-info/RECORD +0 -34
  40. {indexify-0.2.39.dist-info → indexify-0.2.41.dist-info}/LICENSE.txt +0 -0
  41. {indexify-0.2.39.dist-info → indexify-0.2.41.dist-info}/WHEEL +0 -0
  42. {indexify-0.2.39.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.executor.function_worker import FunctionWorker
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
- DEFAULT_IMAGE_3_10,
24
- DEFAULT_IMAGE_3_11,
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
- _build_image(image=DEFAULT_IMAGE_3_10)
154
- _build_image(image=DEFAULT_IMAGE_3_11)
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
- workers: Annotated[
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="Name alias for the executor if it's spun up with the base image"
194
+ None, help="Image name override for the executor"
178
195
  ),
179
- image_version: Optional[int] = typer.Option(
180
- "1", help="Requested Image Version for this executor"
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
- processors = [
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
- logging.info(
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
- image_version=image_version,
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
- image_version=image_version,
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
- logging.info("graceful shutdown")
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
- docker_file = f"""
250
- FROM {image._base_image}
251
-
252
- RUN mkdir -p ~/.indexify
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 touch ~/.indexify/image_name
300
+ docker_contents.extend(["RUN " + i for i in image._run_strs])
255
301
 
256
- RUN echo {image._image_name} > ~/.indexify/image_name
257
-
258
- WORKDIR /app
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
- run_strs = ["RUN " + i for i in image._run_strs]
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
 
@@ -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 indexify.common_util import get_httpx_client
13
- from indexify.functions_sdk.data_objects import (
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 indexify.http_client import IndexifyClient
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
- logging = structlog.get_logger(module=__name__)
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
- image_version: Optional[int] = None,
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
- self._probe = RuntimeProbes()
54
-
55
- self.num_workers = num_workers
36
+ protocol: str = "http"
56
37
  if config_path:
57
- logging.info("running the extractor with TLS enabled")
58
- self._protocol = "https"
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
- workers=num_workers,
66
- indexify_client=IndexifyClient(
67
- service_url=f"{self._protocol}://{server_addr}",
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"{self._protocol}://{self._server_addr}"
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=self._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=self._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
- logging.info("starting task completion reporter")
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
- retryStr = (
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=len(task_outcome.outputs or []),
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
- logging.error(
95
+ logger.error(
118
96
  "failed_to_report_task",
119
97
  task_id=task_outcome.task.id,
120
- exception=f"exception: {type(e).__name__}({e})",
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
- logging.error(
123
+ logger.error(
163
124
  "task_launcher_error, failed to get runnable tasks",
164
- exception=async_task.exception(),
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(task=task, downloader=self._downloader)
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
- logging.error(
145
+ logger.error(
182
146
  "task_launcher_error, failed to download graph",
183
- exception=async_task.exception(),
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
- DownloadInputTask(
194
- task=async_task.task, downloader=self._downloader
161
+ DownloadInputsTask(
162
+ function_worker_input=function_worker_input,
163
+ downloader=self._downloader,
195
164
  )
196
165
  )
197
- elif async_task.get_name() == "download_input":
166
+ elif async_task.get_name() == "download_inputs":
198
167
  if async_task.exception():
199
- logging.error(
200
- "task_launcher_error, failed to download input",
201
- exception=str(async_task.exception()),
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
- downloaded_inputs: DownloadedInputs = await async_task
211
- task: Task = async_task.task
212
- fn_queue.append(
213
- FunctionInput(
214
- task_id=task.id,
215
- namespace=task.namespace,
216
- compute_graph=task.compute_graph,
217
- function=task.compute_fn,
218
- input=downloaded_inputs.input,
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() == "run_function":
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: ExtractTask
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
- outputs=outputs.fn_outputs,
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
- logging.error(
217
+ logger.error(
255
218
  "failed to execute task",
256
- task_id=async_task.task.id,
257
- exception=str(e),
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._should_run = True
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
- logging.info("shutting_down")
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))