Skip to content

Commit 3da8a07

Browse files
JasonHokuclaude
andcommitted
refactor: migrate distribution_worker.py to network gateway
All 6 urllib call patterns (download, register, claim, submit, heartbeat, fail) now use gateway functions from network_utils.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9f7d9ae commit 3da8a07

1 file changed

Lines changed: 90 additions & 130 deletions

File tree

distribution_worker.py

Lines changed: 90 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
import io
1212
import os
1313
import random
14-
import urllib.request
15-
import urllib.error
16-
import urllib.parse
14+
from urllib.parse import quote as url_quote
15+
from .network_utils import (
16+
distribution_get,
17+
distribution_post_json,
18+
distribution_post_multipart,
19+
distribution_stream_download,
20+
)
1721

1822
import torch
1923

@@ -99,48 +103,33 @@ def _download_model_from_master(self, category, filename):
99103
# Create subdirectories
100104
os.makedirs(os.path.dirname(target_path), exist_ok=True)
101105

102-
# Download from master
103-
url = f"{self.master_url}/distribution/download_model"
104-
payload = json.dumps({
105-
"worker_id": self.worker_id,
106-
"category": category,
107-
"filename": filename
108-
}).encode("utf-8")
109-
110-
req = urllib.request.Request(
111-
url, data=payload,
112-
headers={"Content-Type": "application/json"},
113-
method="POST"
114-
)
115-
106+
# Download from master via network gateway
116107
try:
117108
print(f"[Worker {self.worker_id}] ⬇️ Downloading {category}/{filename}...")
118-
with urllib.request.urlopen(req, timeout=3600) as resp:
119-
file_size = int(resp.headers.get("Content-Length", 0))
120-
if file_size > 0:
121-
size_str = f"{file_size / (1024**3):.2f} GB" if file_size > 1024**3 else f"{file_size / (1024**2):.0f} MB"
122-
print(f"[Worker {self.worker_id}] ⬇️ Size: {size_str}")
123-
124-
# Stream to temp file, then rename (atomic-ish)
125-
temp_path = target_path + ".downloading"
126-
downloaded = 0
127-
last_progress = 0
128-
with open(temp_path, "wb") as f:
129-
while True:
130-
chunk = resp.read(8 * 1024 * 1024) # 8 MB chunks
131-
if not chunk:
132-
break
133-
f.write(chunk)
134-
downloaded += len(chunk)
135-
if file_size > 0:
136-
progress = int(downloaded / file_size * 100)
137-
if progress >= last_progress + 10:
138-
print(f"[Worker {self.worker_id}] ⬇️ {category}/{filename}: {progress}%")
139-
last_progress = progress
140-
141-
os.replace(temp_path, target_path)
142-
print(f"[Worker {self.worker_id}] ✅ Downloaded {category}/{filename}")
143-
return target_path
109+
last_progress = 0
110+
111+
def on_progress(downloaded, total_size):
112+
nonlocal last_progress
113+
if total_size > 0:
114+
pct = int(downloaded * 100 / total_size)
115+
if pct >= last_progress + 10:
116+
last_progress = pct
117+
size_str = f"{total_size / (1024**3):.2f} GB" if total_size > 1024**3 else f"{total_size / (1024**2):.0f} MB"
118+
print(f"[Worker {self.worker_id}] ⬇️ {category}/{filename}: {pct}% of {size_str}")
119+
120+
distribution_stream_download(
121+
url=f"{self.master_url}/distribution/download_model",
122+
data_dict={
123+
"worker_id": self.worker_id,
124+
"category": category,
125+
"filename": filename
126+
},
127+
target_path=target_path,
128+
timeout=3600,
129+
progress_callback=on_progress
130+
)
131+
print(f"[Worker {self.worker_id}] ✅ Downloaded {category}/{filename}")
132+
return target_path
144133

145134
except Exception as e:
146135
print(f"[Worker {self.worker_id}] ❌ Failed to download {category}/{filename}: {e}")
@@ -364,25 +353,18 @@ def _register(self):
364353
retry_delay = 3 # seconds between retries
365354

