@@ -234,7 +234,44 @@ 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
+ // The function which triggered this error should have checked and updated attempt_count.
260
+ assert (attempt_count > 0 );
261
+ assert (attempt_count <= retry_delays .len );
262
+
263
+ // Since we'll be sleeping, make sure the progress is up-to-date.
264
+ f .prog_node .context .refresh ();
265
+
266
+ std .time .sleep (retry_delays [attempt_count - 1 ]);
267
+ continue ;
268
+ },
269
+ else = > | e | return e ,
270
+ }
271
+ }
272
+ }
273
+
274
+ fn runInner (f : * Fetch , attempt_count : * u32 ) RetryableRunError ! void {
238
275
const eb = & f .error_bundle ;
239
276
const arena = f .arena .allocator ();
240
277
const gpa = f .arena .child_allocator ;
@@ -278,12 +315,12 @@ pub fn run(f: *Fetch) RunError!void {
278
315
.path_or_url = > | path_or_url | {
279
316
if (fs .cwd ().openIterableDir (path_or_url , .{})) | dir | {
280
317
var resource : Resource = .{ .dir = dir };
281
- return runResource (f , path_or_url , & resource , null );
318
+ return runResource (f , path_or_url , & resource , null , attempt_count );
282
319
} else | dir_err | {
283
320
const file_err = if (dir_err == error .NotDir ) e : {
284
321
if (fs .cwd ().openFile (path_or_url , .{})) | file | {
285
322
var resource : Resource = .{ .file = file };
286
- return runResource (f , path_or_url , & resource , null );
323
+ return runResource (f , path_or_url , & resource , null , attempt_count );
287
324
} else | err | break :e err ;
288
325
} else dir_err ;
289
326
@@ -293,8 +330,9 @@ pub fn run(f: *Fetch) RunError!void {
293
330
.{ path_or_url , @errorName (file_err ), @errorName (uri_err ) },
294
331
));
295
332
};
296
- var resource = try f .initResource (uri );
297
- return runResource (f , uri .path , & resource , null );
333
+
334
+ var resource = try f .initResource (uri , attempt_count );
335
+ return runResource (f , uri .path , & resource , null , attempt_count );
298
336
}
299
337
},
300
338
};
@@ -330,8 +368,8 @@ pub fn run(f: *Fetch) RunError!void {
330
368
f .location_tok ,
331
369
try eb .printString ("invalid URI: {s}" , .{@errorName (err )}),
332
370
);
333
- var resource = try f .initResource (uri );
334
- return runResource (f , uri .path , & resource , remote .hash );
371
+ var resource = try f .initResource (uri , attempt_count );
372
+ return runResource (f , uri .path , & resource , remote .hash , attempt_count );
335
373
}
336
374
337
375
pub fn deinit (f : * Fetch ) void {
@@ -345,7 +383,8 @@ fn runResource(
345
383
uri_path : []const u8 ,
346
384
resource : * Resource ,
347
385
remote_hash : ? Manifest.MultiHashHexDigest ,
348
- ) RunError ! void {
386
+ attempt_count : * u32 ,
387
+ ) RetryableRunError ! void {
349
388
defer resource .deinit ();
350
389
const arena = f .arena .allocator ();
351
390
const eb = & f .error_bundle ;
@@ -371,7 +410,7 @@ fn runResource(
371
410
};
372
411
defer tmp_directory .handle .close ();
373
412
374
- try unpackResource (f , resource , uri_path , tmp_directory );
413
+ try unpackResource (f , resource , uri_path , tmp_directory , attempt_count );
375
414
376
415
// Load, parse, and validate the unpacked build.zig.zon file. It is allowed
377
416
// for the file to be missing, in which case this fetched package is
@@ -707,6 +746,14 @@ fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
707
746
return error .FetchFailed ;
708
747
}
709
748
749
+ fn failRetryable (f : * Fetch , msg_tok : std.zig.Ast.TokenIndex , comptime msg_format : []const u8 , msg_args : anytype , attempt_count : * u32 ) RetryableRunError {
750
+ if (attempt_count .* == retry_delays .len ) {
751
+ return f .fail (msg_tok , try f .error_bundle .printString (msg_format , msg_args ));
752
+ }
753
+ attempt_count .* += 1 ;
754
+ return error .FetchFailedRetryable ;
755
+ }
756
+
710
757
const Resource = union (enum ) {
711
758
file : fs.File ,
712
759
http_request : std.http.Client.Request ,
@@ -797,7 +844,7 @@ const FileType = enum {
797
844
}
798
845
};
799
846
800
- fn initResource (f : * Fetch , uri : std.Uri ) RunError ! Resource {
847
+ fn initResource (f : * Fetch , uri : std.Uri , attempt_count : * u32 ) RetryableRunError ! Resource {
801
848
const gpa = f .arena .child_allocator ;
802
849
const arena = f .arena .allocator ();
803
850
const eb = & f .error_bundle ;
@@ -819,31 +866,39 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
819
866
defer h .deinit ();
820
867
821
868
var req = http_client .request (.GET , uri , h , .{}) catch | err | {
822
- return f .fail (f .location_tok , try eb .printString (
869
+ return f .failRetryable (
870
+ f .location_tok ,
823
871
"unable to connect to server: {s}" ,
824
872
.{@errorName (err )},
825
- ));
873
+ attempt_count ,
874
+ );
826
875
};
827
876
errdefer req .deinit (); // releases more than memory
828
877
829
878
req .start (.{}) catch | err | {
830
- return f .fail (f .location_tok , try eb .printString (
879
+ return f .failRetryable (
880
+ f .location_tok ,
831
881
"HTTP request failed: {s}" ,
832
882
.{@errorName (err )},
833
- ));
883
+ attempt_count ,
884
+ );
834
885
};
835
886
req .wait () catch | err | {
836
- return f .fail (f .location_tok , try eb .printString (
887
+ return f .failRetryable (
888
+ f .location_tok ,
837
889
"invalid HTTP response: {s}" ,
838
890
.{@errorName (err )},
839
- ));
891
+ attempt_count ,
892
+ );
840
893
};
841
894
842
895
if (req .response .status != .ok ) {
843
- return f .fail (f .location_tok , try eb .printString (
896
+ return f .failRetryable (
897
+ f .location_tok ,
844
898
"bad HTTP response code: '{d} {s}'" ,
845
899
.{ @intFromEnum (req .response .status ), req .response .status .phrase () orelse "" },
846
- ));
900
+ attempt_count ,
901
+ );
847
902
}
848
903
849
904
return .{ .http_request = req };
@@ -859,16 +914,19 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
859
914
session .discoverCapabilities (gpa , & redirect_uri ) catch | err | switch (err ) {
860
915
error .Redirected = > {
861
916
defer gpa .free (redirect_uri );
917
+ // We got a valid response, so this wasn't a network issue, so this is not retryable.
862
918
return f .fail (f .location_tok , try eb .printString (
863
919
"repository moved to {s}" ,
864
920
.{redirect_uri },
865
921
));
866
922
},
867
923
else = > | e | {
868
- return f .fail (f .location_tok , try eb .printString (
924
+ return f .failRetryable (
925
+ f .location_tok ,
869
926
"unable to discover remote git server capabilities: {s}" ,
870
927
.{@errorName (e )},
871
- ));
928
+ attempt_count ,
929
+ );
872
930
},
873
931
};
874
932
@@ -883,17 +941,21 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
883
941
.ref_prefixes = &.{ want_ref , want_ref_head , want_ref_tag },
884
942
.include_peeled = true ,
885
943
}) catch | err | {
886
- return f .fail (f .location_tok , try eb .printString (
944
+ return f .failRetryable (
945
+ f .location_tok ,
887
946
"unable to list refs: {s}" ,
888
947
.{@errorName (err )},
889
- ));
948
+ attempt_count ,
949
+ );
890
950
};
891
951
defer ref_iterator .deinit ();
892
952
while (ref_iterator .next () catch | err | {
893
- return f .fail (f .location_tok , try eb .printString (
953
+ return f .failRetryable (
954
+ f .location_tok ,
894
955
"unable to iterate refs: {s}" ,
895
956
.{@errorName (err )},
896
- ));
957
+ attempt_count ,
958
+ );
897
959
}) | ref | {
898
960
if (std .mem .eql (u8 , ref .name , want_ref ) or
899
961
std .mem .eql (u8 , ref .name , want_ref_head ) or
@@ -902,6 +964,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
902
964
break :want_oid ref .peeled orelse ref .oid ;
903
965
}
904
966
}
967
+ // We successfully iterated the refs, so this wasn't a network issue, so this is not retryable.
905
968
return f .fail (f .location_tok , try eb .printString ("ref not found: {s}" , .{want_ref }));
906
969
};
907
970
if (uri .fragment == null ) {
@@ -925,10 +988,12 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
925
988
std .fmt .fmtSliceHexLower (& want_oid ),
926
989
}) catch unreachable ;
927
990
var fetch_stream = session .fetch (gpa , &.{& want_oid_buf }) catch | err | {
928
- return f .fail (f .location_tok , try eb .printString (
991
+ return f .failRetryable (
992
+ f .location_tok ,
929
993
"unable to create fetch stream: {s}" ,
930
994
.{@errorName (err )},
931
- ));
995
+ attempt_count ,
996
+ );
932
997
};
933
998
errdefer fetch_stream .deinit ();
934
999
@@ -949,7 +1014,8 @@ fn unpackResource(
949
1014
resource : * Resource ,
950
1015
uri_path : []const u8 ,
951
1016
tmp_directory : Cache.Directory ,
952
- ) RunError ! void {
1017
+ attempt_count : * u32 ,
1018
+ ) RetryableRunError ! void {
953
1019
const eb = & f .error_bundle ;
954
1020
const file_type = switch (resource .* ) {
955
1021
.file = > FileType .fromPath (uri_path ) orelse
@@ -1010,16 +1076,19 @@ fn unpackResource(
1010
1076
};
1011
1077
1012
1078
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 ),
1079
+ .tar = > try unpackTarball (f , tmp_directory .handle , resource .reader (), attempt_count ),
1080
+ .@"tar.gz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .gzip , attempt_count ),
1081
+ .@"tar.xz" = > try unpackTarballCompressed (f , tmp_directory .handle , resource , std .compress .xz , attempt_count ),
1016
1082
.git_pack = > unpackGitPack (f , tmp_directory .handle , resource ) catch | err | switch (err ) {
1017
1083
error .FetchFailed = > return error .FetchFailed ,
1018
1084
error .OutOfMemory = > return error .OutOfMemory ,
1019
- else = > | e | return f .fail (f .location_tok , try eb .printString (
1085
+ // TODO: don't mark actual git errors as retryable. We're only interested in reader errors.
1086
+ else = > | e | return f .failRetryable (
1087
+ f .location_tok ,
1020
1088
"unable to unpack git files: {s}" ,
1021
1089
.{@errorName (e )},
1022
- )),
1090
+ attempt_count ,
1091
+ ),
1023
1092
},
1024
1093
}
1025
1094
}
@@ -1029,7 +1098,8 @@ fn unpackTarballCompressed(
1029
1098
out_dir : fs.Dir ,
1030
1099
resource : * Resource ,
1031
1100
comptime Compression : type ,
1032
- ) RunError ! void {
1101
+ attempt_count : * u32 ,
1102
+ ) RetryableRunError ! void {
1033
1103
const gpa = f .arena .child_allocator ;
1034
1104
const eb = & f .error_bundle ;
1035
1105
const reader = resource .reader ();
@@ -1043,10 +1113,10 @@ fn unpackTarballCompressed(
1043
1113
};
1044
1114
defer decompress .deinit ();
1045
1115
1046
- return unpackTarball (f , out_dir , decompress .reader ());
1116
+ return unpackTarball (f , out_dir , decompress .reader (), attempt_count );
1047
1117
}
1048
1118
1049
- fn unpackTarball (f : * Fetch , out_dir : fs.Dir , reader : anytype ) RunError ! void {
1119
+ fn unpackTarball (f : * Fetch , out_dir : fs.Dir , reader : anytype , attempt_count : * u32 ) RetryableRunError ! void {
1050
1120
const eb = & f .error_bundle ;
1051
1121
const gpa = f .arena .child_allocator ;
1052
1122
@@ -1063,10 +1133,15 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void {
1063
1133
// bit on Windows from the ACLs (see the isExecutable function).
1064
1134
.mode_mode = .ignore ,
1065
1135
.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
- ));
1136
+ }) catch | err | {
1137
+ // TODO: don't mark actual tar errors as retryable. We're only interested in reader errors.
1138
+ return f .failRetryable (
1139
+ f .location_tok ,
1140
+ "unable to unpack tarball to temporary directory: {s}" ,
1141
+ .{@errorName (err )},
1142
+ attempt_count ,
1143
+ );
1144
+ };
1070
1145
1071
1146
if (diagnostics .errors .items .len > 0 ) {
1072
1147
const notes_len : u32 = @intCast (diagnostics .errors .items .len );
@@ -1163,7 +1238,8 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void
1163
1238
},
1164
1239
}
1165
1240
}
1166
- return error .InvalidGitPack ;
1241
+ // Use FetchFailed since we already added an error.
1242
+ return error .FetchFailed ;
1167
1243
}
1168
1244
}
1169
1245
}
0 commit comments