manta-node 0.6b1.dev664__py3-none-any.whl → 0.6b5.dev672__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.
@@ -4,7 +4,7 @@ This file is auto-generated at build time by scripts/generate_version.py.
4
4
  Do not edit manually.
5
5
  """
6
6
 
7
- __version__ = "2026.09.18.b4934360"
7
+ __version__ = "2026.09.18.ba835c28"
8
8
  __build_date__ = "2026.09.18"
9
- __commit_id__ = "b4934360"
10
- __package_version__ = "0.6b1"
9
+ __commit_id__ = "ba835c28"
10
+ __package_version__ = "0.6b5"
manta_node/cli/version.py CHANGED
@@ -98,7 +98,7 @@ def get_full_version_info() -> dict:
98
98
  "version": get_version_string(),
99
99
  "date": datetime.now().strftime("%Y.%m.%d"),
100
100
  "commit": commit_id or "unknown",
101
- "package_version": "0.6b1", # mirrors pyproject.toml [project].version; nothing syncs the two, so both move together
101
+ "package_version": "0.6b5", # mirrors pyproject.toml [project].version; nothing syncs the two, so both move together
102
102
  }
103
103
 
104
104
 
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import concurrent.futures
3
+ import shutil
3
4
  from datetime import datetime
4
5
  from enum import Enum
5
6
 
@@ -7,6 +8,7 @@ from aiodocker import Docker
7
8
  from aiodocker.docker import DockerContainer
8
9
  from aiodocker.exceptions import DockerError
9
10
  from docker import DockerClient
11
+ from docker.utils import parse_repository_tag
10
12
  from manta_common.errors import MantaDockerError
11
13
  from manta_common.traces import Tracer
12
14
 
@@ -35,6 +37,27 @@ class ImageFreshness(Enum):
35
37
  UNKNOWN = "unknown"
36
38
 
37
39
 
40
+ # The daemon's own vocabulary for an image with no tag and no digest -- one
41
+ # nobody can name and no launch can reach.
42
+ _DANGLING = {"dangling": ["true"]}
43
+
44
+
45
+ def _repository(image: str) -> str:
46
+ """The repository of an image reference, without tag or digest.
47
+
48
+ docker-py's parser, because a registry port (``localhost:5000/r/i:t``) is
49
+ not a tag separator and a hand-written split gets it wrong.
50
+ """
51
+ repository, _tag = parse_repository_tag(image)
52
+ return repository
53
+
54
+
55
+ def _human(size: int) -> str:
56
+ if size >= 2**30:
57
+ return f"{size / 2**30:.1f} GiB"
58
+ return f"{size / 2**20:.1f} MiB"
59
+
60
+
38
61
  class DockerRaiser:
39
62
  async_client: Docker
40
63
  tracer: Tracer
@@ -45,6 +68,8 @@ class DockerRaiser:
45
68
  _PULL_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
46
69
  max_workers=4, thread_name_prefix="docker-pull"
47
70
  )
71
+ # Per instance, created on first use: this class has no __init__ of its own.
72
+ _pull_lock: asyncio.Lock | None = None
48
73
 
49
74
  async def image_freshness(self, image: str) -> ImageFreshness:
50
75
  """
@@ -149,6 +174,39 @@ class DockerRaiser:
149
174
  local-only image with nothing to compare), ``UNKNOWN`` when the
150
175
  registry could not be asked. ``STALE`` is never returned — a stale
151
176
  image is pulled, and a pull that fails raises instead of returning.
