Skip to content

Commit c620284

Browse files
authored
Improve error handling in urlopen / socket read (spack#48707)
* Backward compat with Python 3.9 for socket.timeout * Forward compat with Python [unknown] as HTTPResponse.geturl is deprecated * Catch timeout etc from .read() * Some minor simplifications: json.load(...) takes file object in binary mode. * Fix CDash code which does error handling wrong: non-2XX responses raise.
1 parent b2a75db commit c620284

File tree

11 files changed

+144
-202
lines changed

11 files changed

+144
-202
lines changed

lib/spack/spack/binary_distribution.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ def url_read_method(url):
802802
try:
803803
_, _, spec_file = web_util.read_from_url(url)
804804
contents = codecs.getreader("utf-8")(spec_file).read()
805-
except web_util.SpackWebError as e:
805+
except (web_util.SpackWebError, OSError) as e:
806806
tty.error(f"Error reading specfile: {url}: {e}")
807807
return contents
808808

@@ -2010,7 +2010,7 @@ def fetch_url_to_mirror(url):
20102010

20112011
# Download the config = spec.json and the relevant tarball
20122012
try:
2013-
manifest = json.loads(response.read())
2013+
manifest = json.load(response)
20142014
spec_digest = spack.oci.image.Digest.from_string(manifest["config"]["digest"])
20152015
tarball_digest = spack.oci.image.Digest.from_string(
20162016
manifest["layers"][-1]["digest"]
@@ -2596,11 +2596,14 @@ def try_direct_fetch(spec, mirrors=None):
25962596
)
25972597
try:
25982598
_, _, fs = web_util.read_from_url(buildcache_fetch_url_signed_json)
2599+
specfile_contents = codecs.getreader("utf-8")(fs).read()
25992600
specfile_is_signed = True
2600-
except web_util.SpackWebError as e1:
2601+
except (web_util.SpackWebError, OSError) as e1:
26012602
try:
26022603
_, _, fs = web_util.read_from_url(buildcache_fetch_url_json)
2603-
except web_util.SpackWebError as e2:
2604+
specfile_contents = codecs.getreader("utf-8")(fs).read()
2605+
specfile_is_signed = False
2606+
except (web_util.SpackWebError, OSError) as e2:
26042607
tty.debug(
26052608
f"Did not find {specfile_name} on {buildcache_fetch_url_signed_json}",
26062609
e1,
@@ -2610,7 +2613,6 @@ def try_direct_fetch(spec, mirrors=None):
26102613
f"Did not find {specfile_name} on {buildcache_fetch_url_json}", e2, level=2
26112614
)
26122615
continue
2613-
specfile_contents = codecs.getreader("utf-8")(fs).read()
26142616

26152617
# read the spec from the build cache file. All specs in build caches
26162618
# are concrete (as they are built) so we need to mark this spec
@@ -2704,8 +2706,9 @@ def get_keys(install=False, trust=False, force=False, mirrors=None):
27042706

27052707
try:
27062708
_, _, json_file = web_util.read_from_url(keys_index)
2707-
json_index = sjson.load(codecs.getreader("utf-8")(json_file))
2708-
except web_util.SpackWebError as url_err:
2709+
json_index = sjson.load(json_file)
2710+
except (web_util.SpackWebError, OSError, ValueError) as url_err:
2711+
# TODO: avoid repeated request
27092712
if web_util.url_exists(keys_index):
27102713
tty.error(
27112714
f"Unable to find public keys in {url_util.format(fetch_url)},"
@@ -2955,11 +2958,11 @@ def get_remote_hash(self):
29552958
url_index_hash = url_util.join(self.url, BUILD_CACHE_RELATIVE_PATH, INDEX_HASH_FILE)
29562959
try:
29572960
response = self.urlopen(urllib.request.Request(url_index_hash, headers=self.headers))
2958-
except (TimeoutError, urllib.error.URLError):
2961+
remote_hash = response.read(64)
2962+
except OSError:
29592963
return None
29602964

29612965
# Validate the hash
2962-
remote_hash = response.read(64)
29632966
if not re.match(rb"[a-f\d]{64}$", remote_hash):
29642967
return None
29652968
return remote_hash.decode("utf-8")
@@ -2977,13 +2980,13 @@ def conditional_fetch(self) -> FetchIndexResult:
29772980

29782981
try:
29792982
response = self.urlopen(urllib.request.Request(url_index, headers=self.headers))
2980-
except (TimeoutError, urllib.error.URLError) as e:
2981-
raise FetchIndexError("Could not fetch index from {}".format(url_index), e) from e
2983+
except OSError as e:
2984+
raise FetchIndexError(f"Could not fetch index from {url_index}", e) from e
29822985

29832986
try:
29842987
result = codecs.getreader("utf-8")(response).read()
2985-
except ValueError as e:
2986-
raise FetchIndexError("Remote index {} is invalid".format(url_index), e) from e
2988+
except (ValueError, OSError) as e:
2989+
raise FetchIndexError(f"Remote index {url_index} is invalid") from e
29872990

29882991
computed_hash = compute_hash(result)
29892992

@@ -3027,12 +3030,12 @@ def conditional_fetch(self) -> FetchIndexResult:
30273030
# Not modified; that means fresh.
30283031
return FetchIndexResult(etag=None, hash=None, data=None, fresh=True)
30293032
raise FetchIndexError(f"Could not fetch index {url}", e) from e
3030-
except (TimeoutError, urllib.error.URLError) as e:
3033+
except OSError as e: # URLError, socket.timeout, etc.
30313034
raise FetchIndexError(f"Could not fetch index {url}", e) from e
30323035

30333036
try:
30343037
result = codecs.getreader("utf-8")(response).read()
3035-
except ValueError as e:
3038+
except (ValueError, OSError) as e:
30363039
raise FetchIndexError(f"Remote index {url} is invalid", e) from e
30373040

30383041
headers = response.headers
@@ -3064,11 +3067,11 @@ def conditional_fetch(self) -> FetchIndexResult:
30643067
headers={"Accept": "application/vnd.oci.image.manifest.v1+json"},
30653068
)
30663069
)
3067-
except (TimeoutError, urllib.error.URLError) as e:
3070+
except OSError as e:
30683071
raise FetchIndexError(f"Could not fetch manifest from {url_manifest}", e) from e
30693072

