manta-node 0.6b5.dev705__py3-none-any.whl → 0.6b5.dev706__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.24.d60eab69"
7
+ __version__ = "2026.09.24.8d51d71c"
8
8
  __build_date__ = "2026.09.24"
9
- __commit_id__ = "d60eab69"
9
+ __commit_id__ = "8d51d71c"
10
10
  __package_version__ = "0.6b5"
@@ -28,6 +28,11 @@ from .bounds import Bounds
28
28
  from .inflight import InFlight, clear_inflight, deadline_from_now, write_inflight
29
29
  from .switch import VersionTreeError
30
30
 
31
+ # The free-space floor to stage a version dir. A version dir holds a full
32
+ # dependency closure, so the check refuses well before the disk is full rather
33
+ # than half-staging (NF-12). Operator-overridable via `run_switch`'s argument.
34
+ MIN_FREE_BYTES = 500 * 1024 * 1024
35
+
31
36
 
32
37
  class LifecycleError(Exception):
33
38
  """A lifecycle step failed; the node stays on its current version (NF-3)."""
@@ -88,6 +93,7 @@ def run_switch(
88
93
  is_back: ReturnProbe,
89
94
  bounds: Bounds | None = None,
90
95
  now: Callable[[], float] = time.time,
96
+ min_free_bytes: int = MIN_FREE_BYTES,
91
97
  ) -> SwitchOutcome:
92
98
  """Run the lifecycle for one request and return its terminal outcome.
93
99
 
@@ -121,6 +127,16 @@ def run_switch(
121
127
  if not _within(bounds.verify, lambda: verify(candidate_dir, candidate_python)):
122
128
  return _outcome(request, SeamState.FAILED, "verification failed (no switch)")
123
129
 
130
+ # FREE SPACE — refuse before staging when there is no room for a full version
131
+ # dir (NF-12, §6.2). Refusing here, before any write, is what stops a
132
+ # half-staged tree: the node stays on its current version.
133
+ if not version_tree.has_room_for(min_free_bytes):
134
+ return _outcome(
135
+ request,
136
+ SeamState.REFUSED,
137
+ "insufficient free space to stage a version dir",
138
+ )
139
+
124
140
  # STAGE — materialise the candidate's version dir (idempotent), bounded.
125
141
  staged: dict = {}
126
142
 
@@ -161,6 +177,13 @@ def run_switch(
161
177
 
162
178
  if is_back(record.deadline_epoch):
163
179
  clear_inflight()
180
+ # Reclaim on a SUCCESSFUL switch (NF-12/B-34): the tree stays bounded, and
181
+ # the current and last-good versions are never removed. No separate
182
+ # sweeper: reclamation rides the switch that made a version superseded.
183
+ protect = {target}
184
+ if last_good_dir is not None:
185
+ protect.add(last_good_dir)
186
+ version_tree.reclaim(bounds.retention_dirs, protect)
164
187
  return _outcome(
165
188
  request,
166
189
  SeamState.REPORTING,
@@ -22,6 +22,8 @@ the token, config or datasets.
22
22
  from __future__ import annotations
23
23
 
24
24
  import os
25
+ import shutil
26
+ import sys
25
27
  from dataclasses import dataclass
26
28
  from datetime import datetime, timezone
27
29
  from importlib.metadata import PackageNotFoundError, version
@@ -199,3 +201,72 @@ def is_pointer_target(target: Path) -> bool:
199
201
  """True when ``current`` resolves to ``target``."""
200
202
  link = current_link()
201
203
  return link.is_symlink() and Path(os.path.realpath(link)) == target
204
+
205
+
206
+ def list_versions() -> list[Path]:
207
+ """The version dirs, oldest first.
208
+
209
+ Ordered by the manifest's ``created`` (a dir with no readable manifest sorts
210
+ first, so a corrupt one is reclaimed before a good one). Used by retention to
211
+ pick what to reclaim.
212
+ """
213
+ root = versions_dir()
214
+ if not root.is_dir():
215
+ return []
216
+
217
+ def created(p: Path) -> str:
218
+ manifest = read_manifest(p)
219
+ return manifest.created if manifest else ""
220
+
221
+ dirs = [p for p in root.iterdir() if p.is_dir()]
222
+ return sorted(dirs, key=created)
223
+
224
+
225
+ def reclaim(retention_dirs: int, protect: set[Path]) -> list[Path]:
226
+ """Remove the oldest versions beyond ``retention_dirs``, never a protected one.
227
+
228
+ NF-12/B-34: the version tree stays bounded, but the **current** and last-good
229
+ versions are never removed — ``protect`` names them. Returns the removed dirs.
230
+ Called on a successful switch, so there is no separate sweeper to start.
231
+ """
232
+ versions = list_versions()
233
+ protected = {os.path.realpath(p) for p in protect}
234
+ # Oldest first; drop the front until the count is within the bound, skipping
235
+ # a protected dir (do not count it as removable, but it still occupies a slot).
236
+ removed: list[Path] = []
237
+ keep = max(retention_dirs, 2) # current + last good is the floor
238
+ for candidate in versions:
239
+ if len(versions) - len(removed) <= keep:
240
+ break
241
+ if os.path.realpath(candidate) in protected:
242
+ continue
243
+ try:
244
+ for child in candidate.iterdir():
245
+ if child.is_dir():
246
+ shutil.rmtree(child)
247
+ else:
248
+ child.unlink()
249
+ candidate.rmdir()
250
+ removed.append(candidate)
251
+ except OSError as exc:
252
+ # A dir we cannot remove stays: retention is best-effort on a live
253
+ # node, and failing the switch over a stale dir would be worse.
254
+ print(f"retention: could not reclaim {candidate}: {exc}", file=sys.stderr)
255
+ return removed
256
+
257
+
258
+ def has_room_for(min_bytes: int) -> bool:
259
+ """True when the version tree's filesystem has at least ``min_bytes`` free.
260
+
261
+ The free-space refusal (NF-12, mechanism §6.2): a node with no room to stage
262
+ a full version dir refuses the update rather than half-staging. The caller
263
+ passes the candidate's estimated size.
264
+ """
265
+ root = versions_dir()
266
+ # Check the nearest existing ancestor: the tree may not exist yet on a fresh
267
+ # node, and the capacity is the filesystem's either way.
268
+ probe = root
269
+ while not probe.exists() and probe != probe.parent:
270
+ probe = probe.parent
271
+ free = shutil.disk_usage(probe).free
272
+ return free >= min_bytes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: manta_node
3
- Version: 0.6b5.dev705
3
+ Version: 0.6b5.dev706
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
@@ -6,7 +6,7 @@ manta_node/task_manager.py,sha256=xRE-yKdUW0FFR8vtX1jDwl1AxTbqVD_JS7QVR62GA00,38
6
6
  manta_node/tasks.py,sha256=o837Hi-KFVJg9LF5iOtE8oD6UuTOfrEPWR-JOdhRGeY,6983
7
7
  manta_node/utils.py,sha256=Hw4Iw2jYQYsfVLVJuBMt-_rMFBeml3nCDE2jiiXc8YM,1155
8
8
  manta_node/cli/__init__.py,sha256=arzO3mXDJ-E2yeeytbZfN7mCswwxdQ43_QjyeL2E5yE,134
9
- manta_node/cli/_version_generated.py,sha256=Avtgxf4CTYxrwjUmb8TM92DAZRNRqrzV-eNId81M60o,275
9
+ manta_node/cli/_version_generated.py,sha256=rTGRherNt_ty7j_gNSOWg1VAt6h2GPc4Yfxo9cEWRio,275
10
10
  manta_node/cli/config_manager.py,sha256=BCju9zqzraviL3KrzRnB-vZQUY__Q5kuEqb7EdB8YdI,4409
11
11
  manta_node/cli/main.py,sha256=dZa6zrypWBvpN8hd4qpxevvlyNFhxKINCg1537NJ36w,4318
12
12
  manta_node/cli/version.py,sha256=3lrnIWpOosszBN-VsCBVLxDgpvCC7IB0aDIKx_3e7hI,3115
@@ -56,14 +56,14 @@ manta_node/supervisor/__init__.py,sha256=kgdoNce1tM4e7jvdxkdmnNJV1L92-NI2cRfYSdm
56
56
  manta_node/supervisor/bounds.py,sha256=MlUfj0tYY-US348JsxYA-Ph5pSpgoOPlRdVTwmrPtZA,3181
57
57
  manta_node/supervisor/executor.py,sha256=a0JafVoTmtbIu87nwUtj08KAC-Q-u0Ajdn_ptyu9yoc,6833
58
58
  manta_node/supervisor/inflight.py,sha256=JOqwXYM928fpcQtYdoK7596eAg28KM70bh85cyQgX3Q,2663
59
- manta_node/supervisor/lifecycle.py,sha256=JE74H0ODt5_y997kliiVDiQAf1laYptNjkrnnfGdgK0,7411
59
+ manta_node/supervisor/lifecycle.py,sha256=GDy7d7MHxhabvTRoIkAL-_n0oja-GpmVWYAzOehP_DI,8551
60
60
  manta_node/supervisor/process.py,sha256=mWd2C4-S8NpupcLp0YbBEYmqQUQbrfMAZi5TF9UQ_cM,17504
61
61
  manta_node/supervisor/records.py,sha256=fUlL1SfmWpGnCbJ9956cuy0H3a7-aEF-B-naqbJiYoU,6405
62
62
  manta_node/supervisor/seam.py,sha256=HomMFTDbc0dZQ9VrfGVhQxjiU7E23-zwyjZnLsnq6dc,8031
63
- manta_node/supervisor/switch.py,sha256=BUSvMdWyUfZmeEVSBmQ7wLNRD7troPxZ3pdg8tOGXo4,7433
64
- manta_node-0.6b5.dev705.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
65
- manta_node-0.6b5.dev705.dist-info/METADATA,sha256=ajleN4PRTTgRVihXNc2WTZEatE_cgnmro32xQBBd1JY,4231
66
- manta_node-0.6b5.dev705.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
67
- manta_node-0.6b5.dev705.dist-info/entry_points.txt,sha256=uCwggRApalLEsYs1YxwTBN0-e9oMJAkMoBp01HaSdoY,110
68
- manta_node-0.6b5.dev705.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
69
- manta_node-0.6b5.dev705.dist-info/RECORD,,
63
+ manta_node/supervisor/switch.py,sha256=HQcNULUf6JafDGoKIh1mrw-4m9pxvz1hMLUPnwkY6mc,10173
64
+ manta_node-0.6b5.dev706.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
65
+ manta_node-0.6b5.dev706.dist-info/METADATA,sha256=uSttKoedT1orqAsgzeEQzdiwXS8i8HW1Jdv2ni9q3rE,4231
66
+ manta_node-0.6b5.dev706.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
67
+ manta_node-0.6b5.dev706.dist-info/entry_points.txt,sha256=uCwggRApalLEsYs1YxwTBN0-e9oMJAkMoBp01HaSdoY,110
68
+ manta_node-0.6b5.dev706.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
69
+ manta_node-0.6b5.dev706.dist-info/RECORD,,