177
+
178
+ Raises
179
+ ------
180
+ MantaDockerError
181
+ The pull failed, or was refused because the daemon's filesystem
182
+ cannot hold the image (below).
183
+
184
+ Notes
185
+ -----
186
+ A pull is one serialized sequence, ``_pull_replacing``: prune, measure,
187
+ refuse, pull, remove. The ORDER is the invariant — a moved tag must never
188
+ need more free space than a single image. Removing the image a tag left
189
+ behind AFTER the pull cannot be what makes room, because on a node
190
+ holding N leftovers the pull is the thing that fails; so every dangling
191
+ image no container uses goes BEFORE the pull, the free space on the
192
+ daemon's data root is measured AFTER they are gone, and a pull that still
193
+ cannot fit is refused in one readable error rather than dying inside
194
+ docker-py with its reason swallowed. One lock covers the whole sequence:
195
+ two concurrent starts measuring the same free space would each admit a
196
+ pull the other one spends.
197
+
198
+ Two facts of the containerd image store shape those steps. An image a
199
+ tag moved off keeps NO RepoTags and NO RepoDigests, so the leftovers of
200
+ a repository cannot be asked for by name — ``dangling`` is the criterion,
201
+ narrowed to images no container (running or exited) uses, unforced so the
202
+ daemon's own 409 is the backstop. And ``GET /images/json`` reports
203
+ ``Size`` as the on-disk footprint while ``inspect`` reports the
204
+ compressed content alone, so the pull is sized from the listing: the
205
+ largest local image of the same repository, a floor rather than the
206
+ figure (layer sharing lowers the need, download scratch space raises
207
+ it). Housekeeping never fails a launch, and a question the daemon cannot
208
+ answer — no local image of the repository, a data root not on this host
209
+ — proceeds and says so, the rule ``UNKNOWN`` already sets above.
152
210
  """
153
211
  if force_pull:
154
212
  # Ahead of the comparison, not merged into it: the comparison is a
@@ -172,10 +230,97 @@ class DockerRaiser:
172
230
  return freshness
173
231
 
174
232
  async def _pull_replacing(self, image: str) -> None:
175
- """Pull ``image`` and, when the pull moved the tag, remove the image it left."""
176
- previous_id = await self._local_image_id(image)
177
- await self.pull_image_or_raise(image)
178
- await self._remove_superseded_image(image, previous_id)
233
+ """Prune, measure, refuse, pull, remove what the tag left — under one lock."""
234
+ if self._pull_lock is None:
235
+ self._pull_lock = asyncio.Lock()
236
+ async with self._pull_lock:
237
+ previous_id = await self._local_image_id(image)
238
+ await self._prune_dangling_images(image)
239
+ await self._refuse_pull_without_room(image)
240
+ await self.pull_image_or_raise(image)
241
+ await self._remove_superseded_image(image, previous_id)
242
+
243
+ async def _prune_dangling_images(self, image: str) -> None:
244
+ """Delete every dangling image no container uses; unforced, never raises."""
245
+ try:
246
+ dangling = await self.async_client.images.list(filters=_DANGLING)
247
+ containers = await self.async_client.containers.list(all=True)
248
+ except Exception as exc:
249
+ self.tracer.warning(
250
+ f"Dangling images not pruned before pulling {image}: {exc!r}"
251
+ )
252
+ return
253
+ in_use = {container["ImageID"] for container in containers}
254
+ for entry in dangling:
255
+ image_id = entry.get("Id")
256
+ if not image_id:
257
+ continue
258
+ if image_id in in_use:
259
+ self.tracer.info(
260
+ f"Dangling image {image_id} kept before pulling {image}: a "
261
+ f"container still uses it"
262
+ )
263
+ continue
264
+ try:
265
+ await self.async_client.images.delete(image_id)
266
+ except Exception as exc:
267
+ self.tracer.warning(
268
+ f"Dangling image {image_id} kept before pulling {image}: {exc!r}"
269
+ )
270
+ continue
271
+ self.tracer.info(
272
+ f"Removed dangling image {image_id} "
273
+ f"({_human(int(entry.get('Size') or 0))}) before pulling {image}"
274
+ )
275
+
276
+ async def _refuse_pull_without_room(self, image: str) -> None:
277
+ """Raise before the pull when the daemon's filesystem cannot hold it."""
278
+ try:
279
+ info = await asyncio.to_thread(self.docker_client.info)
280
+ root = info["DockerRootDir"]
281
+ except Exception as exc:
282
+ self.tracer.warning(
283
+ f"Free space not checked before pulling {image}: the daemon's data "
284
+ f"root could not be read: {exc!r}"
285
+ )
286
+ return
287
+ try:
288
+ free = (await asyncio.to_thread(shutil.disk_usage, root)).free
289
+ except OSError as exc:
290
+ # A remote daemon's root is not on this filesystem.
291
+ self.tracer.warning(
292
+ f"Free space not checked before pulling {image}: {root} could not "
293
+ f"be measured from this host: {exc!r}"
294
+ )
295
+ return
296
+
297
+ repository = _repository(image)
298
+ try:
299
+ siblings = await self.async_client.images.list(
300
+ filters={"reference": [repository]}
301
+ )
302
+ except Exception as exc:
303
+ self.tracer.warning(
304
+ f"Free space not checked before pulling {image}: the local images "
305
+ f"of {repository} could not be listed: {exc!r}"
306
+ )
307
+ return
308
+ needed = max((int(entry.get("Size") or 0) for entry in siblings), default=0)
309
+ if needed == 0:
310
+ self.tracer.info(
311
+ f"{_human(free)} free under {root}; no local image of {repository} "
312
+ f"to size the pull of {image} against"
313
+ )
314
+ return
315
+ if free < needed:
316
+ message = (
317
+ f"Image {image} not pulled: {_human(free)} free under {root}, and "
318
+ f"the {repository} image already on this node occupies "
319
+ f"{_human(needed)}. A pull that size cannot complete here; the "
320
+ f"node already pruned what it could -- free the disk and retry."
321
+ )
322
+ self.tracer.error(message)
323
+ raise MantaDockerError(message, metadata=self.tracer.metadata)
179
324
 