30703073
try:
3071-
manifest = json.loads(response.read())
3074+
manifest = json.load(response)
30723075
except Exception as e:
30733076
raise FetchIndexError(f"Remote index {url_manifest} is invalid", e) from e
30743077

@@ -3083,14 +3086,16 @@ def conditional_fetch(self) -> FetchIndexResult:
30833086
return FetchIndexResult(etag=None, hash=None, data=None, fresh=True)
30843087

30853088
# Otherwise fetch the blob / index.json
3086-
response = self.urlopen(
3087-
urllib.request.Request(
3088-
url=self.ref.blob_url(index_digest),
3089-
headers={"Accept": "application/vnd.oci.image.layer.v1.tar+gzip"},
3089+
try:
3090+
response = self.urlopen(
3091+
urllib.request.Request(
3092+
url=self.ref.blob_url(index_digest),
3093+
headers={"Accept": "application/vnd.oci.image.layer.v1.tar+gzip"},
3094+
)
30903095
)
3091-
)
3092-
3093-
result = codecs.getreader("utf-8")(response).read()
3096+
result = codecs.getreader("utf-8")(response).read()
3097+
except (OSError, ValueError) as e:
3098+
raise FetchIndexError(f"Remote index {url_manifest} is invalid", e) from e
30943099

30953100
# Make sure the blob we download has the advertised hash
30963101
if compute_hash(result) != index_digest.digest:

lib/spack/spack/ci/__init__.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import zipfile
1515
from collections import namedtuple
1616
from typing import Callable, Dict, List, Set
17-
from urllib.error import HTTPError, URLError
1817
from urllib.request import HTTPHandler, Request, build_opener
1918

2019
import llnl.util.filesystem as fs
@@ -472,12 +471,9 @@ def generate_pipeline(env: ev.Environment, args) -> None:
472471
# Use all unpruned specs to populate the build group for this set
473472
cdash_config = cfg.get("cdash")
474473
if options.cdash_handler and options.cdash_handler.auth_token:
475-
try:
476-
options.cdash_handler.populate_buildgroup(
477-
[options.cdash_handler.build_name(s) for s in pipeline_specs]
478-
)
479-
except (SpackError, HTTPError, URLError, TimeoutError) as err:
480-
tty.warn(f"Problem populating buildgroup: {err}")
474+
options.cdash_handler.populate_buildgroup(
475+
[options.cdash_handler.build_name(s) for s in pipeline_specs]
476+
)
481477
elif cdash_config:
482478
# warn only if there was actually a CDash configuration.
483479
tty.warn("Unable to populate buildgroup without CDash credentials")

lib/spack/spack/ci/common.py

Lines changed: 27 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
# Copyright Spack Project Developers. See COPYRIGHT file for details.
22
#
33
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
4-
import codecs
54
import copy
65
import json
76
import os
87
import re
9-
import ssl
108
import sys
119
import time
1210
from collections import deque
1311
from enum import Enum
1412
from typing import Dict, Generator, List, Optional, Set, Tuple
1513
from urllib.parse import quote, urlencode, urlparse
16-
from urllib.request import HTTPHandler, HTTPSHandler, Request, build_opener
14+
from urllib.request import Request
1715

1816
import llnl.util.filesystem as fs
1917
import llnl.util.tty as tty
20-
from llnl.util.lang import Singleton, memoized
18+
from llnl.util.lang import memoized
2119

2220
import spack.binary_distribution as bindist
2321
import spack.config as cfg
@@ -35,32 +33,11 @@
3533
from spack.reporters.cdash import SPACK_CDASH_TIMEOUT
3634
from spack.reporters.cdash import build_stamp as cdash_build_stamp
3735

38-
39-
def _urlopen():
40-
error_handler = web_util.SpackHTTPDefaultErrorHandler()
41-
42-
# One opener with HTTPS ssl enabled
43-
with_ssl = build_opener(
44-
HTTPHandler(), HTTPSHandler(context=web_util.ssl_create_default_context()), error_handler
45-
)
46-
47-
# One opener with HTTPS ssl disabled
48-
without_ssl = build_opener(
49-
HTTPHandler(), HTTPSHandler(context=ssl._create_unverified_context()), error_handler
50-
)
51-
52-
# And dynamically dispatch based on the config:verify_ssl.
53-
def dispatch_open(fullurl, data=None, timeout=None, verify_ssl=True):
54-
opener = with_ssl if verify_ssl else without_ssl
55-
timeout = timeout or cfg.get("config:connect_timeout", 1)
56-
return opener.open(fullurl, data, timeout)
57-
58-
return dispatch_open
59-
60-
6136
IS_WINDOWS = sys.platform == "win32"
6237
SPACK_RESERVED_TAGS = ["public", "protected", "notary"]
63-
_dyn_mapping_urlopener = Singleton(_urlopen)
38+
39+
# this exists purely for testing purposes
40+
_urlopen = web_util.urlopen
6441

6542

6643
def copy_files_to_artifacts(src, artifacts_dir):
@@ -279,26 +256,25 @@ def copy_test_results(self, source, dest):
279256
reports = fs.join_path(source, "*_Test*.xml")
280257
copy_files_to_artifacts(reports, dest)
281258

282-
def create_buildgroup(self, opener, headers, url, group_name, group_type):
259+
def create_buildgroup(self, headers, url, group_name, group_type):
283260
data = {"newbuildgroup": group_name, "project": self.project, "type": group_type}
284261

285262
enc_data = json.dumps(data).encode("utf-8")
286263

287264
request = Request(url, data=enc_data, headers=headers)
288265

289-
response = opener.open(request, timeout=SPACK_CDASH_TIMEOUT)
290-
response_code = response.getcode()
291-
292-
if response_code not in [200, 201]:
293-
msg = f"Creating buildgroup failed (response code = {response_code})"
294-
tty.warn(msg)
266+
try:
267+
response_text = _urlopen(request, timeout=SPACK_CDASH_TIMEOUT).read()
268+
except OSError as e:
269+
tty.warn(f"Failed to create CDash buildgroup: {e}")
295270
return None
296271

297-
response_text = response.read()
298-
response_json = json.loads(response_text)
299-
build_group_id = response_json["id"]
300-
301-
return build_group_id
272+
try:
273+
response_json = json.loads(response_text)
274+
return response_json["id"]
275+
except (json.JSONDecodeError, KeyError) as e:
276+
tty.warn(f"Failed to parse CDash response: {e}")
277+
return None
302278

303279
def populate_buildgroup(self, job_names):
304280
url = f"{self.url}/api/v1/buildgroup.php"
@@ -308,16 +284,11 @@ def populate_buildgroup(self, job_names):
308284
"Content-Type": "application/json",
309285
}
310286

