manta-node 0.5b0.dev259__py3-none-any.whl → 0.5b0.dev264__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.
@@ -1,3 +1,5 @@
1
+ import asyncio
2
+ import concurrent.futures
1
3
  from datetime import datetime
2
4
 
3
5
  from aiodocker import Docker
@@ -13,6 +15,80 @@ class DockerRaiser:
13
15
  tracer: Tracer
14
16
  docker_client: DockerClient
15
17
 
18
+ # Dedicated, bounded executor for image pulls only. A multi-GB pull holds
19
+ # a thread for the full download; routing pulls through their own small
20
+ # pool (instead of asyncio.to_thread's shared default ThreadPoolExecutor,
21
+ # min(32, cpu+4) workers) keeps several concurrent large pulls from
22
+ # starving unrelated to_thread/run_in_executor work on the node. Process-
23
+ # lifetime singleton — the node process runs indefinitely, so no explicit
24
+ # shutdown is needed. image_needs_pull's inspect_distribution check is
25
+ # NOT routed through this pool: it's quick and issued right before the
26
+ # pull, and sharing a 4-slot pool with in-flight pulls could block it
27
+ # waiting for a pull slot.
28
+ _PULL_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
29
+ max_workers=4, thread_name_prefix="docker-pull"
30
+ )
31
+
32
+ async def image_needs_pull(self, image: str) -> bool:
33
+ """
34
+ Check whether ``image`` is missing locally or stale relative to the registry.
35
+
36
+ Compares the local image's ``RepoDigests`` against the remote manifest
37
+ digest, obtained via the Docker daemon's own distribution API
38
+ (``docker_client.api.inspect_distribution``). This reuses the daemon's
39
+ registry auth, media-type negotiation and multi-arch resolution — the
40
+ same machinery ``docker pull`` itself relies on — instead of a bespoke
41
+ registry client.
42
+
43
+ Parameters
44
+ ----------
45
+ image : str
46
+ Image reference to check (e.g. ``"repo/image:latest"``)
47
+
48
+ Returns
49
+ -------
50
+ bool
51
+ True if the image should be (re)pulled.
52
+
53
+ Notes
54
+ -----
55
+ Graceful degradation: if the registry check itself fails (unreachable,
56
+ unauthenticated, rate-limited, ...), a warning is logged and this falls
57
+ back to "pull only if absent locally" — it never raises out of task
58
+ launch.
59
+ """
60
+ try:
61
+ local = await self.async_client.images.inspect(image)
62
+ except Exception:
63
+ # Image not present locally: must pull regardless of the registry.
64
+ return True
65
+
66
+ local_digests = {
67
+ digest.split("@", 1)[1]
68
+ for digest in (local.get("RepoDigests") or [])
69
+ if "@" in digest
70
+ }
71
+ if not local_digests:
72
+ # Locally-built/tagged image (never pushed/pulled), or only
73
+ # malformed RepoDigests entries: nothing to compare against the
74
+ # registry. Treat as up-to-date rather than overwriting a
75
+ # local-only image on every launch.
76
+ return False
77
+
78
+ try:
79
+ distribution = await asyncio.to_thread(
80
+ self.docker_client.api.inspect_distribution, image
81
+ )
82
+ remote_digest = distribution["Descriptor"]["digest"]
83
+ except Exception as exc:
84
+ self.tracer.warning(
85
+ f"Cannot verify registry digest for {image}, skipping "
86
+ f"auto-pull staleness check: {exc!r}"
87
+ )
88
+ return False
89
+
90
+ return remote_digest not in local_digests
91
+
16
92
  async def run_or_raise(self, config: dict) -> DockerContainer:
17
93
  """
18
94
  Start, pull or run a container
@@ -74,6 +150,19 @@ class DockerRaiser:
74
150
  """
75
151
  Pull an image
76
152
 
