manta-node 0.6b0.dev607__py3-none-any.whl → 0.6b0.dev615__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,6 +1,6 @@
1
1
  """Container infrastructure - Docker runtime management."""
2
2
 
3
- from .docker_adapter import DockerRaiser
3
+ from .docker_adapter import DockerRaiser, ImageFreshness
4
4
  from .image_executor import ImageExecutor
5
5
  from .manager import ContainerManager
6
6
 
@@ -8,4 +8,5 @@ __all__ = [
8
8
  "ContainerManager",
9
9
  "DockerRaiser",
10
10
  "ImageExecutor",
11
+ "ImageFreshness",
11
12
  ]
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import concurrent.futures
3
3
  from datetime import datetime
4
+ from enum import Enum
4
5
 
5
6
  from aiodocker import Docker
6
7
  from aiodocker.docker import DockerContainer
@@ -10,6 +11,30 @@ from manta_common.errors import MantaDockerError
10
11
  from manta_common.traces import Tracer
11
12
 
12
13
 
14
+ class ImageFreshness(Enum):
15
+ """How the local copy of an image compares with the registry's.
16
+
17
+ THREE outcomes, and ``UNKNOWN`` must never be collapsed into ``CURRENT``.
18
+ A caller that cannot tell *these are the registry's bytes* from *nobody
19
+ could ask* runs stale bytes while reporting an ordinary start -- silent in
20
+ both directions, which is the defect in #457.
21
+
22
+ What separates ``UNKNOWN`` from ``CURRENT`` is WHY no comparison happened,
23
+ never that none happened. An image with no ``RepoDigests`` was built
24
+ locally and never pushed: there is nothing it could be stale against, so it
25
+ is ``CURRENT``. That is not a degraded reading and must not become one --
26
+ ``install_manta_platform.sh`` builds ``manta-light:pytorch`` under exactly
27
+ that unprefixed name, and only the name distinguishes it from the registry
28
+ image, so reporting it ``UNKNOWN`` would put a permanent warning on a
29
+ correct state and spend the third outcome's credibility on the case where
30
+ it means nothing.
31
+ """
32
+
33
+ CURRENT = "current"
34
+ STALE = "stale"
35
+ UNKNOWN = "unknown"
36
+
37
+
13
38
  class DockerRaiser:
14
39
  async_client: Docker
15
40
  tracer: Tracer
@@ -21,9 +46,9 @@ class DockerRaiser:
21
46
  max_workers=4, thread_name_prefix="docker-pull"
22
47
  )
23
48
 
24
- async def image_needs_pull(self, image: str) -> bool:
49
+ async def image_freshness(self, image: str) -> ImageFreshness:
25
50
  """
26
- Check whether ``image`` is missing locally or stale relative to the registry.
51
+ Compare the local copy of ``image`` with the registry's.
27
52
 
28
53
  Compares the local image's ``RepoDigests`` against the remote manifest
29
54
  digest, obtained via the Docker daemon's own distribution API
@@ -39,21 +64,23 @@ class DockerRaiser:
39
64
 
40
65
  Returns
41
66
  -------
42
- bool
43
- True if the image should be (re)pulled.
67
+ ImageFreshness
68
+ ``STALE`` if the image must be (re)pulled, ``CURRENT`` if the local
69
+ bytes are the right ones, ``UNKNOWN`` if that question could not be
70
+ answered.
44
71
 
45
72
  Notes
46
73
  -----
47
- Graceful degradation: if the registry check itself fails (unreachable,
48
- unauthenticated, rate-limited, ...), a warning is logged and this falls
49
- back to "pull only if absent locally" — it never raises out of task
50
- launch.
74
+ Never raises out of task launch. A registry that cannot be reached
75
+ (unreachable, unauthenticated, rate-limited, ...) yields ``UNKNOWN``
76
+ and a warning, NOT ``CURRENT``: the caller decides what to do about an
77
+ unanswered question, and it must be able to see that it was unanswered.
51
78
  """
52
79
  try:
53
80
  local = await self.async_client.images.inspect(image)
54
81
  except Exception:
55
82
  # Image not present locally: must pull regardless of the registry.
56
- return True
83
+ return ImageFreshness.STALE
57
84
 
58
85
  local_digests = {
59
86
  digest.split("@", 1)[1]
@@ -63,9 +90,9 @@ class DockerRaiser:
63
90
  if not local_digests:
64
91
  # Locally-built/tagged image (never pushed/pulled), or only
65
92
  # malformed RepoDigests entries: nothing to compare against the
66
- # registry. Treat as up-to-date rather than overwriting a
67
- # local-only image on every launch.
68
- return False
93
+ # registry. CURRENT, not UNKNOWN — there is no question left open
94
+ # here, so the registry is never consulted and no warning is due.
95
+ return ImageFreshness.CURRENT
69
96
 
70
97
  try:
71
98
  # Default executor, NOT _PULL_EXECUTOR: this quick probe must not
@@ -75,13 +102,74 @@ class DockerRaiser:
75
102
  )
