1111import io
1212import os
1313import 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
1822import 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