180
325
  async def _local_image_id(self, image: str) -> str | None:
181
326
  """The image id ``image`` resolves to on this daemon, or None if absent."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: manta_node
3
- Version: 0.6b1.dev664
3
+ Version: 0.6b5.dev672
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 <hugo.miralles@manta-tech.io>, Matthew Thompson <matthew.thompson@manta-tech.io>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -28,7 +28,7 @@ Requires-Dist: pyyaml
28
28
  Requires-Dist: psutil
29
29
  Requires-Dist: msgpack
30
30
  Requires-Dist: aiomqtt<3.0,>=2.5.1
31
- Requires-Dist: manta-common-core<=0.6b1,>=0.6b1.dev644
31
+ Requires-Dist: manta-common-core<=0.6b5,>=0.6b5.dev0
32
32
  Requires-Dist: toml
33
33
  Requires-Dist: tomli; python_version < "3.11"
34
34
  Requires-Dist: tomli-w
@@ -51,7 +51,7 @@ Dynamic: license-file
51
51
 
52
52
  # Manta Node Python
53
53
 
54
- ![Version](https://img.shields.io/badge/version-0.6b1-orange)
54
+ ![Version](https://img.shields.io/badge/version-0.6b5-orange)
55
55
  ![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20-blue)
56
56
  ![Coverage Badge](.github/coverage/coverage.svg)
57
57
  ![Ruff](https://img.shields.io/badge/code_style-ruff-2a1833)
@@ -5,10 +5,10 @@ manta_node/task_manager.py,sha256=xRE-yKdUW0FFR8vtX1jDwl1AxTbqVD_JS7QVR62GA00,38
5
5
  manta_node/tasks.py,sha256=o837Hi-KFVJg9LF5iOtE8oD6UuTOfrEPWR-JOdhRGeY,6983
6
6
  manta_node/utils.py,sha256=Hw4Iw2jYQYsfVLVJuBMt-_rMFBeml3nCDE2jiiXc8YM,1155
7
7
  manta_node/cli/__init__.py,sha256=arzO3mXDJ-E2yeeytbZfN7mCswwxdQ43_QjyeL2E5yE,134
8
- manta_node/cli/_version_generated.py,sha256=z5l9YimO0j_n8NgmuO_ao5t4sw4wBPFdgX__tVPQQ2Y,275
8
+ manta_node/cli/_version_generated.py,sha256=kQBkXymO8TdGeYtp5QcYpA4uOGjqOWyB_bV_1WRaGGk,275
9
9
  manta_node/cli/config_manager.py,sha256=BCju9zqzraviL3KrzRnB-vZQUY__Q5kuEqb7EdB8YdI,4409
10
10
  manta_node/cli/main.py,sha256=dZa6zrypWBvpN8hd4qpxevvlyNFhxKINCg1537NJ36w,4318
11
- manta_node/cli/version.py,sha256=KbI0pHsBxT5LayA9fTTMBR6LIIcDeZJxyqQCwjQO560,3115
11
+ manta_node/cli/version.py,sha256=3lrnIWpOosszBN-VsCBVLxDgpvCC7IB0aDIKx_3e7hI,3115
12
12
  manta_node/cli/commands/__init__.py,sha256=GKLOURXZqec5B3H3EiwX41xDISGg6cO_kCueoyZvt0Q,505
13
13
  manta_node/cli/commands/cluster.py,sha256=kUVopCN4xkEk-IR1aKpr7bfBSQEtG6pEj_hlWno5SZQ,14257
14
14
  manta_node/cli/commands/config.py,sha256=BiarCD0S8rcNYbio4RLzDvUS7A50s5dd5F4TiZqLR6M,16577
@@ -26,7 +26,7 @@ manta_node/infrastructure/executor.py,sha256=wbH5-G4Y3iNTPr8ROWRDT-bWD-CYl1YFlZQ
26
26
  manta_node/infrastructure/config/__init__.py,sha256=VnKidPAVfzlZMb5WM-MggtsDLM8tdAFFWlB7WSiawzc,617
27
27
  manta_node/infrastructure/config/node_config_manager.py,sha256=Js6PISjR_JRXMhgFC7lzI6StjW5paFhdhmtteao9j5A,35228
28
28
  manta_node/infrastructure/container/__init__.py,sha256=ACDyzGvtM7nfTsBXDAYFjkKgU2yu4Umj3CQ_rlRfw1I,300
29
- manta_node/infrastructure/container/docker_adapter.py,sha256=IkVdzVXGSey6EfRzOmiKFNzJvS9Z7JBfB3Y9x9oJLCY,17689
29
+ manta_node/infrastructure/container/docker_adapter.py,sha256=hI5YenhcZwn19_chA8UM2RT6cmcnknzVcmu7AeHrkWo,24221
30
30
  manta_node/infrastructure/container/executor.py,sha256=0aTFXDv-oXz81qwGi6zl_4qtkFriPWzn7iiiTRg7KJM,7274
31
31
  manta_node/infrastructure/container/image_executor.py,sha256=noNwde03hw9k3EnK1JuQGtByUf-XDHqx623X0DKgHZ0,10315
32
32
  manta_node/infrastructure/container/manager.py,sha256=kylDl-5VZk01tzSXVrWI9VPn8mqi6tV-7Rf1tkMgee0,15790
@@ -51,9 +51,9 @@ manta_node/infrastructure/process/executor.py,sha256=Ab1XvNuelEGCD5KdZLgYYnpjYZ5
51
51
  manta_node/infrastructure/security/__init__.py,sha256=lI6vkbUmfmt0m363iO4HzuzVNoQAc7Clp2R5vAhZbgE,64
52
52
  manta_node/infrastructure/security/auth_agent.py,sha256=WkPTg_g0gix7xomb_on_Ft3TNX0FpeqS7gwgJW9mTvk,11037
53
53
  manta_node/infrastructure/security/token_provider.py,sha256=u3bcbQYNBskFzunO-wOSZpGkDN_CU1BCB5HUbZTC4Vg,1832
54
- manta_node-0.6b1.dev664.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
55
- manta_node-0.6b1.dev664.dist-info/METADATA,sha256=l6ZlfudtPepYV949B8DFT7Fg1zs2v_e58x0KrTJ-vRQ,4215
56
- manta_node-0.6b1.dev664.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
57
- manta_node-0.6b1.dev664.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
58
- manta_node-0.6b1.dev664.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
59
- manta_node-0.6b1.dev664.dist-info/RECORD,,
54
+ manta_node-0.6b5.dev672.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
55
+ manta_node-0.6b5.dev672.dist-info/METADATA,sha256=F8qtn5_RUOnONURnsYxEbm2SWM41xJQ04AbK7eAc8z0,4213
56
+ manta_node-0.6b5.dev672.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
57
+ manta_node-0.6b5.dev672.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
58
+ manta_node-0.6b5.dev672.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
59
+ manta_node-0.6b5.dev672.dist-info/RECORD,,