366355
for attempt in range(1, max_retries + 1):
367-
data = json.dumps({
368-
"worker_id": self.worker_id,
369-
"worker_url": ""
370-
}).encode("utf-8")
371-
372-
req = urllib.request.Request(
373-
f"{self.master_url}/distribution/register_worker",
374-
data=data,
375-
headers={"Content-Type": "application/json"},
376-
method="POST"
377-
)
378356
try:
379-
with urllib.request.urlopen(req, timeout=10) as resp:
380-
result = json.loads(resp.read().decode("utf-8"))
381-
print(f"[Worker {self.worker_id}] 🤝 Registered with master "
382-
f"(session: {result.get('session_name', 'unknown')})")
383-
return # Success
384-
except urllib.error.HTTPError as e:
385-
if e.code == 503 and attempt < max_retries:
357+
status, resp_data = distribution_post_json(
358+
f"{self.master_url}/distribution/register_worker",
359+
{"worker_id": self.worker_id, "worker_url": ""},
360+
timeout=10
361+
)
362+
result = json.loads(resp_data.decode("utf-8"))
363+
print(f"[Worker {self.worker_id}] 🤝 Registered with master "
364+
f"(session: {result.get('session_name', 'unknown')})")
365+
return # Success
366+
except Exception as e:
367+
if hasattr(e, 'code') and e.code == 503 and attempt < max_retries:
386368
print(f"[Worker {self.worker_id}] ⏳ Master not ready yet "
387369
f"(attempt {attempt}/{max_retries}), retrying in {retry_delay}s...")
388370
self._stop_event.wait(retry_delay)
@@ -391,46 +373,42 @@ def _register(self):
391373
continue
392374
print(f"[Worker {self.worker_id}] ⚠️ Registration failed: {e}")
393375
return
394-
except Exception as e:
395-
print(f"[Worker {self.worker_id}] ⚠️ Registration failed: {e}")
396-
return
397376

398377
def _claim_job(self):
399378
"""Claim next job from master. Returns job dict or None."""
400379
url = (f"{self.master_url}/distribution/claim_job"
401-
f"?worker_id={urllib.parse.quote(self.worker_id)}")
380+
f"?worker_id={url_quote(self.worker_id)}")
402381

403382
if self.consecutive_empty == 0 and self.jobs_processed == 0:
404383
print(f"[Worker {self.worker_id}] 🔍 Claiming from: {url}")
405384

406-
req = urllib.request.Request(url, method="GET")
407385
try:
408-
with urllib.request.urlopen(req, timeout=10) as resp:
409-
if resp.status == 204:
410-
return None
411-
return json.loads(resp.read().decode("utf-8"))
412-
except urllib.error.HTTPError as e:
413-
if e.code == 204:
414-
return None
415-
if e.code == 503:
416-
# Distribution not active yet — master may still be starting up
417-
# (e.g. pre-encoding prompts). Don't immediately stop; let the
418-
# normal empty-poll counter handle eventual timeout.
419-
if self.jobs_processed == 0 and self.consecutive_empty < 5:
420-
print(f"[Worker {self.worker_id}] ⏳ Master not active yet, "
421-
f"waiting... ({self.consecutive_empty + 1}/5)")
422-
elif self.jobs_processed > 0:
423-
# Already processed jobs — master has finished and deactivated.
424-
# Stop immediately to avoid wasting time.
425-
print(f"[Worker {self.worker_id}] ℹ️ Distribution ended on master")
426-
self.consecutive_empty = self.max_empty_polls
427-
else:
428-
print(f"[Worker {self.worker_id}] ℹ️ Distribution not active on master")
429-
self.consecutive_empty = self.max_empty_polls
386+
status, resp_data = distribution_get(url, timeout=10)
387+
if status == 204:
430388
return None
431-
print(f"[Worker {self.worker_id}] ⚠️ Claim failed: HTTP {e.code}")
432-
return None
389+
return json.loads(resp_data.decode("utf-8"))
433390
except Exception as e:
391+
if hasattr(e, 'code'):
392+
if e.code == 204:
393+
return None
394+
if e.code == 503:
395+
# Distribution not active yet — master may still be starting up
396+
# (e.g. pre-encoding prompts). Don't immediately stop; let the
397+
# normal empty-poll counter handle eventual timeout.
398+
if self.jobs_processed == 0 and self.consecutive_empty < 5:
399+
print(f"[Worker {self.worker_id}] ⏳ Master not active yet, "
400+
f"waiting... ({self.consecutive_empty + 1}/5)")
401+
elif self.jobs_processed > 0:
402+
# Already processed jobs — master has finished and deactivated.
403+
# Stop immediately to avoid wasting time.
404+
print(f"[Worker {self.worker_id}] ℹ️ Distribution ended on master")
405+
self.consecutive_empty = self.max_empty_polls
406+
else:
407+
print(f"[Worker {self.worker_id}] ℹ️ Distribution not active on master")
408+
self.consecutive_empty = self.max_empty_polls
409+
return None
410+
print(f"[Worker {self.worker_id}] ⚠️ Claim failed: HTTP {e.code}")
411+
return None
434412
print(f"[Worker {self.worker_id}] ⚠️ Claim failed: {e}")
435413
return None
436414

