Skip to content

Commit 06affa5

Browse files
committed
Merge branch 'dev' into pjwerneck/improve-peer-list-display
2 parents 0efcefc + 55a1dbc commit 06affa5

68 files changed

Lines changed: 2995 additions & 771 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/syft-bg/src/syft_bg/notify/monitors/job.py

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Optional
55

66
from syft_job.config import SyftJobConfig
7+
from syft_job.job_storage import JobRef, JobStorage
78
from syft_job.models import JobState, JobStatus, JobSubmissionMetadata
89

910
from syft_bg.common.monitor import Monitor
@@ -29,28 +30,20 @@ def __init__(
2930
self.job_config = SyftJobConfig.from_syftbox_folder(
3031
str(self.syftbox_root), do_email
3132
)
33+
self.job_manager = JobStorage(config=self.job_config)
3234

3335
def _check_all_entities(self):
3436
self.process_local_status_changes()
3537

3638
def process_local_status_changes(self):
37-
inbox_dir = self.job_config.get_all_submissions_dir(self.do_email)
38-
if not inbox_dir.exists():
39-
return
40-
41-
for ds_dir in inbox_dir.iterdir():
42-
if not ds_dir.is_dir():
43-
continue
44-
for job_path in ds_dir.iterdir():
45-
if not job_path.is_dir():
46-
continue
47-
try:
48-
self._maybe_process_job(job_path)
49-
except Exception as e:
50-
print(f"[JobMonitor] Error checking job {job_path.name}: {e}")
51-
52-
def _maybe_process_job(self, job_path: Path):
53-
metadata = self._load_job_metadata(job_path)
39+
for ref in self.job_manager.iter_submission_refs(self.do_email):
40+
try:
41+
self._maybe_process_job(ref)
42+
except Exception as e:
43+
print(f"[JobMonitor] Error checking job {ref.job_name}: {e}")
44+
45+
def _maybe_process_job(self, ref: JobRef):
46+
metadata = self._load_job_metadata(ref)
5447
if not metadata:
5548
return
5649

@@ -62,7 +55,7 @@ def _maybe_process_job(self, job_path: Path):
6255
if success:
6356
print(f"[JobMonitor] Sent new job notification: {job_name}")
6457

65-
review_state = self._load_review_state(ds_email, job_name)
58+
review_state = self._load_review_state(ref)
6659

6760
if review_state and review_state.status in (
6861
JobStatus.APPROVED,
@@ -85,61 +78,40 @@ def _maybe_process_job(self, job_path: Path):
8578

8679
def seed_existing_jobs(self):
8780
"""On fresh state, mark all existing jobs so we don't re-notify old jobs."""
88-
inbox_dir = self.job_config.get_all_submissions_dir(self.do_email)
89-
if not inbox_dir.exists():
90-
return
91-
9281
count = 0
93-
for ds_dir in inbox_dir.iterdir():
94-
if not ds_dir.is_dir():
82+
for ref in self.job_manager.iter_submission_refs(self.do_email):
83+
metadata = self._load_job_metadata(ref)
84+
if not metadata:
9585
continue
96-
for job_path in ds_dir.iterdir():
97-
if not job_path.is_dir():
98-
continue
99-
metadata = self._load_job_metadata(job_path)
100-
if not metadata:
101-
continue
102-
self.state.mark_notified(metadata.name, "new")
103-
review_state = self._load_review_state(
104-
metadata.submitted_by, metadata.name
105-
)
106-
if review_state:
107-
if review_state.status in (
108-
JobStatus.APPROVED,
109-
JobStatus.RUNNING,
110-
JobStatus.DONE,
111-
JobStatus.FAILED,
112-
):
113-
self.state.mark_notified(metadata.name, "approved")
114-
if review_state.status == JobStatus.DONE:
115-
self.state.mark_notified(metadata.name, "executed")
116-
if review_state.status == JobStatus.FAILED:
117-
self.state.mark_notified(metadata.name, "failed")
118-
count += 1
86+
self.state.mark_notified(ref.job_name, "new")
87+
review_state = self._load_review_state(ref)
88+
if review_state:
89+
if review_state.status in (
90+
JobStatus.APPROVED,
91+
JobStatus.RUNNING,
92+
JobStatus.DONE,
93+
JobStatus.FAILED,
94+
):
95+
self.state.mark_notified(metadata.name, "approved")
96+
if review_state.status == JobStatus.DONE:
97+
self.state.mark_notified(metadata.name, "executed")
98+
if review_state.status == JobStatus.FAILED:
99+
self.state.mark_notified(metadata.name, "failed")
100+
count += 1
119101

120102
if count:
121103
print(f"[JobMonitor] Seeded {count} existing jobs on fresh state")
122104

123-
def _load_review_state(self, ds_email: str, job_name: str) -> Optional[JobState]:
105+
def _load_review_state(self, ref: JobRef) -> Optional[JobState]:
124106
"""Load state.yaml from the job's review directory."""
125-
review_dir = self.job_config.get_review_job_dir(
126-
self.do_email, ds_email, job_name
127-
)
128-
state_file = review_dir / "state.yaml"
129-
if not state_file.exists():
130-
return None
131107
try:
132-
return JobState.load(state_file)
108+
return self.job_manager.read_state(ref)
133109
except Exception:
134110
return None
135111

136-
def _load_job_metadata(self, job_path: Path) -> Optional[JobSubmissionMetadata]:
137-
config_file = job_path / "config.yaml"
138-
if not config_file.exists():
139-
return None
140-
112+
def _load_job_metadata(self, ref: JobRef) -> Optional[JobSubmissionMetadata]:
141113
try:
142-
return JobSubmissionMetadata.load(config_file)
114+
return self.job_manager.read_submission(ref)
143115
except Exception as e:
144-
print(f"[JobMonitor] Error reading job config {config_file}: {e}")
116+
print(f"[JobMonitor] Error reading job config for {ref.job_name}: {e}")
145117
return None

packages/syft-enclave/src/syft_enclaves/client.py

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from syft_client.sync.peers.peer_list import PeerList
99
from syft_datasets.dataset_manager import SyftDatasetManager
1010
from syft_job.job import JobInfo, JobsList
11-
from syft_job.models import JobState, JobStatus, JobSubmissionMetadata
11+
from syft_job.job_storage import JobRef
12+
from syft_job.models import JobState, JobStatus
1213

1314
from syft_enclaves.enclave_job_info import (
1415
EnclaveJobInfo,
@@ -255,33 +256,19 @@ def receive_jobs(self):
255256
4. Sets permissions and marks as distributed
256257
"""
257258
self._manager.job_client.scan_inbox()
258-
inbox_dir = self._manager.job_client.config.get_all_submissions_dir(
259-
self._manager.email
260-
)
261-
if not inbox_dir.exists():
262-
return
259+
job_manager = self._manager.job_client.manager
260+
for ref in job_manager.iter_submission_refs(self._manager.email):
261+
self._try_distribute_job(ref)
263262

264-
for ds_dir in inbox_dir.iterdir():
265-
if not ds_dir.is_dir():
266-
continue
267-
for job_dir in ds_dir.iterdir():
268-
if not job_dir.is_dir():
269-
continue
270-
self._try_distribute_job(ds_dir.name, job_dir)
271-
272-
def _try_distribute_job(self, ds_email: str, job_dir: Path):
263+
def _try_distribute_job(self, ref: JobRef):
273264
"""Distribute a single enclave job to relevant DOs if not yet distributed."""
274-
config_path = job_dir / "config.yaml"
275-
if not config_path.exists():
276-
return
277-
278-
config = JobSubmissionMetadata.load(config_path)
265+
job_manager = self._manager.job_client.manager
266+
job_dir = job_manager.submission_dir(ref)
267+
config = job_manager.read_submission(ref)
279268
if config.job_type != "enclave" or not config.datasets:
280269
return
281270

282-
review_dir = self._manager.job_client.config.get_review_job_dir(
283-
self._manager.email, ds_email, job_dir.name
284-
)
271+
review_dir = job_manager.review_dir(ref)
285272
distributed_marker = review_dir / "distributed"
286273
if distributed_marker.exists():
287274
return
@@ -296,7 +283,7 @@ def _try_distribute_job(self, ds_email: str, job_dir: Path):
296283
recipients = list(dict.fromkeys([*submission_dos, *approval_dos]))
297284
self._forward_job_to_dos(job_dir, recipients)
298285
self._save_enclave_job_state(review_dir, approval_dos, config.datasets)
299-
self._set_job_permissions(job_dir, recipients, approval_dos)
286+
self._set_job_permissions(ref, recipients, approval_dos)
300287
self._forward_approval_files_to_dos(review_dir, approval_dos)
301288

302289
distributed_marker.parent.mkdir(parents=True, exist_ok=True)
@@ -369,22 +356,17 @@ def _save_enclave_job_state(
369356

370357
def _set_job_permissions(
371358
self,
372-
job_dir: Path,
359+
ref: JobRef,
373360
read_dos: list[str],
374361
approval_dos: list[str],
375362
):
376363
"""Grant inbox read to everyone who needs to see the job (referenced +
377364
approving DOs), and approval-file write to the approving DOs."""
365+
job_manager = self._manager.job_client.manager
378366
datasite = self._manager.syftbox_folder / self._manager.email
379367
ctx = SyftPermContext(datasite=datasite)
380-
inbox_rel = job_dir.relative_to(datasite)
381-
382-
ds_email = job_dir.parent.name
383-
job_name = job_dir.name
384-
review_dir = self._manager.job_client.config.get_review_job_dir(
385-
self._manager.email, ds_email, job_name
386-
)
387-
review_rel = review_dir.relative_to(datasite)
368+
inbox_rel = job_manager.submission_dir(ref).relative_to(datasite)
369+
review_rel = job_manager.review_dir(ref).relative_to(datasite)
388370

389371
for do_email in dict.fromkeys([*read_dos, *approval_dos]):
390372
ctx.open(inbox_rel).grant_read_access(do_email)

packages/syft-enclave/src/syft_enclaves/enclave_job_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ def __init__(self, job_client: JobClient):
2020
def config(self):
2121
return self._job_client.config
2222

23+
@property
24+
def manager(self):
25+
return self._job_client.manager
26+
2327
@property
2428
def current_user_email(self):
2529
return self._job_client.current_user_email

packages/syft-job/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# syft-job
2+
3+
Job submission and execution for SyftBox: data scientists submit bash/Python jobs
4+
into a data owner's inbox, the data owner reviews, approves, and runs them.
5+
6+
## Releasing
7+
8+
On **every** release (after bumping the version), run both release scripts:
9+
10+
```bash
11+
uv run python scripts/export_release_artifact.py
12+
uv run python scripts/generate_release_fixture.py
13+
```
14+
15+
`export_release_artifact.py` always writes
16+
`src/syft_job/migrations/history/package-artifacts/syft-job-<version>.json`
17+
(the package's identity + the protocol it speaks), and additionally writes
18+
`history/protocols/protocol-<n>.json` when the release ships a new protocol
19+
version. It refuses to run if the job protocol changed without bumping
20+
`JOB_PROTOCOL_VERSION` (`src/syft_job/migrations/registry.py`).
21+
22+
`generate_release_fixture.py` writes a full SyftBox tree —
23+
`tests/migrations/p2p/fixtures/syft_job-<version>-protocol<p>_syftbox/` — exactly
24+
as this release serializes jobs to disk. Commit it: future releases loop over
25+
these fixtures (`test_older_protocol_compatibility.py`) to prove they can still
26+
read and round-trip older on-disk data.
27+
28+
Tests check the code against these artifacts: released object versions are
29+
frozen forever — changing one requires a new version plus migrations.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Export the release artifacts for the current syft-job version.
2+
3+
Run on EVERY release (uv run python scripts/export_release_artifact.py):
4+
always writes the package release info; additionally writes the protocol
5+
artifact when this release introduces a new protocol version.
6+
"""
7+
8+
import sys
9+
10+
from syft_job.migrations.history import PACKAGE_ARTIFACTS_DIR, PROTOCOLS_DIR
11+
from syft_job.migrations.registry import JOB_PROTOCOL_VERSION, job_registry
12+
from syft_job.version import __version__
13+
14+
15+
def main() -> None:
16+
# Import the models so every versioned object is registered.
17+
import syft_job # noqa: F401
18+
19+
if job_registry.protocol_changed_without_bump():
20+
sys.exit(
21+
"The job protocol changed compared to the released "
22+
f"protocol-{JOB_PROTOCOL_VERSION}.json; bump JOB_PROTOCOL_VERSION "
23+
"in syft_job/migrations/registry.py before releasing."
24+
)
25+
26+
info_path = PACKAGE_ARTIFACTS_DIR / f"syft-job-{__version__}.json"
27+
job_registry.compute_released_package_protocol_info().save(info_path)
28+
print(f"Wrote {info_path}")
29+
30+
protocol_path = PROTOCOLS_DIR / f"protocol-{JOB_PROTOCOL_VERSION}.json"
31+
if not protocol_path.exists():
32+
job_registry.compute_released_protocol().save(protocol_path)
33+
print(f"Wrote {protocol_path} (new protocol version)")
34+
35+
36+
if __name__ == "__main__":
37+
main()

0 commit comments

Comments
 (0)