Skip to content

Commit cf616ce

Browse files
committed
ReadMultiRange(): add retry support for 429/5xx responses
Add per-request CPLHTTPRetryContext to ReadMultiRange(), following the existing AdviseRead() pattern. Only failed requests are retried. Resolves #12933
1 parent 4e3c401 commit cf616ce

File tree

2 files changed

+264
-135
lines changed

2 files changed

+264
-135
lines changed

autotest/gcore/vsicurl.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,83 @@ def test_vsicurl_retry_codes_no_match(server):
955955
assert len(data) == 0
956956

957957

958+
###############################################################################
959+
# Test that ReadMultiRange retries on 429
960+
961+
962+
def test_vsicurl_readmultirange_retry(server):
963+
964+
gdal.VSICurlClearCache()
965+
966+
filesize = 262976
967+
968+
def serve_range(request):
969+
if "Range" not in request.headers:
970+
request.send_response(404)
971+
request.end_headers()
972+
return
973+
rng = request.headers["Range"][len("bytes=") :]
974+
start = int(rng.split("-")[0])
975+
end = int(rng.split("-")[1])
976+
request.protocol_version = "HTTP/1.1"
977+
request.send_response(206)
978+
request.send_header("Content-type", "application/octet-stream")
979+
request.send_header("Content-Range", "bytes %d-%d/%d" % (start, end, filesize))
980+
request.send_header("Content-Length", end - start + 1)
981+
request.send_header("Connection", "close")
982+
request.end_headers()
983+
with open("../gdrivers/data/utm.tif", "rb") as f:
984+
f.seek(start, 0)
985+
request.wfile.write(f.read(end - start + 1))
986+
987+
def serve_429(request):
988+
request.protocol_version = "HTTP/1.1"
989+
request.send_response(429)
990+
request.send_header("Connection", "close")
991+
request.end_headers()
992+
993+
handler = webserver.SequentialHandler()
994+
handler.add(
995+
"HEAD",
996+
"/readmultirange_retry.tif",
997+
200,
998+
{"Content-Length": "%d" % filesize},
999+
)
1000+
# GETs 1-3: header/IFD reads + first tile strip (all succeed)
1001+
for i in range(3):
1002+
handler.add("GET", "/readmultirange_retry.tif", custom_method=serve_range)
1003+
# GET 4: second tile strip -> 429
1004+
handler.add("GET", "/readmultirange_retry.tif", custom_method=serve_429)
1005+
# GETs 5-6: remaining tile strips succeed
1006+
for i in range(2):
1007+
handler.add("GET", "/readmultirange_retry.tif", custom_method=serve_range)
1008+
# GET 7: retry of the failed tile strip
1009+
handler.add("GET", "/readmultirange_retry.tif", custom_method=serve_range)
1010+
1011+
with webserver.install_http_handler(handler):
1012+
with gdaltest.config_options(
1013+
{
1014+
"GTIFF_DIRECT_IO": "YES",
1015+
"CPL_VSIL_CURL_ALLOWED_EXTENSIONS": ".tif",
1016+
"GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
1017+
"GDAL_HTTP_MAX_RETRY": "2",
1018+
"GDAL_HTTP_RETRY_DELAY": "0.01",
1019+
}
1020+
):
1021+
ds = gdal.Open(
1022+
"/vsicurl/http://localhost:%d/readmultirange_retry.tif" % server.port
1023+
)
1024+
assert ds is not None
1025+
subsampled_data = ds.ReadRaster(0, 0, 512, 32, 128, 4)
1026+
ds = None
1027+
assert subsampled_data is not None
1028+
ds = gdal.GetDriverByName("MEM").Create("", 128, 4)
1029+
ds.WriteRaster(0, 0, 128, 4, subsampled_data)
1030+
cs = ds.GetRasterBand(1).Checksum()
1031+
ds = None
1032+
assert cs == 6429
1033+
1034+
9581035
###############################################################################
9591036

9601037

0 commit comments

Comments
 (0)