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,351 @@
1
+ import asyncio
2
+ import platform
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from datetime import datetime, timezone
6
+ from logging import getLogger
7
+ from typing import Optional
8
+
9
+ import psutil
10
+
11
+ from manta_common.build.common.informations import (
12
+ AdditionalMetrics,
13
+ CpuMetrics,
14
+ DiskUsage,
15
+ GpuDeviceList,
16
+ MemoryMetrics,
17
+ MetricsSnapshot,
18
+ NetworkUsage,
19
+ PlatformInfo,
20
+ )
21
+ from manta_common.traces import Tracer
22
+
23
+ PSUTIL_AVAILABLE = True
24
+
25
+
26
+ class MetricsCollectorBase(ABC):
27
+ """
28
+ Abstract base class for metrics collection.
29
+
30
+ This class defines the interface for metrics collectors that gather
31
+ system metrics like CPU, memory, and other hardware usage.
32
+
33
+ Different implementations can be created for specific hardware platforms
34
+ (Linux, Jetson, Mac, etc.) by extending this class.
35
+
36
+ Parameters
37
+ ----------
38
+
39
+ The ID of the node where metrics are being collected
40
+ collection_interval : float
41
+ Time in seconds between metrics collection (default: 30.0)
42
+ """
43
+
44
+ def __init__(self, collection_interval: float = 30.0):
45
+ self.collection_interval = collection_interval
46
+ self.tracer = Tracer(getLogger(__name__), None)
47
+ self._latest_metrics: Optional[MetricsSnapshot] = None
48
+ self._running = False
49
+ self._collection_task: Optional[asyncio.Task] = None
50
+ self._lock = asyncio.Lock()
51
+
52
+ @abstractmethod
53
+ async def collect_cpu_metrics(self) -> CpuMetrics:
54
+ """
55
+ Collect CPU usage metrics.
56
+
57
+ Returns
58
+ -------
59
+ CpuMetrics
60
+ Protobuf message with CPU metrics
61
+ """
62
+ pass
63
+
64
+ @abstractmethod
65
+ async def collect_memory_metrics(self) -> MemoryMetrics:
66
+ """
67
+ Collect memory usage metrics.
68
+
69
+ Returns
70
+ -------
71
+ MemoryMetrics
72
+ Protobuf message with memory metrics
73
+ """
74
+ pass
75
+
76
+ @abstractmethod
77
+ async def collect_gpu_metrics(self) -> GpuDeviceList:
78
+ """
79
+ Collect GPU usage metrics if available.
80
+
81
+ Returns
82
+ -------
83
+ GpuDeviceList
84
+ Protobuf message with GPU metrics, or empty if no GPU or metrics unavailable
85
+ """
86
+ pass
87
+
88
+ @abstractmethod
89
+ async def collect_additional_metrics(self) -> AdditionalMetrics:
90
+ """
91
+ Collect disk and network metrics.
92
+
93
+ Returns
94
+ -------
95
+ AdditionalMetrics
96
+ Protobuf message with disk and network metrics
97
+ """
98
+ pass
99
+
100
+ @classmethod
101
+ def get_platform_info(cls) -> PlatformInfo:
102
+ """
103
+ Get platform information (system, version, etc.).
104
+
105
+ Returns
106
+ -------
107
+ PlatformInfo
108
+ Protobuf message with platform information
109
+ """
110
+ return PlatformInfo(
111
+ system=platform.system(),
112
+ release=platform.release(),
113
+ version=platform.version(),
114
+ machine=platform.machine(),
115
+ processor=platform.processor(),
116
+ hostname=platform.node(),
117
+ )
118
+
119
+ async def collect_all_metrics(self) -> MetricsSnapshot:
120
+ """
121
+ Collect all available metrics.
122
+
123
+ Returns
124
+ -------
125
+ MetricsSnapshot
126
+ Protobuf message with all collected metrics
127
+ """
128
+ cpu_metrics = await self.collect_cpu_metrics()
129
+ memory_metrics = await self.collect_memory_metrics()
130
+ # gpu_metrics = await self.collect_gpu_metrics()
131
+ # additional_metrics = await self.collect_additional_metrics()
132
+ # platform_info = self.get_platform_info()
133
+
134
+ # Combine all metrics into a snapshot
135
+ metrics = MetricsSnapshot(
136
+ timestamp=datetime.now(timezone.utc),
137
+ cpu=cpu_metrics,
138
+ memory=memory_metrics,
139
+ # gpu=gpu_metrics,
140
+ # additional=additional_metrics,
141
+ # platform=platform_info,
142
+ )
143
+
144
+ # Update latest metrics with thread safety
145
+ async with self._lock:
146
+ self._latest_metrics = metrics
147
+
148
+ return metrics
149
+
150
+ async def start_collection(self):
151
+ """
152
+ Start the metrics collection background task.
153
+ """
154
+ if self._running:
155
+ self.tracer.warning("Metrics collection already running")
156
+ return
157
+
158
+ self._running = True
159
+ self._collection_task = asyncio.create_task(self._collection_loop())
160
+ self.tracer.info(
161
+ f"Started metrics collection (interval: {self.collection_interval}s)"
162
+ )
163
+
164
+ async def stop_collection(self):
165
+ """
166
+ Stop the metrics collection background task.
167
+ """
168
+ if not self._running:
169
+ return
170
+
171
+ self._running = False
172
+ if self._collection_task:
173
+ self._collection_task.cancel()
174
+ try:
175
+ await self._collection_task
176
+ except asyncio.CancelledError:
177
+ pass
178
+ self._collection_task = None
179
+
180
+ self.tracer.info("Stopped metrics collection")
181
+
182
+ async def _collection_loop(self):
183
+ """
184
+ Background loop that periodically collects metrics.
185
+ """
186
+ while self._running:
187
+ try:
188
+ await self.collect_all_metrics()
189
+ await asyncio.sleep(self.collection_interval)
190
+ except asyncio.CancelledError:
191
+ break
192
+ except Exception as e:
193
+ self.tracer.error(f"Error in metrics collection loop: {e}")
194
+ await asyncio.sleep(self.collection_interval)
195
+
196
+ async def get_latest_metrics(self) -> Optional[MetricsSnapshot]:
197
+ """
198
+ Get the most recently collected metrics.
199
+
200
+ Returns
201
+ -------
202
+ Optional[MetricsSnapshot]
203
+ The latest collected metrics, or None if no metrics have been collected
204
+ """
205
+ async with self._lock:
206
+ return self._latest_metrics
207
+
208
+
209
+ class GenericMetricsCollector(MetricsCollectorBase):
210
+ """
211
+ Generic implementation of the metrics collector using psutil.
212
+
213
+ This implementation works on most platforms (Linux, macOS, Windows)
214
+ where psutil is available.
215
+
216
+ Parameters
217
+ ----------
218
+ node_id : Any
219
+ The ID of the node where metrics are being collected
220
+ collection_interval : float
221
+ Time in seconds between metrics collection (default: 30.0)
222
+ """
223
+
224
+ def __init__(self, collection_interval: float = 30.0):
225
+ super().__init__(collection_interval)
226
+
227
+ # psutil is now always available
228
+
229
+ async def collect_cpu_metrics(self) -> CpuMetrics:
230
+ """
231
+ Collect CPU usage metrics using psutil.
232
+
233
+ Returns
234
+ -------
235
+ CpuMetrics
236
+ Protobuf message with CPU metrics
237
+ """
238
+ # psutil is now always available
239
+
240
+ # Get CPU usage percentage (blocking call, so run in executor)
241
+ cpu_percent = await asyncio.to_thread(psutil.cpu_percent, interval=0.1)
242
+
243
+ # Get per-core CPU usage
244
+ per_cpu = await asyncio.to_thread(psutil.cpu_percent, interval=0.1, percpu=True)
245
+
246
+ # Get CPU times
247
+ cpu_times = await asyncio.to_thread(psutil.cpu_times_percent, interval=0.1)
248
+
249
+ return CpuMetrics(
250
+ usage_percent=cpu_percent,
251
+ per_cpu_percent=per_cpu,
252
+ user_percent=float(cpu_times.user),
253
+ system_percent=float(cpu_times.system),
254
+ idle_percent=float(cpu_times.idle),
255
+ count=psutil.cpu_count(logical=True) or 0,
256
+ physical_count=psutil.cpu_count(logical=False) or 0,
257
+ )
258
+
259
+ async def collect_memory_metrics(self) -> MemoryMetrics:
260
+ """
261
+ Collect memory usage metrics using psutil.
262
+
263
+ Returns
264
+ -------
265
+ MemoryMetrics
266
+ Protobuf message with memory metrics
267
+ """
268
+ # psutil is now always available
269
+
270
+ # Get virtual memory stats
271
+ memory = await asyncio.to_thread(psutil.virtual_memory)
272
+
273
+ # Get swap memory stats
274
+ swap = await asyncio.to_thread(psutil.swap_memory)
275
+
276
+ return MemoryMetrics(
277
+ total_bytes=memory.total,
278
+ available_bytes=memory.available,
279
+ used_bytes=memory.used,
280
+ free_bytes=memory.free,
281
+ percent_used=memory.percent,
282
+ swap_total_bytes=swap.total,
283
+ swap_used_bytes=swap.used,
284
+ swap_free_bytes=swap.free,
285
+ swap_percent_used=swap.percent,
286
+ )
287
+
288
+ async def collect_gpu_metrics(self) -> GpuDeviceList:
289
+ """
290
+ Collect GPU metrics if available.
291
+
292
+ This generic implementation doesn't collect GPU metrics.
293
+ Subclasses for specific platforms will override this method.
294
+
295
+ Returns
296
+ -------
297
+ GpuDeviceList
298
+ Empty build message (no GPU metrics in generic implementation)
299
+ """
300
+ # Generic implementation doesn't collect GPU metrics
301
+ # This will be overridden in platform-specific implementations
302
+ return GpuDeviceList(devices=[])
303
+
304
+ async def collect_additional_metrics(self) -> AdditionalMetrics:
305
+ """
306
+ Collect disk and network metrics using psutil.
307
+
308
+ Returns
309
+ -------
310
+ AdditionalMetrics
311
+ Protobuf message with disk and network metrics
312
+ """
313
+ # psutil is now always available
314
+
315
+ # Get disk usage
316
+ disk_usage = []
317
+ for partition in psutil.disk_partitions(all=False):
318
+ try:
319
+ usage = psutil.disk_usage(partition.mountpoint)
320
+ disk_usage.append(
321
+ DiskUsage(
322
+ mount_point=partition.mountpoint,
323
+ total_bytes=usage.total,
324
+ used_bytes=usage.used,
325
+ free_bytes=usage.free,
326
+ percent_used=usage.percent,
327
+ )
328
+ )
329
+ except (PermissionError, OSError):
330
+ # Some mountpoints may not be accessible
331
+ pass
332
+
333
+ # Get basic network info
334
+ net_io = psutil.net_io_counters()
335
+ network = NetworkUsage(
336
+ bytes_sent=net_io.bytes_sent,
337
+ bytes_recv=net_io.bytes_recv,
338
+ packets_sent=net_io.packets_sent,
339
+ packets_recv=net_io.packets_recv,
340
+ )
341
+
342
+ # Get system uptime
343
+ boot_time = psutil.boot_time()
344
+ uptime_seconds = time.time() - boot_time
345
+
346
+ # Combine all additional metrics
347
+ return AdditionalMetrics(
348
+ disk_usage=disk_usage,
349
+ network=network,
350
+ uptime_seconds=int(uptime_seconds),
351
+ )
@@ -0,0 +1,7 @@
1
+ """MQTT infrastructure - Message broker communication."""
2
+
3
+ from .command_handler import CommandHandler
4
+
5
+ __all__ = [
6
+ "CommandHandler",
7
+ ]
@@ -0,0 +1,315 @@
1
+ import asyncio
2
+ import io
3
+ from datetime import datetime
4
+ from logging import getLogger
5
+ from pathlib import Path
6
+ from typing import AsyncIterable, Awaitable, Callable, List, Optional, cast
7
+
8
+ import aiomqtt
9
+
10
+ from manta_common.base_mqtt import MqttBase
11
+ from manta_common.build.common.mqtt import MqMetricsSnapshot, MqTaskUpdate
12
+ from manta_common.build.common.system import Logs
13
+ from manta_common.build.common.tasks import TaskStatus, TaskUpdate
14
+ from manta_common.const import CHUNK_SIZE
15
+ from manta_common.conversions import ID
16
+ from manta_common.errors import (
17
+ MantaError,
18
+ MantaGRPCError,
19
+ MantaMQTTError,
20
+ MantaNodeError,
21
+ )
22
+ from manta_common.traces import Tracer
23
+ from ...task_manager import TaskManager
24
+ from ..filesystem.dataset_manager import DatasetManager
25
+ from ..grpc.client import NodeClient
26
+ from ..metrics.collector import Collector
27
+ from ..metrics.metrics_collector import MetricsCollectorBase
28
+
29
+ TIMEOUT = 1 # s
30
+
31
+
32
+ class CommandHandler(TaskManager, Collector, MqttBase):
33
+ """
34
+ CommandHandler class to handle commands from the Manager
35
+
36
+ Parameters
37
+ ----------
38
+ node_id: ID
39
+ Node ID
40
+ mqtt_client: MQTTClient
41
+ MQTT client
42
+ node_client: NodeClient
43
+ Node client
44
+ mqtt_broker_host : str
45
+ MQTT broker host
46
+ mqtt_broker_port : int
47
+ MQTT broker port
48
+ topics : List[str]
49
+ List of topics
50
+ light_service_port: int
51
+ Light service port
52
+ dataset_manager: DatasetManager
53
+ Dataset manager
54
+ metrics_collector: MetricsCollectorBase
55
+ Metrics collector
56
+ stop_callback: Callable[[], Awaitable[None]]
57
+ Stop callback
58
+ ssl_cert_folder : Optional[Path]
59
+ SSL Certification folder
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ node_client: NodeClient,
65
+ mqtt_broker_host: str,
66
+ mqtt_broker_port: int,
67
+ light_service_port: int,
68
+ topics: List[str],
69
+ dataset_manager: DatasetManager,
70
+ metrics_collector: MetricsCollectorBase,
71
+ stop_callback: Callable[[], Awaitable[None]],
72
+ ssl_cert_folder: Optional[Path] = None,
73
+ ping_interval: int = TIMEOUT,
74
+ max_concurrent_tasks: int = 2,
75
+ task_timeout: int = 3600,
76
+ ) -> None:
77
+ self.tracer = Tracer(getLogger(__name__))
78
+ self.node_client = node_client
79
+ self.dataset_manager = dataset_manager
80
+ self.metrics_collector = metrics_collector
81
+ self.stop_callback = stop_callback
82
+ self.ping_interval = ping_interval
83
+ self.max_concurrent_tasks = max_concurrent_tasks
84
+ self.task_timeout = task_timeout
85
+
86
+ MqttBase.__init__(
87
+ self,
88
+ mqtt_broker_host=mqtt_broker_host,
89
+ mqtt_broker_port=mqtt_broker_port,
90
+ topics=topics,
91
+ client_id=ID(node_client.metadata.get("authorization", "node_client")),
92
+ ssl_cert_folder=ssl_cert_folder,
93
+ )
94
+
95
+ self.tracer.debug("Trying to connect to docker daemon...")
96
+ TaskManager.__init__(self, light_service_port)
97
+ self.tracer.info("Connected to docker daemon !")
98
+
99
+ async def clean(self): # TODO : use __del__
100
+ """Delete the temporary folder and stop all containers"""
101
+ self.tracer.debug("Cleaning up the temporary folder")
102
+ self.temp_folder.cleanup()
103
+
104
+ self.tracer.info("Stopping all tasks")
105
+ for task_progression in self.tasks:
106
+ try:
107
+ task_id = task_progression.task_id
108
+ swarm_id = task_progression.swarm_id
109
+
110
+ task_progression.unlink()
111
+ await self.stop_container(swarm_id, task_id)
112
+ await self.remove_container(swarm_id, task_id)
113
+ except Exception:
114
+ self.tracer.exception(
115
+ f"Error when stopping task {task_progression.task_id}"
116
+ )
117
+
118
+ self.tracer.info("Closing Docker client")
119
+ try:
120
+ await self.async_client.close()
121
+ except Exception:
122
+ self.tracer.exception("Error when closing Docker client")
123
+
124
+ async def loop_tasks(self):
125
+ """
126
+ Processes messages or check tasks to ping
127
+ """
128
+ while True:
129
+ # Wait until we either (1) receive a message or (2) publish a message
130
+ try:
131
+ subscribe_task = asyncio.create_task(self.client.messages.__anext__())
132
+ publish_task = asyncio.create_task(self.queue.get())
133
+ ping_task = asyncio.create_task(self._ping())
134
+ done, _ = await asyncio.wait(
135
+ (subscribe_task, publish_task, ping_task),
136
+ return_when=asyncio.FIRST_COMPLETED,
137
+ )
138
+ # If the asyncio.wait is cancelled, we must also cancel the queue task
139
+ except asyncio.CancelledError:
140
+ subscribe_task.cancel()
141
+ publish_task.cancel()
142
+ ping_task.cancel()
143
+ raise
144
+
145
+ # When we receive a message, process it
146
+ if subscribe_task in done and publish_task in done:
147
+ await self._subscribe_from_task(subscribe_task)
148
+ await self._publish_from_task(publish_task)
149
+ ping_task.cancel()
150
+ elif subscribe_task in done:
151
+ await self._subscribe_from_task(subscribe_task)
152
+ publish_task.cancel()
153
+ ping_task.cancel()
154
+ # Publish the message to its topic with specific QoS
155
+ elif publish_task in done:
156
+ await self._publish_from_task(publish_task)
157
+ subscribe_task.cancel()
158
+ ping_task.cancel()
159
+ # If we disconnect from the broker, stop the loop with an exception
160
+ elif ping_task in done:
161
+ ping_task.cancel()
162
+ publish_task.cancel()
163
+ subscribe_task.cancel()
164
+ else:
165
+ self.tracer.warning("Disconnected during message iteration")
166
+ publish_task.cancel()
167
+ subscribe_task.cancel()
168
+ ping_task.cancel()
169
+ raise aiomqtt.MqttError("Disconnected during message iteration")
170
+
171
+ async def _ping(self):
172
+ """
173
+ Ping the Manager
174
+ """
175
+ await asyncio.sleep(self.ping_interval)
176
+ updated = await self.check_tasks()
177
+ if len(updated) > 0:
178
+ for task_progression in updated:
179
+ if task_progression.task_status == TaskStatus.STOPPED:
180
+ try:
181
+ date, processed_logs = await self.get_task_logs(
182
+ task_progression
183
+ )
184
+ self.tracer.info(
185
+ f"Sending logs to the Manager for task {task_progression.task_id}"
186
+ )
187
+ await self.send_task_logs(date, processed_logs)
188
+ except MantaGRPCError as e:
189
+ self.tracer.exception(
190
+ f"gRPCError when updating finished tasks: {e}"
191
+ )
192
+ task_progression.task_status = TaskStatus.FAILED
193
+ except MantaError as manta_error:
194
+ self.tracer.exception("Error updating finished tasks.")
195
+ manta_error.metadata = self.tracer.metadata
196
+ task_progression.task_status = TaskStatus.FAILED
197
+ await self.node_client.send_error(manta_error)
198
+ except Exception as exc:
199
+ message = f"Error updating finished tasks: {repr(exc)}"
200
+ self.tracer.exception(message)
201
+ task_progression.task_status = TaskStatus.FAILED
202
+ await self.node_client.send_error(
203
+ MantaNodeError(message, metadata=self.tracer.metadata)
204
+ )
205
+ else:
206
+ try:
207
+ await self.publish_task_update(task_progression.task_update)
208
+ except Exception as exc:
209
+ message = f"Error updating active tasks: {repr(exc)}"
210
+ self.tracer.exception("Error updating active tasks.")
211
+ await self.node_client.send_error(
212
+ MantaNodeError(message, metadata=self.tracer.metadata)
213
+ )
214
+ else:
215
+ await self.ping()
216
+
217
+ async def ping(self):
218
+ """
219
+ Ping the Manager
220
+ """
221
+ try:
222
+ metrics = await self.metrics_collector.collect_all_metrics()
223
+ await self.publish_message(
224
+ "manager/set_system_summary",
225
+ MqMetricsSnapshot(
226
+ token=self.node_client.secured_token, snapshot=metrics
227
+ ),
228
+ )
229
+ except MantaGRPCError as e:
230
+ self.tracer.exception(f"Error when pinging the Manager: {e}")
231
+ except MantaError as e:
232
+ self.tracer.exception(f"Error when pinging the Manager: {e}")
233
+ await self.node_client.send_error(e)
234
+
235
+ async def process_message(self, message: aiomqtt.Message):
236
+ """
237
+ Handle messages from MQTT
238
+
239
+ Parameters
240
+ ----------
241
+ message : aiomqtt.Message
242
+ Message
243
+ """
244
+ try:
245
+ self.tracer.clean_metadata()
246
+ key = str(message.topic).split("/")[-1]
247
+ payload = cast(bytes, message.payload)
248
+
249
+ if key == "deploy_task":
250
+ await self.deploy_task(payload)
251
+ elif key == "stop_task":
252
+ task_update = await self.stop_task(payload)
253
+ if task_update is not None:
254
+ await self.publish_task_update(task_update)
255
+ elif key == "stop_swarm":
256
+ await self.stop_swarm(payload)
257
+ elif key == "collect_information":
258
+ await self.collect_information(payload)
259
+ elif key == "stop_node":
260
+ await self.stop_node(payload)
261
+ else:
262
+ self.tracer.error(f"Not allowed method: {key}")
263
+ raise MantaMQTTError(
264
+ f"Not allowed method: {key}", metadata=self.tracer.metadata
265
+ )
266
+ except MantaGRPCError as e:
267
+ self.tracer.exception(f"Error sending message to the Manager: {e}")
268
+ except MantaError as manta_error:
269
+ self.tracer.exception(f"Error when handling message: {manta_error}")
270
+ await self.node_client.send_error(manta_error)
271
+
272
+ async def publish_task_update(self, task_update: TaskUpdate):
273
+ """
274
+ Publish a task update to the Manager through MQTT
275
+ Message broker
276
+
277
+ Parameters
278
+ ----------
279
+ task_update : TaskUpdate
280
+ Task Update
281
+ """
282
+ await self.publish_message(
283
+ "manager/update_task_status",
284
+ MqTaskUpdate(token=self.node_client.secured_token, update=task_update),
285
+ )
286
+
287
+ async def send_task_logs(self, date: datetime, processed_logs: bytes):
288
+ """
289
+ Send task logs to the Manager
290
+
291
+ Parameters
292
+ ----------
293
+ date : datetime
294
+ Date when logs where created
295
+ processed_logs : bytes
296
+ Logs prepared from tracer
297
+ """
298
+
299
+ async def chunk_logs(content: bytes) -> AsyncIterable[Logs]:
300
+ data_stream = io.BytesIO(content)
301
+ while chunk := data_stream.read(CHUNK_SIZE):
302
+ yield Logs(content=chunk, timestamp=date)
303
+
304
+ await self.node_client.add_task_logs(chunk_logs(processed_logs))
305
+
306
+ async def stop_node(self, _payload: bytes):
307
+ """
308
+ Stop the node and prepare for graceful shutdown
309
+
310
+ Parameters
311
+ ----------
312
+ payload : bytes
313
+ Node ID to stop
314
+ """
315
+ await self.stop_callback()