76
103
  remote_digest = distribution["Descriptor"]["digest"]
77
104
  except Exception as exc:
105
+ # Wording pinned by test_image_freshness_keeps_the_operator_grep_target.
78
106
  self.tracer.warning(
79
107
  f"Cannot verify registry digest for {image}, skipping "
80
108
  f"auto-pull staleness check: {exc!r}"
81
109
  )
82
- return False
110
+ return ImageFreshness.UNKNOWN
83
111
 
84
- return remote_digest not in local_digests
112
+ if remote_digest in local_digests:
113
+ return ImageFreshness.CURRENT
114
+ return ImageFreshness.STALE
115
+
116
+ async def pull_if_stale(
117
+ self, image: str, force_pull: bool = False
118
+ ) -> ImageFreshness:
119
+ """
120
+ The single site that decides whether a container start pulls first.
121
+
122
+ BOTH container-start paths call this — ``ContainerManager.run_container``
123
+ (zipapp) and ``ImageExecutor.run_image`` (native entrypoint). That is the
124
+ invariant, and it is the whole point: a start path that decides for
125
+ itself can silently stop deciding, because nothing about one path's
126
+ behaviour is asserted by the other's tests (#457, #306).
127
+
128
+ ``STALE`` and ``UNKNOWN`` are deliberately NOT treated alike. Known-wrong
129
+ bytes are refused loudly: the pull runs and ``pull_image_or_raise``
130
+ propagates its failure, so the launch dies rather than running an image
131
+ we have established is out of date. An unanswered question proceeds on
132
+ the local copy and says so, because refusing there would turn any
133
+ registry outage into a fleet-wide stoppage — and ``image_freshness``
134
+ commits to never raising out of task launch.
135
+
136
+ Parameters
137
+ ----------
138
+ image : str
139
+ Image reference the container will run
140
+ force_pull : bool
141
+ Pull unconditionally, skipping the comparison entirely — including
142
+ the registry round trip that comparison costs
143
+
144
+ Returns
145
+ -------
146
+ ImageFreshness
147
+ What is known about the bytes the container will run: ``CURRENT``
148
+ when they are the registry's (compared equal, just pulled, or a
149
+ local-only image with nothing to compare), ``UNKNOWN`` when the
150
+ registry could not be asked. ``STALE`` is never returned — a stale
151
+ image is pulled, and a pull that fails raises instead of returning.
152
+ """
153
+ if force_pull:
154
+ # Ahead of the comparison, not merged into it: the comparison is a
155
+ # registry round trip, and evaluating it here would make the
156
+ # documented bypass the slowest path in this function.
157
+ self.tracer.info(f"Pulling image {image}...")
158
+ await self.pull_image_or_raise(image)
159
+ return ImageFreshness.CURRENT
160
+
161
+ freshness = await self.image_freshness(image)
162
+ if freshness is ImageFreshness.STALE:
163
+ self.tracer.info(f"Pulling image {image}...")
164
+ await self.pull_image_or_raise(image)
165
+ return ImageFreshness.CURRENT
166
+ if freshness is ImageFreshness.UNKNOWN:
167
+ self.tracer.warning(
168
+ f"Starting {image} from the local copy with its freshness "
169
+ f"unverified: the registry digest could not be read, so these "
170
+ f"bytes are unconfirmed rather than known-current."
171
+ )
172
+ return freshness
85
173
 
86
174
  async def run_or_raise(self, config: dict) -> DockerContainer:
