@@ -234,7 +234,40 @@ pub const RunError = error{
234
234
FetchFailed ,
235
235
};
236
236
237
+ const RetryableRunError = RunError || error {
238
+ /// An error occurred, but it may be temporary (e.g. due to network conditions).
239
+ /// Also, we have not yet retried this fetch `retry_delays.len` many times.
240
+ /// No error was added to the bundle. Attempt the fetch again after a short delay.
241
+ FetchFailedRetryable ,
242
+ };
243
+
244
+ /// The delay between fetch attempts (after retryable errors) in ns.
245
+ /// The length of this list is the number of times a fetch will be retried.
246
+ const retry_delays = [_ ]u32 {
247
+ 200 * std .time .ns_per_ms ,
248
+ 800 * std .time .ns_per_ms ,
249
+ 4000 * std .time .ns_per_ms ,
250
+ };
251
+
237
252
pub fn run (f : * Fetch ) RunError ! void {
253
+ var attempt_count : u32 = 0 ;
254
+ while (true ) {
255
+ if (f .runInner (& attempt_count )) | _ | {
256
+ return ;
257
+ } else | err | switch (err ) {
258
+ error .FetchFailedRetryable = > {
259
+ // Since we'll be sleeping, make sure the progress is up-to-date.
260
+ f .prog_node .context .refresh ();
261
+ // The function which triggered this error has checked and updated attempt_count.
262
+ std .time .sleep (retry_delays [attempt_count - 1 ]);
263
+ continue ;
264
+ },
265
+ else = > | e | return e ,
266
+ }
267
+ }
268
+ }
269
+
270
+ fn runInner (f : * Fetch , attempt_count : * u32 ) RetryableRunError ! void {
238
271
const eb = & f .error_bundle ;
239
272
const arena = f .arena .allocator ();
240
273
const gpa = f .arena .child_allocator ;
@@ -278,12 +311,12 @@ pub fn run(f: *Fetch) RunError!void {
278
311
.path_or_url = > | path_or_url | {
279
312
if (fs .cwd ().openIterableDir (path_or_url , .{})) | dir | {
280
313
var resource : Resource = .{ .dir = dir };
281
- return runResource (f , path_or_url , & resource , null );
314
+ return runResource (f , path_or_url , & resource , null , attempt_count );
282
315
} else | dir_err | {
283
316
const file_err = if (dir_err == error .NotDir ) e : {
284
317
if (fs .cwd ().openFile (path_or_url , .{})) | file | {
285
318
var resource : Resource = .{ .file = file };
286
- return runResource (f , path_or_url , & resource , null );
319
+ return runResource (f , path_or_url , & resource , null , attempt_count );
287
320
} else | err | break :e err ;
288
321
} else dir_err ;
289
322
@@ -293,8 +326,9 @@ pub fn run(f: *Fetch) RunError!void {
293
326
.{ path_or_url , @errorName (file_err ), @errorName (uri_err ) },
294
327
));
295
328
};
296
- var resource = try f .initResource (uri );
297
- return runResource (f , uri .path , & resource , null );
329
+
330
+ var resource = try f .initResource (uri , attempt_count );
331
+ return runResource (f , uri .path , & resource , null , attempt_count );
298
332
}
299
333
},
300
334
};
@@ -330,8 +364,8 @@ pub fn run(f: *Fetch) RunError!void {
330
364
f .location_tok ,
331
365
try eb .printString ("invalid URI: {s}" , .{@errorName (err )}),
332
366
);
333
- var resource = try f .initResource (uri );
334
- return runResource (f , uri .path , & resource , remote .hash );
367
+ var resource = try f .initResource (uri , attempt_count );
368
+ return runResource (f , uri .path , & resource , remote .hash , attempt_count );
335
369
}
336
370
337
371
pub fn deinit (f : * Fetch ) void {
@@ -345,7 +379,8 @@ fn runResource(
345
379
uri_path : []const u8 ,
346
380
resource : * Resource ,
347
381
remote_hash : ? Manifest.MultiHashHexDigest ,
348
- ) RunError ! void {
382
+ attempt_count : * u32 ,
383
+ ) RetryableRunError ! void {
349
384
defer resource .deinit ();
350
385
const arena = f .arena .allocator ();
351
386
const eb = & f .error_bundle ;
@@ -371,7 +406,7 @@ fn runResource(
371
406
};
372
407
defer tmp_directory .handle .close ();
373
408
374
- try unpackResource (f , resource , uri_path , tmp_directory );
409
+ try unpackResource (f , resource , uri_path , tmp_directory , attempt_count );
375
410
376
411
// Load, parse, and validate the unpacked build.zig.zon file. It is allowed
377
412
// for the file to be missing, in which case this fetched package is
@@ -707,6 +742,14 @@ fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
707
742
return error .FetchFailed ;
708
743
}
709
744
745
+ fn failRetryable (f : * Fetch , msg_tok : std.zig.Ast.TokenIndex , comptime msg_format : []const u8 , msg_args : anytype , attempt_count : * u32 ) RetryableRunError {
746
+ if (attempt_count .* == retry_delays .len ) {
747
+ return f .fail (msg_tok , try f .error_bundle .printString (msg_format , msg_args ));
748
+ }
749
+ attempt_count .* += 1 ;
750
+ return error .FetchFailedRetryable ;
751
+ }
752
+
710
753
const Resource = union (enum ) {
711
754
file : fs.File ,
712
755
http_request : std.http.Client.Request ,
@@ -797,7 +840,7 @@ const FileType = enum {
797
840
}
798
841
};
799
842
800
- fn initResource (f : * Fetch , uri : std.Uri ) RunError ! Resource {
843
+ fn initResource (f : * Fetch , uri : std.Uri , attempt_count : * u32 ) RetryableRunError ! Resource {
801
844
const gpa = f .arena .child_allocator ;
802
845
const arena = f .arena .allocator ();
803
846
const eb = & f .error_bundle ;
@@ -819,31 +862,39 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
819
862
defer h .deinit ();
820
863
821
864
var req = http_client .request (.GET , uri , h , .{}) catch | err | {
822
- return f .fail (f .location_tok , try eb .printString (
865
+ return f .failRetryable (
866
+ f .location_tok ,
823
867
"unable to connect to server: {s}" ,
824
868
.{@errorName (err )},
825
- ));
869
+ attempt_count ,
870
+ );
826
871
};
827
872
errdefer req .deinit (); // releases more than memory
828
873
829
874
req .start (.{}) catch | err | {
830
- return f .fail (f .location_tok , try eb .printString (
875
+ return f .failRetryable (
876
+ f .location_tok ,
831
877
"HTTP request failed: {s}" ,
832
878
.{@errorName (err )},
833
- ));
879
+ attempt_count ,
880
+ );
834
881
};
835
882
req .wait () catch | err | {
836
- return f .fail (f .location_tok , try eb .printString (
883
+ return f .failRetryable (
884
+ f .location_tok ,
837
885
"invalid HTTP response: {s}" ,
838
886
.{@errorName (err )},
839
- ));
887
+ attempt_count ,
888
+ );
840
889
};
841
890
842
891
if (req .response .status != .ok ) {
843
- return f .fail (f .location_tok , try eb .printString (
892
+ return f .failRetryable (
893
+ f .location_tok ,
844
894
"bad HTTP response code: '{d} {s}'" ,
845
895
.{ @intFromEnum (req .response .status ), req .response .status .phrase () orelse "" },
846
- ));
896
+ attempt_count ,
897
+ );
847
898
}
848
899
849
900
return .{ .http_request = req };
@@ -859,16 +910,19 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
859
910
session .discoverCapabilities (gpa , & redirect_uri ) catch | err | switch (err ) {
860
911
error .Redirected = > {
861
912
defer gpa .free (redirect_uri );
913
+ // We got a valid response, so this wasn't a network issue, so this is not retryable.
862
914
return f .fail (f .location_tok , try eb .printString (
863
915
"repository moved to {s}" ,
864
916
.{redirect_uri },
865
917
));
866
918
},
867
919
else = > | e | {
868
- return f .fail (f .location_tok , try eb .printString (
920
+ return f .failRetryable (
921
+ f .location_tok ,
869
922
"unable to discover remote git server capabilities: {s}" ,
870
923
.{@errorName (e )},
871
- ));
924
+ attempt_count ,
925
+ );
872
926
},
873
927
};
874
928
@@ -883,17 +937,21 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
883
937
.ref_prefixes = &.{ want_ref , want_ref_head , want_ref_tag },
884
938
.include_peeled = true ,
885
939
}) catch | err | {
886
- return f .fail (f .location_tok , try eb .printString (
940
+ return f .failRetryable (
941
+ f .location_tok ,
887
942
"unable to list refs: {s}" ,
888
943
.{@errorName (err )},
889
- ));
944
+ attempt_count ,
945
+ );
890
946
};
891
947
defer ref_iterator .deinit ();
892
948
while (ref_iterator .next () catch | err | {
893
- return f .fail (f .location_tok , try eb .printString (
949
+ return f .failRetryable (
950
+ f .location_tok ,
894
951
"unable to iterate refs: {s}" ,
895
952
.{@errorName (err )},
896
- ));
953
+ attempt_count ,
954
+ );
897
955
}) | ref | {
898
956
if (std .mem .eql (u8 , ref .name , want_ref ) or
899
957
std .mem .eql (u8 , ref .name , want_ref_head ) or
@@ -902,6 +960,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
902
960
break :want_oid ref .peeled orelse ref .oid ;
903
961
}
904
962
}
963
+ // We successfully iterated the refs, so this wasn't a network issue, so this is not retryable.
905
964
return f .fail (f .location_tok , try eb .printString ("ref not found: {s}" , .{want_ref }));
906
965
};
907
966
if (uri .fragment == null ) {
@@ -925,10 +984,12 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
925
984
std .fmt .fmtSliceHexLower (& want_oid ),
926
985
}) catch unreachable ;
927
986
var fetch_stream = session .fetch (gpa , &.{& want_oid_buf }) catch | err | {
928
- return f .fail (f .location_tok , try eb .printString (
987
+ return f .failRetryable (
988
+ f .location_tok ,
929
989
"unable to create fetch stream: {s}" ,
930
990
.{@errorName (err )},
931
- ));
991
+ attempt_count ,
992
+ );
932
993
};
933
994
errdefer fetch_stream .deinit ();
934
995
@@ -949,7 +1010,8 @@ fn unpackResource(
949
1010
resource : * Resource ,
950
1011
uri_path : []const u8 ,
951
1012
tmp_directory : Cache.Directory ,
952
- ) RunError ! void {
1013
+ attempt_count : * u32 ,
1014
+ ) RetryableRunError ! void {
953
1015
const eb = & f .error_bundle ;
954
1016
const file_type = switch (resource .* ) {
955
1017
.file = > FileType .fromPath (uri_path ) orelse
@@ -1010,16 +1072,19 @@ fn unpackResource(
1010
1072
};
1011
1073
1012
1074
switch (file_type ) {
1013
- .tar = > try unpackTarball (f , tmp_directory .handle , resource .reader ()),
1014
- .@"tar.gz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .gzip ),
1015
- .@"tar.xz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .xz ),
1075
+ .tar = > try unpackTarball (f , tmp_directory .handle , resource .reader (), attempt_count ),
1076
+ .@"tar.gz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .gzip , attempt_count ),
1077
+ .@"tar.xz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .xz , attempt_count ),
1016
1078
.git_pack = > unpackGitPack (f , tmp_directory .handle , resource ) catch | err | switch (err ) {
1017
1079
error .FetchFailed = > return error .FetchFailed ,
1018
1080
error .OutOfMemory = > return error .OutOfMemory ,
1019
- else = > | e | return f .fail (f .location_tok , try eb .printString (
1081
+ // TODO: don't mark actual git errors as retryable. We're only interested in reader errors.
1082
+ else = > | e | return f .failRetryable (
1083
+ f .location_tok ,
1020
1084
"unable to unpack git files: {s}" ,
1021
1085
.{@errorName (e )},
1022
- )),
1086
+ attempt_count ,
1087
+ ),
1023
1088
},
1024
1089
}
1025
1090
}
@@ -1029,7 +1094,8 @@ fn unpackTarballCompressed(
1029
1094
out_dir : fs.Dir ,
1030
1095
resource : * Resource ,
1031
1096
comptime Compression : type ,
1032
- ) RunError ! void {
1097
+ attempt_count : * u32 ,
1098
+ ) RetryableRunError ! void {
1033
1099
const gpa = f .arena .child_allocator ;
1034
1100
const eb = & f .error_bundle ;
1035
1101
const reader = resource .reader ();
@@ -1043,10 +1109,10 @@ fn unpackTarballCompressed(
1043
1109
};
1044
1110
defer decompress .deinit ();
1045
1111
1046
- return unpackTarball (f , out_dir , decompress .reader ());
1112
+ return unpackTarball (f , out_dir , decompress .reader (), attempt_count );
1047
1113
}
1048
1114
1049
- fn unpackTarball (f : * Fetch , out_dir : fs.Dir , reader : anytype ) RunError ! void {
1115
+ fn unpackTarball (f : * Fetch , out_dir : fs.Dir , reader : anytype , attempt_count : * u32 ) RetryableRunError ! void {
1050
1116
const eb = & f .error_bundle ;
1051
1117
const gpa = f .arena .child_allocator ;
1052
1118
@@ -1063,10 +1129,15 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void {
1063
1129
// bit on Windows from the ACLs (see the isExecutable function).
1064
1130
.mode_mode = .ignore ,
1065
1131
.exclude_empty_directories = true ,
1066
- }) catch | err | return f .fail (f .location_tok , try eb .printString (
1067
- "unable to unpack tarball to temporary directory: {s}" ,
1068
- .{@errorName (err )},
1069
- ));
1132
+ }) catch | err | {
1133
+ // TODO: don't mark actual tar errors as retryable. We're only interested in reader errors.
1134
+ return f .failRetryable (
1135
+ f .location_tok ,
1136
+ "unable to unpack tarball to temporary directory: {s}" ,
1137
+ .{@errorName (err )},
1138
+ attempt_count ,
1139
+ );
1140
+ };
1070
1141
1071
1142
if (diagnostics .errors .items .len > 0 ) {
1072
1143
const notes_len : u32 = @intCast (diagnostics .errors .items .len );
@@ -1163,7 +1234,8 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void
1163
1234
},
1164
1235
}
1165
1236
}
1166
- return error .InvalidGitPack ;
1237
+ // Use FetchFailed since we already added an error.
1238
+ return error .FetchFailed ;
1167
1239
}
1168
1240
}
1169
1241
}
0 commit comments