311-
opener = build_opener(HTTPHandler)
312-
313-
parent_group_id = self.create_buildgroup(opener, headers, url, self.build_group, "Daily")
314-
group_id = self.create_buildgroup(
315-
opener, headers, url, f"Latest {self.build_group}", "Latest"
316-
)
287+
parent_group_id = self.create_buildgroup(headers, url, self.build_group, "Daily")
288+
group_id = self.create_buildgroup(headers, url, f"Latest {self.build_group}", "Latest")
317289

318290
if not parent_group_id or not group_id:
319-
msg = f"Failed to create or retrieve buildgroups for {self.build_group}"
320-
tty.warn(msg)
291+
tty.warn(f"Failed to create or retrieve buildgroups for {self.build_group}")
321292
return
322293

323294
data = {
@@ -329,15 +300,12 @@ def populate_buildgroup(self, job_names):
329300

330301
enc_data = json.dumps(data).encode("utf-8")
331302

332-
request = Request(url, data=enc_data, headers=headers)
333-
request.get_method = lambda: "PUT"
303+
request = Request(url, data=enc_data, headers=headers, method="PUT")
334304

335-
response = opener.open(request, timeout=SPACK_CDASH_TIMEOUT)
336-
response_code = response.getcode()
337-
338-
if response_code != 200:
339-
msg = f"Error response code ({response_code}) in populate_buildgroup"
340-
tty.warn(msg)
305+
try:
306+
_urlopen(request, timeout=SPACK_CDASH_TIMEOUT)
307+
except OSError as e:
308+
tty.warn(f"Failed to populate CDash buildgroup: {e}")
341309

342310
def report_skipped(self, spec: spack.spec.Spec, report_dir: str, reason: Optional[str]):
343311
"""Explicitly report skipping testing of a spec (e.g., it's CI
@@ -735,9 +703,6 @@ def _apply_section(dest, src):
735703
for value in header.values():
736704
value = os.path.expandvars(value)
737705

738-
verify_ssl = mapping.get("verify_ssl", spack.config.get("config:verify_ssl", True))
739-
timeout = mapping.get("timeout", spack.config.get("config:connect_timeout", 1))
740-
741706
required = mapping.get("require", [])
742707
allowed = mapping.get("allow", [])
743708
ignored = mapping.get("ignore", [])
@@ -771,19 +736,15 @@ def job_query(job):
771736
endpoint_url._replace(query=query).geturl(), headers=header, method="GET"
772737
)
773738
try:
774-
response = _dyn_mapping_urlopener(
775-
request, verify_ssl=verify_ssl, timeout=timeout
776-
)
739+
response = _urlopen(request)
740+
config = json.load(response)
777741
except Exception as e:
778742
# For now just ignore any errors from dynamic mapping and continue
779743
# This is still experimental, and failures should not stop CI
780744
# from running normally
781-
tty.warn(f"Failed to fetch dynamic mapping for query:\n\t{query}")
782-
tty.warn(f"{e}")
745+
tty.warn(f"Failed to fetch dynamic mapping for query:\n\t{query}: {e}")
783746
continue
784747

785-
config = json.load(codecs.getreader("utf-8")(response))
786-
787748
# Strip ignore keys
788749
if ignored:
789750
for key in ignored:

0 commit comments

Comments
 (0)