manta-node 0.6b0.dev585__py3-none-any.whl → 0.6b0.dev587__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.
@@ -21,6 +21,68 @@ from ...node_orchestrator import Node, NodeFatalError
21
21
 
22
22
  console = Console()
23
23
 
24
+ _DEFAULT_LOG_MAX_BYTES = 100 * 1024 * 1024
25
+
26
+ _SIZE_SUFFIXES = {"K": 1024, "M": 1024**2, "G": 1024**3}
27
+
28
+
29
+ def _log_file_max_bytes(declared: str) -> int:
30
+ """Turn a ``LoggingConfig.max_file_size`` string into a byte count.
31
+
32
+ Accepts a bare number of bytes or a K/M/G suffix. An unparsable value falls
33
+ back to the default rather than raising: this runs on the start path, and
34
+ refusing to start a node over a typo in a log setting would turn a cosmetic
35
+ config error into an outage.
36
+ """
37
+ text = str(declared).strip().upper().removesuffix("B")
38
+ multiplier = _SIZE_SUFFIXES.get(text[-1:], 1)
39
+ if multiplier != 1:
40
+ text = text[:-1]
41
+ try:
42
+ return int(float(text) * multiplier)
43
+ except ValueError:
44
+ return _DEFAULT_LOG_MAX_BYTES
45
+
46
+
47
+ def _stdout_file_is_held(stdout_file: Path) -> bool:
48
+ """True when a still-running recorded instance has this path open."""
49
+ instance_dir = Path.home() / ".manta" / "nodes" / "instances"
50
+ target = str(stdout_file)
51
+ try:
52
+ recorded = sorted(instance_dir.glob("*.json"))
53
+ except OSError:
54
+ # Unknown counts as held, and the asymmetry is deliberate: a roll
55
+ # skipped under a dead writer costs disk, one performed under a live
56
+ # writer costs that writer's output.
57
+ return True
58
+
59
+ for instance_file in recorded:
60
+ try:
61
+ data = json.loads(instance_file.read_text())
62
+ except (OSError, ValueError):
63
+ continue
64
+ # `isinstance` and not `.get`: a JSON list or bare string parses fine and
65
+ # has no `.get`, and the AttributeError would escape both this function
66
+ # and the caller's `except OSError` to land in `start_node_background`'s
67
+ # `except Exception` -- a node that will not start because a file in an
68
+ # unrelated directory is malformed.
69
+ if not isinstance(data, dict) or data.get("stdout_file") != target:
70
+ continue
71
+ pid = data.get("pid")
72
+ # `pid > 0` is load-bearing, not a range check: os.kill(0, ...) addresses
73
+ # the caller's whole process group and os.kill(-1, ...) every process it
74
+ # may signal. This pid is read from a file on disk, so it is untrusted.
75
+ if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
76
+ continue
77
+ try:
78
+ os.kill(pid, 0)
79
+ except ProcessLookupError:
80
+ continue
81
+ except OSError:
82
+ pass # Alive, but not ours to signal (EPERM). Still a writer.
83
+ return True
84
+ return False
85
+
24
86
 
25
87
  def print_error(message: str):
26
88
  """Print error message in red."""
@@ -172,22 +234,42 @@ def start_node_foreground(
172
234
  },
173
235
  },
174
236
  "handlers": {
237
+ # NEVER give this handler `mode: "w"`, and never swap it back to
238
+ # a plain FileHandler. This file is the only untruncated account
239
+ # of a crash, and it is opened by the restart the crash causes:
240
+ # truncating on open destroys the evidence of the very incident
241
+ # a reader came for. Rotation is what keeps that append bounded.
175
242
  "file": {
176
- "class": "logging.FileHandler",
243
+ "class": "logging.handlers.RotatingFileHandler",
177
244
  "formatter": "standard",
178
245
  "filename": log_config.filename,
179
246
  "filters": ["manta_filter"],
180
- "mode": "w",
181
- },
182
- "rich": {
183
- "class": "rich.logging.RichHandler",
184
- "formatter": "standard",
185
- "markup": True,
186
- "filters": ["manta_filter"],
187
- "console": Console(
188
- _environ={"COLUMNS": "284"},
189
- ),
247
+ "maxBytes": _log_file_max_bytes(log_config.max_file_size),
248
+ "backupCount": log_config.backup_count,
190
249
  },
250
+ # Rich elides each line to the console width as it renders, so
251
+ # what systemd captures is already cut -- `journalctl` cannot
252
+ # restore characters that were never written. Under a unit there
253
+ # is no tty, and the journal is then the only surviving record,
254
+ # so it gets a plain handler that formats the message whole.
255
+ # Rich stays for an operator running the node interactively.
256
+ "console": (
257
+ {
258
+ "class": "rich.logging.RichHandler",
259
+ "formatter": "standard",
260
+ "markup": True,
261
+ "filters": ["manta_filter"],
262
+ "console": Console(
263
+ _environ={"COLUMNS": "284"},
264
+ ),
265
+ }
266
+ if sys.stdout.isatty()
267
+ else {
268
+ "class": "logging.StreamHandler",
269
+ "formatter": "standard",
270
+ "filters": ["manta_filter"],
271
+ }
272
+ ),
191
273
  },
192
274
  "root": {"level": log_config.level, "handlers": []},
193
275
  }
