furu 0.0.4__py3-none-any.whl → 0.0.5__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.
- furu/config.py +27 -40
- furu/core/furu.py +194 -126
- furu/core/list.py +3 -2
- furu/dashboard/frontend/dist/assets/{index-DS3FsqcY.js → index-BjyrY-Zz.js} +1 -1
- furu/dashboard/frontend/dist/index.html +1 -1
- furu/execution/local.py +9 -7
- furu/execution/plan.py +117 -25
- furu/execution/slurm_dag.py +16 -14
- furu/execution/slurm_pool.py +5 -5
- furu/execution/slurm_spec.py +2 -2
- furu/migration.py +1 -2
- furu/runtime/env.py +1 -1
- furu/runtime/logging.py +30 -4
- furu/storage/metadata.py +25 -29
- furu/storage/migration.py +0 -1
- furu/storage/state.py +86 -92
- {furu-0.0.4.dist-info → furu-0.0.5.dist-info}/METADATA +18 -6
- {furu-0.0.4.dist-info → furu-0.0.5.dist-info}/RECORD +20 -20
- {furu-0.0.4.dist-info → furu-0.0.5.dist-info}/WHEEL +1 -1
- {furu-0.0.4.dist-info → furu-0.0.5.dist-info}/entry_points.txt +0 -0
furu/core/furu.py
CHANGED
|
@@ -9,6 +9,7 @@ import threading
|
|
|
9
9
|
import time
|
|
10
10
|
import traceback
|
|
11
11
|
from abc import ABC, abstractmethod
|
|
12
|
+
from functools import cached_property
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from types import FrameType
|
|
14
15
|
from typing import (
|
|
@@ -21,17 +22,16 @@ from typing import (
|
|
|
21
22
|
Protocol,
|
|
22
23
|
Self,
|
|
23
24
|
Sequence,
|
|
24
|
-
TypedDict,
|
|
25
25
|
TypeAlias,
|
|
26
|
+
TypedDict,
|
|
26
27
|
TypeVar,
|
|
27
28
|
cast,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
import chz
|
|
31
32
|
import submitit
|
|
32
|
-
from typing_extensions import dataclass_transform
|
|
33
|
-
|
|
34
33
|
from chz.field import Field as ChzField
|
|
34
|
+
from typing_extensions import dataclass_transform
|
|
35
35
|
|
|
36
36
|
from ..adapters import SubmititAdapter
|
|
37
37
|
from ..adapters.submitit import SubmititJob
|
|
@@ -63,7 +63,6 @@ from ..storage.state import (
|
|
|
63
63
|
_StateAttemptRunning,
|
|
64
64
|
_StateResultAbsent,
|
|
65
65
|
_StateResultFailed,
|
|
66
|
-
_StateResultMigrated,
|
|
67
66
|
_StateResultSuccess,
|
|
68
67
|
compute_lock,
|
|
69
68
|
)
|
|
@@ -207,7 +206,7 @@ class Furu[T](ABC):
|
|
|
207
206
|
|
|
208
207
|
def _get_dependencies(self: Self, *, recursive: bool = True) -> list["Furu"]:
|
|
209
208
|
"""Collect Furu dependencies from fields and `_dependencies()`."""
|
|
210
|
-
seen = {self.
|
|
209
|
+
seen = {self.furu_hash}
|
|
211
210
|
dependencies: list[Furu] = []
|
|
212
211
|
_collect_dependencies(self, dependencies, seen, recursive=recursive)
|
|
213
212
|
return dependencies
|
|
@@ -221,7 +220,7 @@ class Furu[T](ABC):
|
|
|
221
220
|
for dependency in dependencies:
|
|
222
221
|
if dependency is self:
|
|
223
222
|
raise ValueError("Furu dependencies cannot include self")
|
|
224
|
-
digests.add(dependency.
|
|
223
|
+
digests.add(dependency.furu_hash)
|
|
225
224
|
return sorted(digests)
|
|
226
225
|
|
|
227
226
|
def _invalidate_cached_success(self: Self, directory: Path, *, reason: str) -> None:
|
|
@@ -229,7 +228,7 @@ class Furu[T](ABC):
|
|
|
229
228
|
logger.warning(
|
|
230
229
|
"invalidate %s %s %s (%s)",
|
|
231
230
|
self.__class__.__name__,
|
|
232
|
-
self.
|
|
231
|
+
self.furu_hash,
|
|
233
232
|
directory,
|
|
234
233
|
reason,
|
|
235
234
|
)
|
|
@@ -262,14 +261,9 @@ class Furu[T](ABC):
|
|
|
262
261
|
if isinstance(state.result, _StateResultSuccess):
|
|
263
262
|
self._invalidate_cached_success(directory, reason="always_rerun enabled")
|
|
264
263
|
|
|
265
|
-
@
|
|
264
|
+
@cached_property
|
|
266
265
|
def furu_hash(self: Self) -> str:
|
|
267
266
|
"""Return the stable content hash for this Furu object."""
|
|
268
|
-
return self._furu_hash
|
|
269
|
-
|
|
270
|
-
@property
|
|
271
|
-
def _furu_hash(self: Self) -> str:
|
|
272
|
-
"""Compute hash of this object's content for storage identification."""
|
|
273
267
|
return FuruSerializer.compute_hash(self)
|
|
274
268
|
|
|
275
269
|
def _always_rerun(self: Self) -> bool:
|
|
@@ -282,15 +276,17 @@ class Furu[T](ABC):
|
|
|
282
276
|
|
|
283
277
|
def _base_furu_dir(self: Self) -> Path:
|
|
284
278
|
root = FURU_CONFIG.get_root(self.version_controlled)
|
|
285
|
-
return root / self.__class__._namespace() / self.
|
|
279
|
+
return root / self.__class__._namespace() / self.furu_hash
|
|
286
280
|
|
|
287
|
-
@
|
|
281
|
+
@cached_property
|
|
288
282
|
def furu_dir(self: Self) -> Path:
|
|
289
283
|
"""Get the directory for this Furu object."""
|
|
290
284
|
directory = self._base_furu_dir()
|
|
291
285
|
migration = self._alias_record(directory)
|
|
292
|
-
if migration is not None
|
|
293
|
-
|
|
286
|
+
if migration is not None:
|
|
287
|
+
target_dir = self._alias_target_dir(directory, migration)
|
|
288
|
+
if target_dir is not None:
|
|
289
|
+
return target_dir
|
|
294
290
|
return directory
|
|
295
291
|
|
|
296
292
|
@property
|
|
@@ -321,9 +317,8 @@ class Furu[T](ABC):
|
|
|
321
317
|
|
|
322
318
|
def _exists_quiet(self: Self) -> bool:
|
|
323
319
|
directory = self._base_furu_dir()
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if not isinstance(state.result, _StateResultSuccess):
|
|
320
|
+
success_dir = self._success_marker_dir(directory)
|
|
321
|
+
if success_dir is None:
|
|
327
322
|
return False
|
|
328
323
|
try:
|
|
329
324
|
return self._validate()
|
|
@@ -332,7 +327,7 @@ class Furu[T](ABC):
|
|
|
332
327
|
logger.warning(
|
|
333
328
|
"exists %s -> false (validate invalid for %s: %s)",
|
|
334
329
|
directory,
|
|
335
|
-
f"{self.__class__.__name__}({self.
|
|
330
|
+
f"{self.__class__.__name__}({self.furu_hash})",
|
|
336
331
|
exc,
|
|
337
332
|
)
|
|
338
333
|
return False
|
|
@@ -341,7 +336,7 @@ class Furu[T](ABC):
|
|
|
341
336
|
logger.exception(
|
|
342
337
|
"exists %s -> false (validate crashed for %s: %s)",
|
|
343
338
|
directory,
|
|
344
|
-
f"{self.__class__.__name__}({self.
|
|
339
|
+
f"{self.__class__.__name__}({self.furu_hash})",
|
|
345
340
|
exc,
|
|
346
341
|
)
|
|
347
342
|
return False
|
|
@@ -350,9 +345,8 @@ class Furu[T](ABC):
|
|
|
350
345
|
"""Check if result exists and is valid."""
|
|
351
346
|
logger = get_logger()
|
|
352
347
|
directory = self._base_furu_dir()
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if not isinstance(state.result, _StateResultSuccess):
|
|
348
|
+
success_dir = self._success_marker_dir(directory)
|
|
349
|
+
if success_dir is None:
|
|
356
350
|
logger.info("exists %s -> false", directory)
|
|
357
351
|
return False
|
|
358
352
|
|
|
@@ -382,72 +376,110 @@ class Furu[T](ABC):
|
|
|
382
376
|
Raises:
|
|
383
377
|
FuruComputeError: If computation fails with detailed error information
|
|
384
378
|
"""
|
|
385
|
-
from furu.execution.context import EXEC_CONTEXT
|
|
386
379
|
from furu.errors import (
|
|
387
380
|
FuruExecutionError,
|
|
388
381
|
FuruMissingArtifact,
|
|
389
382
|
FuruSpecMismatch,
|
|
390
383
|
)
|
|
384
|
+
from furu.execution.context import EXEC_CONTEXT
|
|
391
385
|
|
|
392
386
|
ctx = EXEC_CONTEXT.get()
|
|
393
387
|
if ctx.mode == "executor":
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
)
|
|
409
|
-
|
|
388
|
+
logger = get_logger()
|
|
389
|
+
parent_holder = current_holder()
|
|
390
|
+
has_parent = parent_holder is not None and parent_holder is not self
|
|
391
|
+
needs_holder = parent_holder is None or has_parent
|
|
392
|
+
caller_info: _CallerInfo = {}
|
|
393
|
+
if has_parent:
|
|
394
|
+
caller_info = self._get_caller_info()
|
|
395
|
+
|
|
396
|
+
def _executor_get() -> T:
|
|
397
|
+
directory = self._base_furu_dir()
|
|
398
|
+
if force:
|
|
399
|
+
if (
|
|
400
|
+
ctx.current_node_hash is None
|
|
401
|
+
or self.furu_hash != ctx.current_node_hash
|
|
402
|
+
):
|
|
403
|
+
raise FuruExecutionError(
|
|
404
|
+
"force=True not allowed: only the current node may compute in executor mode. "
|
|
405
|
+
f"current_node_hash={ctx.current_node_hash!r} "
|
|
406
|
+
f"obj={self.__class__.__name__}({self.furu_hash})",
|
|
407
|
+
hints=[
|
|
408
|
+
"Declare this object as a dependency instead of calling dep.get(force=True).",
|
|
409
|
+
"Inside executor mode, use get(force=True) only on the node being executed.",
|
|
410
|
+
],
|
|
411
|
+
)
|
|
412
|
+
self._prepare_executor_rerun(directory)
|
|
413
|
+
|
|
414
|
+
exists_ok = self._exists_quiet()
|
|
415
|
+
if exists_ok and not (force and self._always_rerun()):
|
|
416
|
+
return self._load()
|
|
410
417
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
418
|
+
if force and not exists_ok:
|
|
419
|
+
state = self.get_state(directory)
|
|
420
|
+
if isinstance(state.result, _StateResultSuccess):
|
|
421
|
+
self._invalidate_cached_success(
|
|
422
|
+
directory, reason="_validate returned false (executor)"
|
|
423
|
+
)
|
|
414
424
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
425
|
+
if not force:
|
|
426
|
+
raise FuruMissingArtifact(
|
|
427
|
+
"Missing artifact "
|
|
428
|
+
f"{self.__class__.__name__}({self.furu_hash}) in executor mode. "
|
|
429
|
+
f"Requested by {ctx.current_node_hash}. Declare it as a dependency."
|
|
420
430
|
)
|
|
421
431
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
432
|
+
required = self._executor_spec_key()
|
|
433
|
+
if ctx.spec_key is None or required != ctx.spec_key:
|
|
434
|
+
raise FuruSpecMismatch(
|
|
435
|
+
"force=True not allowed: "
|
|
436
|
+
f"required={required!r} != worker={ctx.spec_key!r} (v1 exact match)"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
StateManager.ensure_internal_dir(directory)
|
|
440
|
+
status, created_here, result = self._run_locally(
|
|
441
|
+
start_time=time.time(),
|
|
442
|
+
allow_failed=FURU_CONFIG.retry_failed,
|
|
443
|
+
executor_mode=True,
|
|
427
444
|
)
|
|
445
|
+
if status == "success":
|
|
446
|
+
if created_here:
|
|
447
|
+
return cast(T, result)
|
|
448
|
+
return self._load()
|
|
428
449
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"
|
|
433
|
-
f"required={required!r} != worker={ctx.spec_key!r} (v1 exact match)"
|
|
450
|
+
raise self._build_failed_state_error(
|
|
451
|
+
self._base_furu_dir(),
|
|
452
|
+
None,
|
|
453
|
+
message="Computation previously failed",
|
|
434
454
|
)
|
|
435
455
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return self._load()
|
|
456
|
+
if has_parent:
|
|
457
|
+
logger.debug(
|
|
458
|
+
"dep: begin %s %s %s",
|
|
459
|
+
self.__class__.__name__,
|
|
460
|
+
self.furu_hash,
|
|
461
|
+
self._base_furu_dir(),
|
|
462
|
+
extra=caller_info,
|
|
463
|
+
)
|
|
445
464
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
465
|
+
ok = False
|
|
466
|
+
try:
|
|
467
|
+
if needs_holder:
|
|
468
|
+
with enter_holder(self):
|
|
469
|
+
result = _executor_get()
|
|
470
|
+
else:
|
|
471
|
+
result = _executor_get()
|
|
472
|
+
ok = True
|
|
473
|
+
return result
|
|
474
|
+
finally:
|
|
475
|
+
if has_parent:
|
|
476
|
+
logger.debug(
|
|
477
|
+
"dep: end %s %s (%s)",
|
|
478
|
+
self.__class__.__name__,
|
|
479
|
+
self.furu_hash,
|
|
480
|
+
"ok" if ok else "error",
|
|
481
|
+
extra=caller_info,
|
|
482
|
+
)
|
|
451
483
|
|
|
452
484
|
return self._get_impl_interactive(force=force)
|
|
453
485
|
|
|
@@ -455,13 +487,15 @@ class Furu[T](ABC):
|
|
|
455
487
|
logger = get_logger()
|
|
456
488
|
parent_holder = current_holder()
|
|
457
489
|
has_parent = parent_holder is not None and parent_holder is not self
|
|
490
|
+
caller_info = self._get_caller_info()
|
|
458
491
|
retry_failed_effective = FURU_CONFIG.retry_failed
|
|
459
492
|
if has_parent:
|
|
460
493
|
logger.debug(
|
|
461
494
|
"dep: begin %s %s %s",
|
|
462
495
|
self.__class__.__name__,
|
|
463
|
-
self.
|
|
496
|
+
self.furu_hash,
|
|
464
497
|
self._base_furu_dir(),
|
|
498
|
+
extra=caller_info,
|
|
465
499
|
)
|
|
466
500
|
|
|
467
501
|
ok = False
|
|
@@ -469,19 +503,21 @@ class Furu[T](ABC):
|
|
|
469
503
|
with enter_holder(self):
|
|
470
504
|
start_time = time.time()
|
|
471
505
|
base_dir = self._base_furu_dir()
|
|
472
|
-
base_dir.mkdir(parents=True, exist_ok=True)
|
|
473
506
|
directory = base_dir
|
|
474
507
|
migration = self._alias_record(base_dir)
|
|
475
508
|
alias_active = False
|
|
509
|
+
base_marker = StateManager.success_marker_exists(base_dir)
|
|
476
510
|
|
|
477
511
|
if (
|
|
478
512
|
migration is not None
|
|
479
513
|
and migration.kind == "alias"
|
|
480
514
|
and migration.overwritten_at is None
|
|
515
|
+
and not base_marker
|
|
481
516
|
):
|
|
482
|
-
target_dir =
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
target_dir = self._alias_target_dir(
|
|
518
|
+
base_dir, migration, base_marker=base_marker
|
|
519
|
+
)
|
|
520
|
+
if target_dir is not None:
|
|
485
521
|
alias_active = True
|
|
486
522
|
directory = target_dir
|
|
487
523
|
else:
|
|
@@ -582,17 +618,25 @@ class Furu[T](ABC):
|
|
|
582
618
|
# Cache hits can be extremely noisy in pipelines; keep logs for state
|
|
583
619
|
# transitions (create/wait) and error cases, but suppress repeated
|
|
584
620
|
# "success->load" lines and the raw separator on successful loads.
|
|
585
|
-
self._log_console_start(
|
|
621
|
+
self._log_console_start(
|
|
622
|
+
action_color=action_color,
|
|
623
|
+
caller_info=caller_info,
|
|
624
|
+
)
|
|
586
625
|
|
|
587
626
|
if decision != "success->load":
|
|
627
|
+
if decision == "create":
|
|
628
|
+
StateManager.ensure_internal_dir(directory)
|
|
588
629
|
write_separator()
|
|
589
630
|
logger.debug(
|
|
590
631
|
"get %s %s %s (%s)",
|
|
591
632
|
self.__class__.__name__,
|
|
592
|
-
self.
|
|
633
|
+
self.furu_hash,
|
|
593
634
|
directory,
|
|
594
635
|
decision,
|
|
595
|
-
extra={
|
|
636
|
+
extra={
|
|
637
|
+
"furu_action_color": action_color,
|
|
638
|
+
**caller_info,
|
|
639
|
+
},
|
|
596
640
|
)
|
|
597
641
|
|
|
598
642
|
# Fast path: already successful
|
|
@@ -609,7 +653,7 @@ class Furu[T](ABC):
|
|
|
609
653
|
logger.error(
|
|
610
654
|
"get %s %s (load failed)",
|
|
611
655
|
self.__class__.__name__,
|
|
612
|
-
self.
|
|
656
|
+
self.furu_hash,
|
|
613
657
|
)
|
|
614
658
|
raise FuruComputeError(
|
|
615
659
|
f"Failed to load result from {directory}",
|
|
@@ -646,15 +690,14 @@ class Furu[T](ABC):
|
|
|
646
690
|
logger.debug(
|
|
647
691
|
"dep: end %s %s (%s)",
|
|
648
692
|
self.__class__.__name__,
|
|
649
|
-
self.
|
|
693
|
+
self.furu_hash,
|
|
650
694
|
"ok" if ok else "error",
|
|
695
|
+
extra=caller_info,
|
|
651
696
|
)
|
|
652
697
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
logger = get_logger()
|
|
698
|
+
@staticmethod
|
|
699
|
+
def _get_caller_info() -> _CallerInfo:
|
|
656
700
|
frame = sys._getframe(1)
|
|
657
|
-
|
|
658
701
|
caller_info: _CallerInfo = {}
|
|
659
702
|
if frame is not None:
|
|
660
703
|
# Walk up the stack to find the caller outside of furu package
|
|
@@ -669,11 +712,20 @@ class Furu[T](ABC):
|
|
|
669
712
|
}
|
|
670
713
|
break
|
|
671
714
|
frame = frame.f_back
|
|
715
|
+
return caller_info
|
|
716
|
+
|
|
717
|
+
def _log_console_start(
|
|
718
|
+
self, action_color: str, caller_info: _CallerInfo | None = None
|
|
719
|
+
) -> None:
|
|
720
|
+
"""Log the start of get to console with caller info."""
|
|
721
|
+
logger = get_logger()
|
|
722
|
+
if caller_info is None:
|
|
723
|
+
caller_info = self._get_caller_info()
|
|
672
724
|
|
|
673
725
|
logger.info(
|
|
674
726
|
"get %s %s",
|
|
675
727
|
self.__class__.__name__,
|
|
676
|
-
self.
|
|
728
|
+
self.furu_hash,
|
|
677
729
|
extra={
|
|
678
730
|
"furu_console_only": True,
|
|
679
731
|
"furu_action_color": action_color,
|
|
@@ -744,9 +796,11 @@ class Furu[T](ABC):
|
|
|
744
796
|
"""Return the alias-aware state for this Furu directory."""
|
|
745
797
|
base_dir = directory or self._base_furu_dir()
|
|
746
798
|
record = self._alias_record(base_dir)
|
|
747
|
-
if record is None
|
|
799
|
+
if record is None:
|
|
800
|
+
return StateManager.read_state(base_dir)
|
|
801
|
+
target_dir = self._alias_target_dir(base_dir, record)
|
|
802
|
+
if target_dir is None:
|
|
748
803
|
return StateManager.read_state(base_dir)
|
|
749
|
-
target_dir = MigrationManager.resolve_dir(record, target="from")
|
|
750
804
|
return StateManager.read_state(target_dir)
|
|
751
805
|
|
|
752
806
|
def _alias_record(self, directory: Path) -> MigrationRecord | None:
|
|
@@ -755,15 +809,36 @@ class Furu[T](ABC):
|
|
|
755
809
|
return None
|
|
756
810
|
return record
|
|
757
811
|
|
|
758
|
-
def
|
|
812
|
+
def _alias_target_dir(
|
|
813
|
+
self,
|
|
814
|
+
directory: Path,
|
|
815
|
+
record: MigrationRecord,
|
|
816
|
+
*,
|
|
817
|
+
base_marker: bool | None = None,
|
|
818
|
+
) -> Path | None:
|
|
759
819
|
if record.overwritten_at is not None:
|
|
760
|
-
return
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
820
|
+
return None
|
|
821
|
+
if base_marker is None:
|
|
822
|
+
base_marker = StateManager.success_marker_exists(directory)
|
|
823
|
+
if base_marker:
|
|
824
|
+
return None
|
|
764
825
|
target = MigrationManager.resolve_dir(record, target="from")
|
|
765
|
-
|
|
766
|
-
|
|
826
|
+
if StateManager.success_marker_exists(target):
|
|
827
|
+
return target
|
|
828
|
+
return None
|
|
829
|
+
|
|
830
|
+
def _success_marker_dir(self, directory: Path) -> Path | None:
|
|
831
|
+
base_marker = StateManager.success_marker_exists(directory)
|
|
832
|
+
record = self._alias_record(directory)
|
|
833
|
+
if record is None:
|
|
834
|
+
return directory if base_marker else None
|
|
835
|
+
target_dir = self._alias_target_dir(directory, record, base_marker=base_marker)
|
|
836
|
+
if target_dir is not None:
|
|
837
|
+
return target_dir
|
|
838
|
+
return directory if base_marker else None
|
|
839
|
+
|
|
840
|
+
def _alias_is_active(self, directory: Path, record: MigrationRecord) -> bool:
|
|
841
|
+
return self._alias_target_dir(directory, record) is not None
|
|
767
842
|
|
|
768
843
|
def _maybe_detach_alias(
|
|
769
844
|
self: Self,
|
|
@@ -804,6 +879,7 @@ class Furu[T](ABC):
|
|
|
804
879
|
) -> SubmititJob | None:
|
|
805
880
|
"""Submit job once without waiting (fire-and-forget mode)."""
|
|
806
881
|
logger = get_logger()
|
|
882
|
+
StateManager.ensure_internal_dir(directory)
|
|
807
883
|
self._reconcile(directory, adapter=adapter)
|
|
808
884
|
state = StateManager.read_state(directory)
|
|
809
885
|
attempt = state.attempt
|
|
@@ -824,7 +900,7 @@ class Furu[T](ABC):
|
|
|
824
900
|
logger.debug(
|
|
825
901
|
"submit: waiting for submit lock %s %s %s",
|
|
826
902
|
self.__class__.__name__,
|
|
827
|
-
self.
|
|
903
|
+
self.furu_hash,
|
|
828
904
|
directory,
|
|
829
905
|
)
|
|
830
906
|
time.sleep(0.5)
|
|
@@ -833,9 +909,7 @@ class Furu[T](ABC):
|
|
|
833
909
|
attempt_id: str | None = None
|
|
834
910
|
try:
|
|
835
911
|
# Create metadata
|
|
836
|
-
metadata = MetadataManager.create_metadata(
|
|
837
|
-
self, directory, ignore_diff=FURU_CONFIG.ignore_git_diff
|
|
838
|
-
)
|
|
912
|
+
metadata = MetadataManager.create_metadata(self, directory)
|
|
839
913
|
MetadataManager.write_metadata(metadata, directory)
|
|
840
914
|
|
|
841
915
|
env_info = MetadataManager.collect_environment_info()
|
|
@@ -919,12 +993,12 @@ class Furu[T](ABC):
|
|
|
919
993
|
mode="executor",
|
|
920
994
|
spec_key=self._executor_spec_key(),
|
|
921
995
|
backend="submitit",
|
|
922
|
-
current_node_hash=self.
|
|
996
|
+
current_node_hash=self.furu_hash,
|
|
923
997
|
)
|
|
924
998
|
)
|
|
925
999
|
try:
|
|
926
1000
|
directory = self._base_furu_dir()
|
|
927
|
-
|
|
1001
|
+
StateManager.ensure_internal_dir(directory)
|
|
928
1002
|
always_rerun = self._always_rerun()
|
|
929
1003
|
needs_success_invalidation = False
|
|
930
1004
|
if not always_rerun:
|
|
@@ -979,11 +1053,7 @@ class Furu[T](ABC):
|
|
|
979
1053
|
stage = "metadata"
|
|
980
1054
|
try:
|
|
981
1055
|
# Refresh metadata (now safe - attempt is already recorded)
|
|
982
|
-
metadata = MetadataManager.create_metadata(
|
|
983
|
-
self,
|
|
984
|
-
directory,
|
|
985
|
-
ignore_diff=FURU_CONFIG.ignore_git_diff,
|
|
986
|
-
)
|
|
1056
|
+
metadata = MetadataManager.create_metadata(self, directory)
|
|
987
1057
|
MetadataManager.write_metadata(metadata, directory)
|
|
988
1058
|
|
|
989
1059
|
# Set up signal handlers
|
|
@@ -999,14 +1069,14 @@ class Furu[T](ABC):
|
|
|
999
1069
|
logger.debug(
|
|
1000
1070
|
"_create: begin %s %s %s",
|
|
1001
1071
|
self.__class__.__name__,
|
|
1002
|
-
self.
|
|
1072
|
+
self.furu_hash,
|
|
1003
1073
|
directory,
|
|
1004
1074
|
)
|
|
1005
1075
|
self._create()
|
|
1006
1076
|
logger.debug(
|
|
1007
1077
|
"_create: ok %s %s %s",
|
|
1008
1078
|
self.__class__.__name__,
|
|
1009
|
-
self.
|
|
1079
|
+
self.furu_hash,
|
|
1010
1080
|
directory,
|
|
1011
1081
|
)
|
|
1012
1082
|
StateManager.write_success_marker(
|
|
@@ -1018,7 +1088,7 @@ class Furu[T](ABC):
|
|
|
1018
1088
|
logger.info(
|
|
1019
1089
|
"_create ok %s %s",
|
|
1020
1090
|
self.__class__.__name__,
|
|
1021
|
-
self.
|
|
1091
|
+
self.furu_hash,
|
|
1022
1092
|
extra={"furu_console_only": True},
|
|
1023
1093
|
)
|
|
1024
1094
|
except Exception as e:
|
|
@@ -1026,7 +1096,7 @@ class Furu[T](ABC):
|
|
|
1026
1096
|
logger.error(
|
|
1027
1097
|
"_create failed %s %s %s",
|
|
1028
1098
|
self.__class__.__name__,
|
|
1029
|
-
self.
|
|
1099
|
+
self.furu_hash,
|
|
1030
1100
|
directory,
|
|
1031
1101
|
extra={"furu_file_only": True},
|
|
1032
1102
|
)
|
|
@@ -1035,7 +1105,7 @@ class Furu[T](ABC):
|
|
|
1035
1105
|
"attempt failed (%s) %s %s %s",
|
|
1036
1106
|
stage,
|
|
1037
1107
|
self.__class__.__name__,
|
|
1038
|
-
self.
|
|
1108
|
+
self.furu_hash,
|
|
1039
1109
|
directory,
|
|
1040
1110
|
extra={"furu_file_only": True},
|
|
1041
1111
|
)
|
|
@@ -1082,7 +1152,7 @@ class Furu[T](ABC):
|
|
|
1082
1152
|
f"backend {attempt.backend}"
|
|
1083
1153
|
)
|
|
1084
1154
|
hints = [
|
|
1085
|
-
f"Furu hash: {self.
|
|
1155
|
+
f"Furu hash: {self.furu_hash}",
|
|
1086
1156
|
f"Directory: {directory}",
|
|
1087
1157
|
f"State file: {state_path}",
|
|
1088
1158
|
f"Attempt: {attempt_info}",
|
|
@@ -1169,9 +1239,7 @@ class Furu[T](ABC):
|
|
|
1169
1239
|
stage = "metadata"
|
|
1170
1240
|
try:
|
|
1171
1241
|
# Create metadata (now safe - attempt is already recorded)
|
|
1172
|
-
metadata = MetadataManager.create_metadata(
|
|
1173
|
-
self, directory, ignore_diff=FURU_CONFIG.ignore_git_diff
|
|
1174
|
-
)
|
|
1242
|
+
metadata = MetadataManager.create_metadata(self, directory)
|
|
1175
1243
|
MetadataManager.write_metadata(metadata, directory)
|
|
1176
1244
|
|
|
1177
1245
|
# Set up preemption handler
|
|
@@ -1185,7 +1253,7 @@ class Furu[T](ABC):
|
|
|
1185
1253
|
logger.debug(
|
|
1186
1254
|
"_create: begin %s %s %s",
|
|
1187
1255
|
self.__class__.__name__,
|
|
1188
|
-
self.
|
|
1256
|
+
self.furu_hash,
|
|
1189
1257
|
directory,
|
|
1190
1258
|
)
|
|
1191
1259
|
token = None
|
|
@@ -1197,7 +1265,7 @@ class Furu[T](ABC):
|
|
|
1197
1265
|
mode="executor",
|
|
1198
1266
|
spec_key=self._executor_spec_key(),
|
|
1199
1267
|
backend="local",
|
|
1200
|
-
current_node_hash=self.
|
|
1268
|
+
current_node_hash=self.furu_hash,
|
|
1201
1269
|
)
|
|
1202
1270
|
)
|
|
1203
1271
|
try:
|
|
@@ -1208,7 +1276,7 @@ class Furu[T](ABC):
|
|
|
1208
1276
|
logger.debug(
|
|
1209
1277
|
"_create: ok %s %s %s",
|
|
1210
1278
|
self.__class__.__name__,
|
|
1211
|
-
self.
|
|
1279
|
+
self.furu_hash,
|
|
1212
1280
|
directory,
|
|
1213
1281
|
)
|
|
1214
1282
|
StateManager.write_success_marker(
|
|
@@ -1220,7 +1288,7 @@ class Furu[T](ABC):
|
|
|
1220
1288
|
logger.info(
|
|
1221
1289
|
"_create ok %s %s",
|
|
1222
1290
|
self.__class__.__name__,
|
|
1223
|
-
self.
|
|
1291
|
+
self.furu_hash,
|
|
1224
1292
|
extra={"furu_console_only": True},
|
|
1225
1293
|
)
|
|
1226
1294
|
return "success", True, result
|
|
@@ -1229,7 +1297,7 @@ class Furu[T](ABC):
|
|
|
1229
1297
|
logger.error(
|
|
1230
1298
|
"_create failed %s %s %s",
|
|
1231
1299
|
self.__class__.__name__,
|
|
1232
|
-
self.
|
|
1300
|
+
self.furu_hash,
|
|
1233
1301
|
directory,
|
|
1234
1302
|
extra={"furu_file_only": True},
|
|
1235
1303
|
)
|
|
@@ -1238,7 +1306,7 @@ class Furu[T](ABC):
|
|
|
1238
1306
|
"attempt failed (%s) %s %s %s",
|
|
1239
1307
|
stage,
|
|
1240
1308
|
self.__class__.__name__,
|
|
1241
|
-
self.
|
|
1309
|
+
self.furu_hash,
|
|
1242
1310
|
directory,
|
|
1243
1311
|
extra={"furu_file_only": True},
|
|
1244
1312
|
)
|
|
@@ -1350,7 +1418,7 @@ def _collect_dependencies(
|
|
|
1350
1418
|
recursive: bool,
|
|
1351
1419
|
) -> None:
|
|
1352
1420
|
for dependency in _direct_dependencies(obj):
|
|
1353
|
-
digest = dependency.
|
|
1421
|
+
digest = dependency.furu_hash
|
|
1354
1422
|
if digest in seen:
|
|
1355
1423
|
continue
|
|
1356
1424
|
seen.add(digest)
|
|
@@ -1504,7 +1572,7 @@ def _sorted_dependency_set(
|
|
|
1504
1572
|
|
|
1505
1573
|
def _dependency_sort_key(value: DependencyScanValue) -> tuple[int, str]:
|
|
1506
1574
|
if isinstance(value, Furu):
|
|
1507
|
-
return (0, value.
|
|
1575
|
+
return (0, cast(str, value.furu_hash))
|
|
1508
1576
|
return (1, f"{type(value).__name__}:{value!r}")
|
|
1509
1577
|
|
|
1510
1578
|
|
furu/core/list.py
CHANGED
|
@@ -25,10 +25,11 @@ class _FuruListMeta(type):
|
|
|
25
25
|
if not isinstance(obj, Furu):
|
|
26
26
|
raise TypeError(f"{obj!r} is not a Furu instance")
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
furu_obj = cast(Furu, obj)
|
|
29
|
+
digest = furu_obj.furu_hash
|
|
29
30
|
if digest not in seen:
|
|
30
31
|
seen.add(digest)
|
|
31
|
-
items.append(cast(_H,
|
|
32
|
+
items.append(cast(_H, furu_obj))
|
|
32
33
|
|
|
33
34
|
for name, value in cls.__dict__.items():
|
|
34
35
|
if name.startswith("_") or callable(value):
|