87
175
  """
@@ -147,7 +235,7 @@ class DockerRaiser:
147
235
  Pulls via the sync docker-py client rather than the async aiodocker
148
236
  client. docker-py auto-resolves registry credentials from
149
237
  ``~/.docker/config.json`` (including credHelpers/credsStore) — the
150
- same client ``image_needs_pull`` already uses for
238
+ same client ``image_freshness`` already uses for
151
239
  ``inspect_distribution`` — whereas aiodocker pulls anonymously and
152
240
  404s on private images with "pull access denied ... may require
153
241
  'docker login'" (refs #348).
@@ -166,9 +166,7 @@ class ImageExecutor(DockerRaiser):
166
166
  metadata=self.tracer.metadata,
167
167
  )
168
168
 
169
- if force_pull or await self.image_needs_pull(image):
170
- self.tracer.info(f"Pulling image {image}...")
171
- await self.pull_image_or_raise(image)
169
+ await self.pull_if_stale(image, force_pull=force_pull)
172
170
 
173
171
  # Copy (not alias) gpu_config's HostConfig — it is shared, long-lived
174
172
  # state on self; mutating it in place would leak NetworkMode/ExtraHosts
@@ -239,9 +239,8 @@ class ContainerManager(DockerRaiser):
239
239
  for key, value in config.items():
240
240
  self.tracer.debug(f" {key}: {value}")
241
241
 
242
- if force_pull:
243
- self.tracer.info(f"Pulling image {image}...")
244
- await self.pull_image_or_raise(image)
242
+ # Must resolve the image before the container is created, never after.
243
+ await self.pull_if_stale(image, force_pull=force_pull)
245
244
 
246
245
  self.containers[(swarm_id, task_id)] = await self.run_or_raise(config)
247
246
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: manta_node
3
- Version: 0.6b0.dev607
3
+ Version: 0.6b0.dev615
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
@@ -24,11 +24,11 @@ manta_node/infrastructure/__init__.py,sha256=yM60jKhdaD18BOsLb-0aR9D2XxGvrTnV92o
24
24
  manta_node/infrastructure/executor.py,sha256=wbH5-G4Y3iNTPr8ROWRDT-bWD-CYl1YFlZQ8dd9tpW4,2636
25
25
  manta_node/infrastructure/config/__init__.py,sha256=VnKidPAVfzlZMb5WM-MggtsDLM8tdAFFWlB7WSiawzc,617
26
26
  manta_node/infrastructure/config/node_config_manager.py,sha256=Js6PISjR_JRXMhgFC7lzI6StjW5paFhdhmtteao9j5A,35228
27
- manta_node/infrastructure/container/__init__.py,sha256=EO4SndUwmXrHczM4qkleUbxC7lTGz3c3Hd3iGrPg6D8,262
28
- manta_node/infrastructure/container/docker_adapter.py,sha256=DCCcKN7ahQK7rEX1gjzRH3lR2mi9YeEi7dFfNvSaF28,11990
27
+ manta_node/infrastructure/container/__init__.py,sha256=ACDyzGvtM7nfTsBXDAYFjkKgU2yu4Umj3CQ_rlRfw1I,300
28
+ manta_node/infrastructure/container/docker_adapter.py,sha256=Uxd3cNZMPyu7p68fq5QWvkF_vju2SjEBCqsvcnWYI-8,16368
29
29
  manta_node/infrastructure/container/executor.py,sha256=0aTFXDv-oXz81qwGi6zl_4qtkFriPWzn7iiiTRg7KJM,7274
30
- manta_node/infrastructure/container/image_executor.py,sha256=TnukiJWEtEcutqo45rYWkLH4fPXBzPnHMxcwCP7TOBs,10421
31
- manta_node/infrastructure/container/manager.py,sha256=VA-j6jO74Y53uu7Gvy8ibWSI-8GclwZ3dRCKTEA-v7A,15779
30
+ manta_node/infrastructure/container/image_executor.py,sha256=noNwde03hw9k3EnK1JuQGtByUf-XDHqx623X0DKgHZ0,10315
31
+ manta_node/infrastructure/container/manager.py,sha256=kylDl-5VZk01tzSXVrWI9VPn8mqi6tV-7Rf1tkMgee0,15790
32
32
  manta_node/infrastructure/filesystem/__init__.py,sha256=nf6Fakp3wuFKjSAm_oihI6WE_yyUY6Uelr7A1GwSvXs,145
33
33
  manta_node/infrastructure/filesystem/dataset_manager.py,sha256=sW7vPKYBLjwbtju1yzFEob9GlY1v0VI-xu6gxF9KM90,15075
34
34
  manta_node/infrastructure/grpc/__init__.py,sha256=6I-yzQM42gdBfcZ1FPVEKF2-pgJMdW-UssDHgz2EFcc,254
@@ -50,9 +50,9 @@ manta_node/infrastructure/process/executor.py,sha256=Ab1XvNuelEGCD5KdZLgYYnpjYZ5
50
50
  manta_node/infrastructure/security/__init__.py,sha256=lI6vkbUmfmt0m363iO4HzuzVNoQAc7Clp2R5vAhZbgE,64
51
51
  manta_node/infrastructure/security/auth_agent.py,sha256=WkPTg_g0gix7xomb_on_Ft3TNX0FpeqS7gwgJW9mTvk,11037
52
52
  manta_node/infrastructure/security/token_provider.py,sha256=u3bcbQYNBskFzunO-wOSZpGkDN_CU1BCB5HUbZTC4Vg,1832
53
- manta_node-0.6b0.dev607.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
54
- manta_node-0.6b0.dev607.dist-info/METADATA,sha256=z0twRxUnPrpGmoilw-uIUvYmgNc3UxRKAxZ33Dxq6TA,4213
55
- manta_node-0.6b0.dev607.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
56
- manta_node-0.6b0.dev607.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
57
- manta_node-0.6b0.dev607.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
58
- manta_node-0.6b0.dev607.dist-info/RECORD,,
53
+ manta_node-0.6b0.dev615.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
54
+ manta_node-0.6b0.dev615.dist-info/METADATA,sha256=5GbkeVrFKSatf7YuxFnn-QqLT2KEE3htAxlFv5jx0Hc,4213
55
+ manta_node-0.6b0.dev615.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
56
+ manta_node-0.6b0.dev615.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
57
+ manta_node-0.6b0.dev615.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
58
+ manta_node-0.6b0.dev615.dist-info/RECORD,,