153
+ Pulls via the sync docker-py client rather than the async aiodocker
154
+ client. docker-py auto-resolves registry credentials from
155
+ ``~/.docker/config.json`` (including credHelpers/credsStore) — the
156
+ same client ``image_needs_pull`` already uses for
157
+ ``inspect_distribution`` — whereas aiodocker pulls anonymously and
158
+ 404s on private images with "pull access denied ... may require
159
+ 'docker login'" (refs #348).
160
+
161
+ Routed through ``self._PULL_EXECUTOR`` (a dedicated, bounded thread
162
+ pool) rather than ``asyncio.to_thread``'s shared default executor, so
163
+ a long-running multi-GB pull can't starve unrelated to_thread/
164
+ run_in_executor work elsewhere on the node (refs #347 review).
165
+
77
166
  Parameters
78
167
  ----------
79
168
  image : str
@@ -85,7 +174,10 @@ class DockerRaiser:
85
174
  Cannot pull image
86
175
  """
87
176
  try:
88
- await self.async_client.images.pull(image)
177
+ loop = asyncio.get_running_loop()
178
+ await loop.run_in_executor(
179
+ self._PULL_EXECUTOR, self.docker_client.images.pull, image
180
+ )
89
181
  except Exception as exc:
90
182
  self.tracer.error(f"Image {image} cannot be pulled")
91
183
  raise MantaDockerError(
@@ -115,37 +115,80 @@ class ImageExecutor(DockerRaiser):
115
115
  task_id: ID,
116
116
  image: str,
117
117
  environment: dict | None = None,
118
+ gpu: bool = False,
119
+ force_pull: bool = False,
118
120
  ) -> datetime:
119
121
  """
120
- Pull (if not present locally) and start a Docker image with its native ENTRYPOINT.
122
+ Pull (when missing or stale) and start a Docker image with its native ENTRYPOINT.
123
+
124
+ Parameters
125
+ ----------
126
+ swarm_id : ID
127
+ Swarm ID
128
+ task_id : ID
129
+ Task ID
130
+ image : str
131
+ Image to run
132
+ environment : Optional[dict]
133
+ Environment variables to inject into the container
134
+ gpu : bool
135
+ Attach GPU support, reusing the same ``gpu_config`` detection as
136
+ ``ContainerManager.run_container``
137
+ force_pull : bool
138
+ Force a pull before running, bypassing the digest staleness check
121
139
 
122
140
  Returns
123
141
  -------
124
142
  datetime
125
143
  Container start time
144
+
145
+ Raises
146
+ ------
147
+ MantaDockerError
148
+ GPU requested but not supported by this container manager
126
149
  """
127
150
  tracked = set(getattr(self, "tasks", None) and self.tasks._env_ids or [])
128
151
  tracked |= getattr(self, "_launching", None) or set()
129
152
  await self._ensure_port_free(ORPHAN_PORT, tracked)
130
- try:
131
- await self.async_client.images.inspect(image)
132
- except Exception:
133
- self.tracer.info(f"Image {image} not found locally, pulling...")
153
+
154
+ # gpu_config is provided by ContainerManager via MRO in the real
155
+ # TaskManager(ContainerManager, ImageExecutor) composition; guard with
156
+ # getattr so a standalone ImageExecutor degrades to the same error
157
+ # run_container raises, instead of an AttributeError. This check runs
158
+ # BEFORE the pull decision — same ordering as run_container — so a
159
+ # GPU task on a non-GPU node fails fast instead of wastefully pulling
160
+ # a (possibly large) image first.
161
+ gpu_config = getattr(self, "gpu_config", None)
162
+ if gpu and not gpu_config:
163
+ raise MantaDockerError(
164
+ "GPU support is not enabled for this container manager.",
165
+ metadata=self.tracer.metadata,
166
+ )
167
+
168
+ if force_pull or await self.image_needs_pull(image):
169
+ self.tracer.info(f"Pulling image {image}...")
134
170
  await self.pull_image_or_raise(image)
135
171
 
172
+ # Copy (not alias) gpu_config's HostConfig — it is shared, long-lived
173
+ # state on self; mutating it in place would leak NetworkMode/ExtraHosts
174
+ # onto every future GPU-enabled run.
175
+ host_config = (
176
+ dict(gpu_config.get("HostConfig", {})) if (gpu and gpu_config) else {}
177
+ )
178
+ host_config["NetworkMode"] = "host"
179
+ host_config["ExtraHosts"] = ["host.docker.internal:host-gateway"]
180
+
136
181
  env = [f"{k}={v}" for k, v in environment.items()] if environment else None
137
182
  config = {
138
183
  "Image": image,
139
- "HostConfig": {
140
- "NetworkMode": "host",
141
- "ExtraHosts": ["host.docker.internal:host-gateway"],
142
- },
184
+ "HostConfig": host_config,
143
185
  "Env": env,
144
186
  "Detach": True,
145
187
  "LogConfig": {
146
188
  "type": "json-file",
147
189
  "config": {"max-size": "10m", "max-file": "3"},
148
190
  },
191
+ "Runtime": (gpu_config.get("Runtime") if (gpu and gpu_config) else None),
149
192
  "Labels": {
150
193
  "com.docker.compose.project": "manta_tasks",
151
194
  "manta.swarm-id": ID(swarm_id).xid,
@@ -292,6 +292,7 @@ class TaskManager(ContainerManager, ImageExecutor):
292
292
  "SWARM_ID": swarm_id.xid,
293
293
  }
294
294
  ),
295
+ gpu=task.gpu,
295
296
  )
296
297
  self.image_task_ids.add(env_tid_pair)
297
298
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: manta_node
3
- Version: 0.5b0.dev259
3
+ Version: 0.5b0.dev264
4
4
  Summary: Node side software of the Manta platform. It is responsible for the communication with the Manta Core and the execution of the tasks.
5
5
  Author-email: Hugo Miralles <hugomiralles@manta-tech.io>, Matthew Thompson <matthew.thompson@manta-tech.io>
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,7 +1,7 @@
1
1
  manta_node/__init__.py,sha256=bgGWXH3yQVhNf0v1ZGUBAy5Y51fqb0Y-EaLnocbcino,153
2
2
  manta_node/__main__.py,sha256=SjrJyuMg_ZFKV6WOVe3MrtWM8Qmu065dkKZYl4n2YLw,1665
3
3
  manta_node/node_orchestrator.py,sha256=DW1kiaeybytw656-OJqfhsRiJGMtGS0hfDzIpPyjmdU,17327
4
- manta_node/task_manager.py,sha256=5C7KN2t985wbs7tf4BP1CcNVKF0A1Av1m8JohyUPoaM,24368
4
+ manta_node/task_manager.py,sha256=aylQs3FAN-zaFd3G99eC_7XWDqBZyVXPJeW-ZqQ5euU,24402
5
5
  manta_node/tasks.py,sha256=uBVaWwFq73vHxmoG8Fr4xxkX7rao37KU5hEs79jIrgo,6785
6
6
  manta_node/utils.py,sha256=Hw4Iw2jYQYsfVLVJuBMt-_rMFBeml3nCDE2jiiXc8YM,1155
7
7
  manta_node/cli/__init__.py,sha256=arzO3mXDJ-E2yeeytbZfN7mCswwxdQ43_QjyeL2E5yE,134
@@ -21,8 +21,8 @@ manta_node/infrastructure/__init__.py,sha256=yM60jKhdaD18BOsLb-0aR9D2XxGvrTnV92o
21
21
  manta_node/infrastructure/config/__init__.py,sha256=VnKidPAVfzlZMb5WM-MggtsDLM8tdAFFWlB7WSiawzc,617
22
22
  manta_node/infrastructure/config/node_config_manager.py,sha256=u9ga_z7EQ775wkgCVOT0oY4o2uYjAjhCzR1BZmmtNZ0,30605
23
23
  manta_node/infrastructure/container/__init__.py,sha256=EO4SndUwmXrHczM4qkleUbxC7lTGz3c3Hd3iGrPg6D8,262
24
- manta_node/infrastructure/container/docker_adapter.py,sha256=v6tbwSyZp1eGe-rZUat-Y2vSNUKc3kz6zCafsRz7d7M,8395
25
- manta_node/infrastructure/container/image_executor.py,sha256=ueFl9q4BguZlkBwQMbkdcQ4SQOqMdpLE0Hb4p60GPEs,8499
24
+ manta_node/infrastructure/container/docker_adapter.py,sha256=j4gMPB0FJI_jBRgHX-24er9AgHNU87kb6R4pyKhKaqs,12432
25
+ manta_node/infrastructure/container/image_executor.py,sha256=E3vT3PnfkVxnmM29_r74GKwL_Yr-iYp5PItAGHn-e24,10327
26
26
  manta_node/infrastructure/container/manager.py,sha256=ZOMOZaeF0LZUEbcPWGIvpoAfmPmbtzXzayY_v7uP518,18198
27
27
  manta_node/infrastructure/filesystem/__init__.py,sha256=nf6Fakp3wuFKjSAm_oihI6WE_yyUY6Uelr7A1GwSvXs,145
28
28
  manta_node/infrastructure/filesystem/dataset_manager.py,sha256=8aexrjrkWZ7OCuS0HkjYXZFtg_8Y5WOJ540inK7BYRs,14968
@@ -35,9 +35,9 @@ manta_node/infrastructure/metrics/collector.py,sha256=9ESw__Pc2tV6ldG5yhos2IS7T5
35
35
  manta_node/infrastructure/metrics/metrics_collector.py,sha256=WKN3e4q5PANHMrBScaxNSpSoS5ObGLH8QGR0gE1IAOk,10428
36
36
  manta_node/infrastructure/mqtt/__init__.py,sha256=riXo1sXgO4YV6DHCmWRqK61BAmuGLdHiVdQFunAJadA,140
37
37
  manta_node/infrastructure/mqtt/command_handler.py,sha256=Oj1MORxriUvkBymKsSuXoLqVaOoZWgAcoXy537x35oU,17389
38
- manta_node-0.5b0.dev259.dist-info/licenses/LICENSE,sha256=P5HojupjKoAusNtHqWOvO-YFolwLFWQ7Uq0XVKK9nFQ,35291
39
- manta_node-0.5b0.dev259.dist-info/METADATA,sha256=wmdFVZkG6LbgyuLCPVO4OrgfBu3fCDXPYfwMQt2GT_Y,44683
40
- manta_node-0.5b0.dev259.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
41
- manta_node-0.5b0.dev259.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
42
- manta_node-0.5b0.dev259.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
43
- manta_node-0.5b0.dev259.dist-info/RECORD,,
38
+ manta_node-0.5b0.dev264.dist-info/licenses/LICENSE,sha256=P5HojupjKoAusNtHqWOvO-YFolwLFWQ7Uq0XVKK9nFQ,35291
39
+ manta_node-0.5b0.dev264.dist-info/METADATA,sha256=-QVhTQWavTpz2b-lDkW0eE56rGZ5qAZPR6dlCRJa5EA,44683
40
+ manta_node-0.5b0.dev264.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
41
+ manta_node-0.5b0.dev264.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
42
+ manta_node-0.5b0.dev264.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
43
+ manta_node-0.5b0.dev264.dist-info/RECORD,,