manta-node 0.5b0.dev9__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. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +83 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +9 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/manager.py +536 -0
  22. manta_node/infrastructure/filesystem/__init__.py +7 -0
  23. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  24. manta_node/infrastructure/grpc/__init__.py +11 -0
  25. manta_node/infrastructure/grpc/client.py +373 -0
  26. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  27. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  28. manta_node/infrastructure/metrics/__init__.py +10 -0
  29. manta_node/infrastructure/metrics/collector.py +94 -0
  30. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  31. manta_node/infrastructure/mqtt/__init__.py +7 -0
  32. manta_node/infrastructure/mqtt/command_handler.py +315 -0
  33. manta_node/node_orchestrator.py +462 -0
  34. manta_node/task_manager.py +519 -0
  35. manta_node/tasks.py +272 -0
  36. manta_node/utils.py +52 -0
  37. manta_node-0.5b0.dev9.dist-info/METADATA +799 -0
  38. manta_node-0.5b0.dev9.dist-info/RECORD +42 -0
  39. manta_node-0.5b0.dev9.dist-info/WHEEL +5 -0
  40. manta_node-0.5b0.dev9.dist-info/entry_points.txt +2 -0
  41. manta_node-0.5b0.dev9.dist-info/licenses/LICENSE +683 -0
  42. manta_node-0.5b0.dev9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,519 @@