@@ -715,24 +693,20 @@ def _submit_result(self, job_id, image_bytes, meta, max_retries=3):
715693
last_error = None
716694
for attempt in range(max_retries):
717695
try:
718-
req = urllib.request.Request(
696+
status, _ = distribution_post_multipart(
719697
f"{self.master_url}/distribution/submit_result",
720-
data=body,
721-
headers={
722-
"Content-Type": f"multipart/form-data; boundary={boundary}",
723-
},
724-
method="POST"
698+
f"multipart/form-data; boundary={boundary}",
699+
body,
700+
timeout=60
725701
)
726-
727-
with urllib.request.urlopen(req, timeout=60) as resp:
728-
if resp.status != 200:
729-
raise RuntimeError(f"Submit failed: HTTP {resp.status}")
702+
if status != 200:
703+
raise RuntimeError(f"Submit failed: HTTP {status}")
730704
return # Success
731705

732-
except urllib.error.HTTPError as e:
706+
except Exception as e:
733707
# 503 = master deactivated (all jobs done). Don't retry — it won't help.
734708
# Raise a special exception so the main loop can stop gracefully.
735-
if e.code == 503:
709+
if hasattr(e, 'code') and e.code == 503:
736710
raise _MasterFinishedError(
737711
f"Master distribution is no longer active (HTTP 503)"
738712
)
@@ -743,27 +717,16 @@ def _submit_result(self, job_id, image_bytes, meta, max_retries=3):
743717
f"for job {job_id}: {e} (waiting {wait}s)")
744718
time.sleep(wait)
745719

746-
except (urllib.error.URLError, ConnectionError, OSError) as e:
747-
last_error = e
748-
if attempt < max_retries - 1:
749-
wait = 2 ** attempt # Exponential backoff: 1s, 2s, 4s
750-
print(f"[Worker {self.worker_id}] ⚠️ Submit retry {attempt + 1}/{max_retries} "
751-
f"for job {job_id}: {e} (waiting {wait}s)")
752-
time.sleep(wait)
753-
754720
raise RuntimeError(f"Submit failed after {max_retries} attempts: {last_error}")
755721

756722
def _send_heartbeat(self):
757723
"""Send heartbeat to master."""
758-
data = json.dumps({"worker_id": self.worker_id}).encode("utf-8")
759-
req = urllib.request.Request(
760-
f"{self.master_url}/distribution/heartbeat",
761-
data=data,
762-
headers={"Content-Type": "application/json"},
763-
method="POST"
764-
)
765724
try:
766-
urllib.request.urlopen(req, timeout=5)
725+
distribution_post_json(
726+
f"{self.master_url}/distribution/heartbeat",
727+
{"worker_id": self.worker_id},
728+
timeout=5
729+
)
767730
except Exception:
768731
pass
769732

@@ -780,19 +743,16 @@ def _heartbeat_loop(self):
780743

781744
def _report_failure(self, job_id, error):
782745
"""Report job failure to master."""
783-
data = json.dumps({
784-
"job_id": job_id,
785-
"error": error,
786-
"worker_id": self.worker_id
787-
}).encode("utf-8")
788-
req = urllib.request.Request(
789-
f"{self.master_url}/distribution/fail_job",
790-
data=data,
791-
headers={"Content-Type": "application/json"},
792-
method="POST"
793-
)
794746
try:
795-
urllib.request.urlopen(req, timeout=10)
747+
distribution_post_json(
748+
f"{self.master_url}/distribution/fail_job",
749+
{
750+
"job_id": job_id,
751+
"error": error,
752+
"worker_id": self.worker_id
753+
},
754+
timeout=10
755+
)
796756
except Exception:
797757
pass
798758

0 commit comments

Comments
 (0)