@@ -196,7 +278,7 @@ def start_node_foreground(
196
278
  if log_config.log_to_file:
197
279
  config_dict["root"]["handlers"].append("file")
198
280
  if log_config.log_to_console:
199
- config_dict["root"]["handlers"].append("rich")
281
+ config_dict["root"]["handlers"].append("console")
200
282
 
201
283
  # Call configure_logging with the config dictionary
202
284
  configure_logging(config=config_dict, debug=log_config.debug_mode)
@@ -304,8 +386,35 @@ def start_node_background(
304
386
  # Ensure log directory exists
305
387
  log_file.parent.mkdir(parents=True, exist_ok=True)
306
388
 
307
- # Start subprocess
308
- with open(log_file, "w") as log:
389
+ # One writer per path, and NEVER `log_file`: the child rotates that
390
+ # path, and a rename does not retarget an fd already open here, so this
391
+ # stream would follow the orphaned inode down the .1/.2 chain and be
392
+ # deleted at backupCount -- taking with it any traceback written before
393
+ # logging is configured.
394
+ # The suffix must stay off `.log`: logs.py globs `*.log` beside this
395
+ # file and keys by stem, so a `.log` sibling appears as a second node
396
+ # and defeats the single-instance auto-select.
397
+ stdout_file = log_file.with_suffix(".stdout")
398
+
399
+ # Bounds an append that never truncates. One previous run is kept: it is
400
+ # what explains the restart that replaced it.
401
+ # Rolled BEFORE the fd is opened, and never while another instance holds
402
+ # it. A rename does not retarget an already-open fd, so either mistake
403
+ # leaves a writer appending to an inode the next roll unlinks.
404
+ max_bytes = _log_file_max_bytes(config_manager.config.logging.max_file_size)
405
+ try:
406
+ if stdout_file.stat().st_size >= max_bytes and not _stdout_file_is_held(
407
+ stdout_file
408
+ ):
409
+ stdout_file.replace(stdout_file.with_name(f"{stdout_file.name}.1"))
410
+ except OSError:
411
+ # Housekeeping on a log file, running inside the caller's `except
412
+ # Exception`: anything escaping here becomes a node that will not
413
+ # start. Failing to roll costs disk; failing to start costs the node.
414
+ pass
415
+
416
+ # Append, never "w": a restart must not erase the run that prompted it.
417
+ with open(stdout_file, "a") as log:
309
418
  proc = subprocess.Popen(
310
419
  cmd,
311
420
  stdout=log,
@@ -337,6 +446,7 @@ def start_node_background(
337
446
  "manager_host": config_manager.config.network.manager_host,
338
447
  "manager_port": config_manager.config.network.manager_port,
339
448
  "log_file": str(log_file),
449
+ "stdout_file": str(stdout_file),
340
450
  }
341
451
 
342
452
  with open(instance_file, "w") as f:
@@ -344,7 +454,11 @@ def start_node_background(
344
454
 
345
455
  return 0
346
456
  else:
457
+ # stdout_file first: a node that died this early never reached
458
+ # configure_logging, so log_file holds nothing about it.
347
459
  print_error("Node failed to start. Check logs for details.")
460
+ if stdout_file.exists():
461
+ console.print(f"Startup output: {stdout_file}")
348
462
  if log_file.exists():
349
463
  console.print(f"Log file: {log_file}")
350
464
  return 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: manta_node
3
- Version: 0.6b0.dev585
3
+ Version: 0.6b0.dev587
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
@@ -13,7 +13,7 @@ manta_node/cli/commands/cluster.py,sha256=kUVopCN4xkEk-IR1aKpr7bfBSQEtG6pEj_hlWn
13
13
  manta_node/cli/commands/config.py,sha256=BiarCD0S8rcNYbio4RLzDvUS7A50s5dd5F4TiZqLR6M,16577
14
14
  manta_node/cli/commands/logs.py,sha256=cXOSdwhDIJAiKMmJYdzrnpapIxudxVdMzYT-KAWDKVk,5259
15
15
  manta_node/cli/commands/network.py,sha256=QqncD6vbNlg65ptQdFYexEUMALQm8UwcftbW3sfQrjw,42070
16
- manta_node/cli/commands/start.py,sha256=8qpOTv4LsCAMaygmPyhC9ZqlH8rkFPko-u2bVYTvyIo,18771
16
+ manta_node/cli/commands/start.py,sha256=5l6AG3nBvJqu3TrqtBSBAHVR71o1YWAlUHWiYOL2Bos,24547
17
17
  manta_node/cli/commands/status.py,sha256=FhlJuFxIRzxv9y8jUJRuWaCLfDBsGBqv6DCHUFQta3A,6401
18
18
  manta_node/cli/commands/stop.py,sha256=8on7NqqaBeHRcbrZ5pGJ3Ms_6E_HNcF22mZ908JVu4A,7695
19
19
  manta_node/domain/__init__.py,sha256=uyJXDf0QgfGCsXLMggqKCB5QV9oneYJ4iM-REaJGXDM,217
@@ -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.dev585.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
54
- manta_node-0.6b0.dev585.dist-info/METADATA,sha256=ondVpeyOG7n76C7roKaiYN_oeysKmokIxrKJI8Lolhg,4213
55
- manta_node-0.6b0.dev585.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
56
- manta_node-0.6b0.dev585.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
57
- manta_node-0.6b0.dev585.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
58
- manta_node-0.6b0.dev585.dist-info/RECORD,,
53
+ manta_node-0.6b0.dev587.dist-info/licenses/LICENSE,sha256=9mBgLhiB9goQPQjg9XQJ6FjgVxYw9Q8lyOZYkPGwF_c,34484
54
+ manta_node-0.6b0.dev587.dist-info/METADATA,sha256=UmduRXnFC2bzwzzQa06NsSBDwAo6gdrilYwV2WnEcHE,4213
55
+ manta_node-0.6b0.dev587.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
56
+ manta_node-0.6b0.dev587.dist-info/entry_points.txt,sha256=C_zcw6mdmztNxVUZGLZ83UH5TwcSuqb2C-rB4e2CQoc,56
57
+ manta_node-0.6b0.dev587.dist-info/top_level.txt,sha256=nbT5J5WETuXM9UPoQckl29uUOq8wMYdRb9QDx5kAFeo,11
58
+ manta_node-0.6b0.dev587.dist-info/RECORD,,