1
+ import asyncio
2
+ from collections import deque
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+ from typing import List, Optional, Tuple
7
+
8
+ from manta_common.build.common.system import EnvId, PairId
9
+ from manta_common.build.common.tasks import MqttTask, TaskStatus, TaskUpdate
10
+ from manta_common.conversions import ID
11
+ from manta_common.errors import (
12
+ MantaError,
13
+ MantaGRPCError,
14
+ MantaMQTTError,
15
+ MantaNodeError,
16
+ )
17
+ from manta_common.traces import Tracer
18
+ from .domain.task_lifecycle import TaskLifecycleService
19
+ from .infrastructure.container.manager import ContainerManager
20
+ from .infrastructure.grpc.client import NodeClient
21
+ from .tasks import TaskProgression, TaskProgressions
22
+
23
+ __all__ = ["TaskManager"]
24
+
25
+
26
+ class TaskManager(ContainerManager):
27
+ """
28
+ The Task Manager is responsible to distribute tasks depending on device resources.
29
+ It is composed with a Queue of Tasks.
30
+
31
+ Parameters
32
+ ----------
33
+ light_service_port : int
34
+ Light service port
35
+ data_folder : str
36
+ Data folder
37
+ """
38
+
39
+ tracer: Tracer
40
+ node_client: NodeClient
41
+
42
+ def __init__(
43
+ self,
44
+ light_service_port: int,
45
+ ) -> None:
46
+ # Initialize the TaskManager
47
+ self.ls_port = light_service_port
48
+ self.temp_folder = TemporaryDirectory()
49
+
50
+ # Temporary directory for the module python files
51
+ self.module_folder = Path(self.temp_folder.name)
52
+ self.tasks = TaskProgressions()
53
+
54
+ # Initialize the task queue
55
+ self.task_queue = deque()
56
+ self.future_task = None
57
+
58
+ ContainerManager.__init__(self)
59
+
60
+ async def check_tasks(self) -> List[TaskProgression]:
61
+ """
62
+ Check which tasks are active and update new status to the Manager
63
+
64
+ Returns
65
+ -------
66
+ List[TaskProgression]
67
+ List of updated tasks
68
+ """
69
+ if len(self.task_queue) > 0 or len(self.tasks._env_ids) > 0:
70
+ self.tracer.debug(
71
+ f"Checking tasks (task_queue={len(self.task_queue)}, tasks={len(self.tasks._env_ids)})"
72
+ )
73
+ await self.start_next_task()
74
+ updated = await self.update_active_tasks()
75
+ updated.extend(await self.update_finished_tasks())
76
+ return updated
77
+
78
+ # TODO : make subfunctions with decorator
79
+ async def start_next_task(self):
80
+ """
81
+ Start next start
82
+ """
83
+ if self.future_task is None and len(self.task_queue) > 0:
84
+ task = self.task_queue.pop()
85
+ self.future_task = asyncio.create_task(self.exec_task(task))
86
+ await self.check_future_task()
87
+
88
+ async def check_future_task(self):
89
+ """
90
+ Check future task
91
+ """
92
+ if self.future_task is None or not self.future_task.done():
93
+ return
94
+
95
+ try:
96
+ task_id = await self.future_task
97
+ self.tracer.info(f"Task {task_id} deployed from queue")
98
+ except MantaGRPCError as e:
99
+ self.tracer.exception(f"Error when executing task: {e}")
100
+ except MantaError as manta_error:
101
+ self.tracer.exception("Error when executing task.")
102
+ await self.node_client.send_error(manta_error)
103
+ except Exception as exc:
104
+ message = f"Error when executing task: {repr(exc)}"
105
+ self.tracer.exception(message)
106
+ await self.node_client.send_error(
107
+ MantaNodeError(message, metadata=self.tracer.metadata)
108
+ )
109
+
110
+ # Reinitialize the future task
111
+ self.future_task = None
112
+
113
+ # TODO : make subfunctions with decorator
114
+ async def update_active_tasks(self) -> List[TaskProgression]:
115
+ """
116
+ Update active tasks
117
+
118
+ Returns
119
+ -------
120
+ List[TaskProgression]
121
+ List of updated tasks
122
+ """
123
+ # Update the tasks status
124
+ updated = []
125
+ for task_progression in self.tasks.active_tasks():
126
+ try:
127
+ has_changed = await self.from_pending_to_running(task_progression)
128
+ if not task_progression.is_finished_task() and has_changed:
129
+ updated.append(task_progression)
130
+ except Exception as exc:
131
+ message = f"Error updating active tasks: {repr(exc)}"
132
+ self.tracer.exception("Error updating active tasks.")
133
+ await self.node_client.send_error(
134
+ MantaNodeError(message, metadata=self.tracer.metadata)
135
+ )
136
+ return updated
137
+
138
+ # TODO : make subfunctions with decorator
139
+ async def update_finished_tasks(self) -> List[TaskProgression]:
140
+ """
141
+ Update finished tasks
142
+
143
+ Returns
144
+ -------
145
+ List[TaskProgression]
146
+ List of updated tasks
147
+ """
148
+ # Send logs of the finished tasks
149
+ updated = []
150
+ for task_progression in self.tasks.finished_tasks():
151
+ task_progression.task_status = TaskStatus.STOPPED
152
+ self.tracer.info(
153
+ f"Task {task_progression.task_id} finished: {task_progression.task_status}"
154
+ )
155
+ updated.append(task_progression)
156
+ return updated
157
+
158
+ async def from_pending_to_running(self, task_progression: TaskProgression) -> bool:
159
+ """
160
+ Change a task progression from pending status (supposed) to running status
161
+
162
+ Parameters
163
+ ----------
164
+ task_progression : TaskProgression
165
+ Task Progression
166
+
167
+ Returns
168
+ -------
169
+ bool
170
+ If task status has changed
171
+ """
172
+ task_id = task_progression.task_id
173
+ swarm_id = task_progression.swarm_id
174
+ old_task_status = task_progression.task_status
175
+ new_task_status = await self.get_task_status(swarm_id, task_id)
176
+ task_progression.task_status = new_task_status
177
+ return old_task_status != new_task_status
178
+
179
+ async def from_completed_to_pending(
180
+ self, task_progression: TaskProgression, task: MqttTask
181
+ ):
182
+ """
183
+ Change a task progression from completed status to pending status
184
+
185
+ Parameters
186
+ ----------
187
+ task_progression : TaskProgression
188
+ Task Progression
189
+ task : MqttTask
190
+ Task from MQTT
191
+ """
192
+ # Update the task information
193
+ task_progression.task_update.iteration = task.iteration
194
+ task_progression.task_update.circular = task.circular
195
+ task_progression.task_update.task_status = TaskStatus.PENDING
196
+
197
+ # Restart the container
198
+ start_time = await self.restart_container(
199
+ task_progression.swarm_id, task_progression.task_id
200
+ )
201
+ task_progression.start_time = start_time
202
+
203
+ async def create_task(self, task: MqttTask):
204
+ """
205
+ Create a task
206
+
207
+ Parameters
208
+ ----------
209
+ task : MqttTask
210
+ Task information from MQTT
211
+ """
212
+ task_id = ID(task.task_id)
213
+ swarm_id = ID(task.swarm_id)
214
+ self.tracer.set_metadata(
215
+ task_id=task_id,
216
+ swarm_id=swarm_id,
217
+ iteration=task.iteration,
218
+ circular=task.circular,
219
+ )
220
+ self.tracer.debug(f"Creating task {task_id} for {swarm_id}")
221
+
222
+ # Create a temporary file and write the module code
223
+ # (even if the file does not exist, it will be created)
224
+ module_file = self.module_folder / f"{swarm_id.oid}_{task_id.oid}.pyz"
225
+ module_file.unlink(missing_ok=True)
226
+ module_file.touch(exist_ok=True)
227
+ module_file.write_bytes(task.payload)
228
+
229
+ container_file = "/usr/src/module.pyz"
230
+
231
+ network = None
232
+ if task.network != str(None):
233
+ if task.auth_key != str(None) or task.login_server != str(None):
234
+ self.tracer.debug(
235
+ f"Task {task_id} has auth_key {task.auth_key} and login_server {task.login_server}"
236
+ )
237
+ network = await self.ensure_tailscale_service(
238
+ swarm_id,
239
+ task.auth_key,
240
+ task.login_server,
241
+ task.alias,
242
+ self.module_folder,
243
+ )
244
+ else:
245
+ self.tracer.error(
246
+ f"Task {task_id} has network {task.network} but no auth_key or login_server"
247
+ )
248
+ raise MantaNodeError(
249
+ f"Task {task_id} has network {task.network} but no auth_key or login_server",
250
+ metadata=self.tracer.metadata,
251
+ )
252
+
253
+ # Deploy the task
254
+ start_time = await self.run_container(
255
+ swarm_id=swarm_id,
256
+ task_id=task_id,
257
+ image=task.image,
258
+ command=f"python {container_file}",
259
+ volumes={module_file.absolute(): {"bind": container_file, "mode": "ro"}},
260
+ environment={
261
+ "TASK_ID": task_id.xid,
262
+ "SWARM_ID": swarm_id.xid,
263
+ "RPC_PORT": self.ls_port,
264
+ "RPC_HOST": "host.docker.internal",
265
+ },
266
+ gpu=task.gpu,
267
+ network=network,
268
+ alias=task.alias,
269
+ )
270
+
271
+ # Update the task information
272
+ self.tasks.append(
273
+ TaskProgression(
274
+ task_update=TaskUpdate(
275
+ swarm_id=swarm_id.oid,
276
+ node_id=self.tracer.metadata.node_id,
277
+ task_id=task_id.oid,
278
+ iteration=task.iteration,
279
+ circular=task.circular,
280
+ task_status=TaskStatus.PENDING,
281
+ ),
282
+ module_file=module_file,
283
+ start_time=start_time,
284
+ )
285
+ )
286
+
287
+ async def exec_task(self, task: MqttTask) -> ID:
288
+ """
289
+ Deploy a task
290
+
291
+ Parameters
292
+ ----------
293
+ task : MqttTask
294
+ Task
295
+
296
+ Returns
297
+ -------
298
+ ID
299
+ Task ID
300
+ """
301
+ task_id = ID(task.task_id)
302
+ swarm_id = ID(task.swarm_id)
303
+ self.tracer.info(f"Deploying task {task_id}")
304
+
305
+ if (swarm_id.tid, task_id.tid) in self.tasks:
306
+ await self.restart_task(self.tasks[(swarm_id.tid, task_id.tid)], task)
307
+ else:
308
+ await self.create_task(task)
309
+ return task_id
310
+
311
+ async def restart_task(self, task_progression: TaskProgression, task: MqttTask):
312
+ """
313
+ Restart a task
314
+
315
+ Parameters
316
+ ----------
317
+ task_progression : TaskProgression
318
+ Task Progression
319
+ task : MqttTask
320
+ Task from MQTT
321
+ """
322
+ swarm_id = task_progression.swarm_id
323
+ task_id = task_progression.task_id
324
+ self.tracer.set_metadata(
325
+ task_id=task_id,
326
+ swarm_id=swarm_id,
327
+ iteration=task.iteration,
328
+ circular=task.circular,
329
+ )
330
+ # Use domain service to check if task should restart
331
+ if not TaskLifecycleService.should_restart_task(
332
+ task_progression.task_update.iteration,
333
+ task.iteration,
334
+ task_progression.task_update.circular,
335
+ task.circular,
336
+ ):
337
+ # Same iteration/circular - this is a duplicate message, ignore it
338
+ self.tracer.warning(
339
+ f"Ignoring duplicate deploy message for task {task_id} "
340
+ f"(iteration={task.iteration}, circular={task.circular}). "
341
+ f"Task already exists with same parameters."
342
+ )
343
+ return
344
+
345
+ self.tracer.debug(f"Task {task_id} is already created, restarting it")
346
+ await self.from_completed_to_pending(task_progression, task)
347
+ self.tracer.info(f"Task {task_id} restarted successfully")
348
+
349
+ async def get_task_logs(
350
+ self, task_progression: TaskProgression
351
+ ) -> Tuple[datetime, bytes]:
352
+ """
353
+ Send the logs to the Manager
354
+
355
+ Parameters
356
+ ----------
357
+ task_id : str
358
+ Task ID
359
+
360
+ Raises
361
+ ------
362
+ TaskManagerException
363
+ If an error occurs during the add_task_logs
364
+ """
365
+ task_id = task_progression.task_id
366
+ swarm_id = task_progression.swarm_id
367
+ self.tracer.set_metadata(
368
+ task_id=task_id,
369
+ swarm_id=swarm_id,
370
+ iteration=task_progression.task_update.iteration,
371
+ circular=task_progression.task_update.circular,
372
+ )
373
+ logs_bytes = await self.get_container_logs(
374
+ swarm_id,
375
+ task_id,
376
+ since=task_progression.start_time,
377
+ )
378
+
379
+ return self.tracer.store_raw_logs(logs_bytes)
380
+
381
+ async def deploy_task(self, message: bytes):
382
+ """
383
+ Convert the message into a MqttTask and add it to the
384
+ task queue
385
+
386
+ Parameters
387
+ ----------
388
+ message : bytes
389
+ Message
390
+
391
+ Raises
392
+ ------
393
+ MantaMQTTError
394
+ Could not convert the message
395
+ """
396
+ try:
397
+ task = MqttTask().parse(message)
398
+ task_id = ID(task.task_id)
399
+ swarm_id = ID(task.swarm_id)
400
+ self.tracer.set_metadata(
401
+ task_id=task_id,
402
+ swarm_id=swarm_id,
403
+ iteration=task.iteration,
404
+ circular=task.circular,
405
+ )
406
+ self.task_queue.append(task)
407
+ self.tracer.info(f"Task {task_id} added to queue")
408
+ except Exception as exc:
409
+ trace_message = f"Unable to convert message into 'MqttTask': {repr(exc)}"
410
+ self.tracer.exception(trace_message)
411
+ raise MantaMQTTError(trace_message, metadata=self.tracer.metadata)
412
+
413
+ async def stop_task(self, message: bytes) -> Optional[TaskUpdate]:
414
+ """
415
+ Stop a task
416
+
417
+ Parameters
418
+ ----------
419
+ message : bytes
420
+ Message from MQTT client
421
+
422
+ Returns
423
+ -------
424
+ Optional[TaskUpdate]
425
+ Task update
426
+ """
427
+ try:
428
+ env_id = EnvId().parse(message)
429
+ swarm_id = ID(env_id.swarm_id)
430
+ task_id = ID(env_id.task_id)
431
+ task_progression = self.tasks[(swarm_id.tid, task_id.tid)]
432
+ self.tracer.set_metadata(
433
+ task_id=task_id,
434
+ swarm_id=swarm_id,
435
+ iteration=task_progression.task_update.iteration,
436
+ circular=task_progression.task_update.circular,
437
+ )
438
+
439
+ # Stop the container
440
+ await self.stop_container(swarm_id, task_id)
441
+
442
+ # Set the new TaskStatus "Stopped"
443
+ task_progression.task_status = TaskStatus.STOPPED
444
+ self.tracer.info(f"Task {task_id} stopped successfully")
445
+ return task_progression.task_update
446
+ except MantaGRPCError as e:
447
+ self.tracer.exception(f"gRPCError when stopping task: {e}")
448
+ except MantaError as manta_error:
449
+ self.tracer.error("Unable to stop task.")
450
+ raise manta_error
451
+ except Exception as exc:
452
+ trace_message = f"Unable to stop task: {repr(exc)}"
453
+ self.tracer.exception(trace_message)
454
+ raise MantaNodeError(trace_message, metadata=self.tracer.metadata)
455
+
456
+ # TODO : add decorator for loop
457
+ async def stop_swarm(self, message: bytes):
458
+ """
459
+ Stop a swarm
460
+
461
+ Parameters
462
+ ----------
463
+ swarm_id : bytess
464
+ Swarm ID
465
+ """
466
+ swarm_id = ID(PairId().parse(message))
467
+ self.tracer.set_metadata(swarm_id=swarm_id)
468
+
469
+ def match_swarm_id(task_progression: TaskProgression):
470
+ return task_progression.swarm_id == swarm_id
471
+
472
+ # Convert to list to avoid concurrent modification during iteration
473
+ tasks_to_stop = list(self.tasks.filter(match_swarm_id))
474
+ for task_progression in tasks_to_stop:
475
+ try:
476
+ await self.stop_and_remove_task(task_progression)
477
+ except MantaGRPCError as e:
478
+ self.tracer.exception(f"gRPCError when stopping swarm: {e}")
479
+ except MantaError as manta_error:
480
+ self.tracer.exception(
481
+ f"Unable to stop task {task_progression.task_id}"
482
+ f" of swarm {swarm_id}"
483
+ )
484
+ await self.node_client.send_error(manta_error)
485
+ except Exception as exc:
486
+ trace_message = (
487
+ f"Unable to stop task {task_progression.task_id}"
488
+ f" of swarm {swarm_id}: {repr(exc)}"
489
+ )
490
+ self.tracer.exception(trace_message)
491
+ await self.node_client.send_error(
492
+ MantaNodeError(trace_message, metadata=self.tracer.metadata)
493
+ )
494
+
495
+ self.tracer.info(f"All tasks in swarm {swarm_id} stopped successfully")
496
+
497
+ async def stop_and_remove_task(self, task_progression: TaskProgression):
498
+ """
499
+ Stop task's container, remove the task's container and remove the task
500
+
501
+ Parameters
502
+ ----------
503
+ task_progression : TaskProgression
504
+ Task Progression
505
+ """
506
+ await self.stop_container(
507
+ task_progression.swarm_id,
508
+ task_progression.task_id,
509
+ )
510
+ await self.remove_container(
511
+ task_progression.swarm_id,
512
+ task_progression.task_id,
513
+ )
514
+ self.tasks.pop(
515
+ (
516
+ task_progression.swarm_id.tid,
517
+ task_progression.task_id.tid,
518
+ )
519
+ )