Fix mirrors hang when /status endpoint is down (#4031)

* add explicit _fetched_remote bool

* Attempt 2

* Adds about 15 seconds time-out to fetch_data_from_url with 3 retries (4, 5, 6)
Then fallsback to fully local list

* Feedbacks: 20 -> 30
Do not return early
Add debug
Remove new flag
60 second timeout for reflector

* Clean up install logs by hiding mirror scores behind --verbose
This commit is contained in:
HADEON 2025-12-28 01:10:03 +01:00 committed by GitHub
parent c1eae10e93
commit 79313c4942
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 30 additions and 8 deletions

View File

@ -194,9 +194,16 @@ class Installer:
else: else:
info(tr('Skipping waiting for automatic time sync (this can cause issues if time is out of sync during installation)')) info(tr('Skipping waiting for automatic time sync (this can cause issues if time is out of sync during installation)'))
info('Waiting for automatic mirror selection (reflector) to complete.') if not arch_config_handler.args.offline:
while self._service_state('reflector') not in ('dead', 'failed', 'exited'): info('Waiting for automatic mirror selection (reflector) to complete.')
time.sleep(1) for _ in range(60):
if self._service_state('reflector') in ('dead', 'failed', 'exited'):
break
time.sleep(1)
else:
warn('Reflector did not complete within 60 seconds, continuing anyway...')
else:
info('Skipped reflector...')
# info('Waiting for pacman-init.service to complete.') # info('Waiting for pacman-init.service to complete.')
# while self._service_state('pacman-init') not in ('dead', 'failed', 'exited'): # while self._service_state('pacman-init') not in ('dead', 'failed', 'exited'):

View File

@ -389,6 +389,7 @@ class MirrorListHandler:
) -> None: ) -> None:
self._local_mirrorlist = local_mirrorlist self._local_mirrorlist = local_mirrorlist
self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None self._status_mappings: dict[str, list[MirrorStatusEntryV3]] | None = None
self._fetched_remote: bool = False
def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]: def _mappings(self) -> dict[str, list[MirrorStatusEntryV3]]:
if self._status_mappings is None: if self._status_mappings is None:
@ -412,9 +413,12 @@ class MirrorListHandler:
from .args import arch_config_handler from .args import arch_config_handler
if arch_config_handler.args.offline: if arch_config_handler.args.offline:
self._fetched_remote = False
self.load_local_mirrors() self.load_local_mirrors()
else: else:
if not self.load_remote_mirrors(): self._fetched_remote = self.load_remote_mirrors()
debug(f'load mirrors: {self._fetched_remote}')
if not self._fetched_remote:
self.load_local_mirrors() self.load_local_mirrors()
def load_remote_mirrors(self) -> bool: def load_remote_mirrors(self) -> bool:
@ -441,7 +445,15 @@ class MirrorListHandler:
def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]: def get_status_by_region(self, region: str, speed_sort: bool) -> list[MirrorStatusEntryV3]:
mappings = self._mappings() mappings = self._mappings()
region_list = mappings[region] region_list = mappings[region]
return sorted(region_list, key=lambda mirror: (mirror.score, mirror.speed))
# Only sort if we have remote mirror data with score/speed info
# Local mirrors lack this data and can be modified manually before-hand
# Or reflector potentially ran already
if self._fetched_remote and speed_sort:
# original return
return sorted(region_list, key=lambda mirror: (mirror.score, mirror.speed))
# just return as-is without sorting?
return region_list
def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]: def _parse_remote_mirror_list(self, mirrorlist: str) -> dict[str, list[MirrorStatusEntryV3]]:
mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist) mirror_status = MirrorStatusListV3.model_validate_json(mirrorlist)

View File

@ -105,10 +105,13 @@ class MirrorStatusEntryV3(BaseModel):
@model_validator(mode='after') @model_validator(mode='after')
def debug_output(self) -> 'MirrorStatusEntryV3': def debug_output(self) -> 'MirrorStatusEntryV3':
from ..args import arch_config_handler
self._hostname, *port = urllib.parse.urlparse(self.url).netloc.split(':', 1) self._hostname, *port = urllib.parse.urlparse(self.url).netloc.split(':', 1)
self._port = int(port[0]) if port and len(port) >= 1 else None self._port = int(port[0]) if port and len(port) >= 1 else None
debug(f'Loaded mirror {self._hostname}' + (f' with current score of {self.score}' if self.score else '')) if arch_config_handler.args.verbose:
debug(f'Loaded mirror {self._hostname}' + (f' with current score of {self.score}' if self.score else ''))
return self return self

View File

@ -121,7 +121,7 @@ def enrich_iface_types(interfaces: list[str]) -> dict[str, str]:
return result return result
def fetch_data_from_url(url: str, params: dict[str, str] | None = None) -> str: def fetch_data_from_url(url: str, params: dict[str, str] | None = None, timeout: int = 30) -> str:
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
@ -133,7 +133,7 @@ def fetch_data_from_url(url: str, params: dict[str, str] | None = None) -> str:
full_url = url full_url = url
try: try:
response = urlopen(full_url, context=ssl_context) response = urlopen(full_url, context=ssl_context, timeout=timeout)
data = response.read().decode('UTF-8') data = response.read().decode('UTF-8')
return data return data
except URLError as e: except URLError as e: