The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

I wish Go had a retry block

Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 0eb26108 a1841325

+152 -1
+2 -1
.vscode/extensions.json
··· 8 8 "denoland.vscode-deno", 9 9 "bradlc.vscode-tailwindcss", 10 10 "ronnidc.nunjucks", 11 - "streetsidesoftware.code-spell-checker" 11 + "streetsidesoftware.code-spell-checker", 12 + "pbkit.vscode-pbkit" 12 13 ] 13 14 }
+150
lume/src/blog/2024/retry-block.mdx
··· 1 + --- 2 + title: I wish Go had a retry block 3 + date: 2024-02-11 4 + hero: 5 + ai: iPhone 13 Pro, Photo by Xe Iaso 6 + file: sf-skyline 7 + prompt: A picture of the amazingly blue sky near SFO. 8 + --- 9 + 10 + I kinda wish that Go had some kind of language-level construct for "an action that is composed of multiple parts that can fail, and when one fails in a non-permanent way, then the program will wait for some time before trying again". This would prevent me from having to write code like this in XeDN using [this backoff package that I'm probably going to rewrite](https://pkg.go.dev/github.com/cenkalti/backoff/v4): 11 + 12 + ```go 13 + pong, err := backoff.RetryWithData[*pb.Echo](func() (*pb.Echo, error) { 14 + return client.Ping(ctx, &pb.Echo{Nonce: id}) 15 + }, bo) 16 + if err != nil { 17 + slog.Error("cannot ping machine", "err", err) 18 + http.Error(w, err.Error(), http.StatusInternalServerError) 19 + return 20 + } 21 + ``` 22 + 23 + <XeblogConv name="Cadey" mood="coffee"> 24 + Oh my god, it's so bad to write code like this with voice control. You end up saying things like this: 25 + 26 + > word pong swipe oops to be smashed back off over dot hammer retry with data over square asterisk of type p b dot echo r square args state funk args go right space args asterisk of type p b dot echo swipe error r paren brack pour this 27 + > 28 + > state return word client dot hammer ping over args cats swipe amp of type p b dot echo brack hammer nonce over colon sit drum r brack r paren pour this 29 + > 30 + > r brack swipe bat odd r paren 31 + > 32 + > [fucking hell](https://github.com/Xe/invocations/blob/b056ba09e3475ac9d12835090081bc569a26b0b9/languages/go/go.talon#L14-L15) 33 + > 34 + > slog error with error cannot ping machine pour this 35 + > 36 + > of type h t t p dot error over args whale swipe oops dot hammer error over args go right swipe of type h t t p dot status internal server error over pour this 37 + > 38 + > state return 39 + 40 + It is suffering. If you've never had to do this full time, you don't understand the suffering that you have to endure. I wonder if this is why my husband thinks that my voice coding is some kind of demonic summoning ritual. 41 + 42 + </XeblogConv> 43 + 44 + You can kinda see what is going on here, I'm trying to make a gRPC connection and then run a `Ping` method on it, but there's some absolutely atrocious abuse of the programming language in the process. This really feels like there's some room for monads here, where each fallible step is taken as a chunk that returns an `error` with a method like: 45 + 46 + ```go 47 + type PermanentError interface { 48 + error 49 + Permanent() bool 50 + } 51 + ``` 52 + 53 + If the method `Permanent()` is defined and returns `true`, the pipeline aborts from there and an error handler will then run. Ideally, I'd love to get something that looks like this (with better syntax that I didn't blatantly steal from Haskell `do` syntax, ofc): 54 + 55 + ```go 56 + retry { 57 + conn <- grpc.DialContext(ctx, addr, 58 + grpc.WithTransportCredentials(insecure.NewCredentials()), 59 + grpc.WithMaxMsgSize(chonkiness), 60 + ) 61 + pong <- client.Ping(ctx, &pb.Echo{Nonce: id}) 62 + variants <- client.Upload(ctx, &pb.UploadReq{FileName: "foo.jpg", Data: data, Folder: "bar"}) 63 + // do something with variants 64 + } unless err { 65 + slog.Error("can't upload image", "err", err) 66 + http.Error(w, err.Error(), http.StatusInternalServerError) 67 + return 68 + } 69 + ``` 70 + 71 + <XeblogConv name="Aoi" mood="wut"> 72 + Isn't that just a nerfed `try`/`catch` with extra steps? 73 + </XeblogConv> 74 + 75 + <XeblogConv name="Cadey" mood="aha"> 76 + No, not really, the main difference here is that every step still has `error` 77 + values, but a lot of the difference is in how it would be handled by the 78 + compiler and runtime. Imagine that block getting compiled to something like 79 + this: 80 + </XeblogConv> 81 + 82 + ```go 83 + var conn *grpc.ClientConn 84 + var err error 85 + done := false 86 + interval := 50 * time.Millisecond 87 + for !done { 88 + conn, err = grpc.DialContext(ctx, addr, 89 + grpc.WithTransportCredentials(insecure.NewCredentials()), 90 + grpc.WithMaxMsgSize(chonkiness), 91 + ) 92 + if err != nil { 93 + perr, ok := err.(PermanentError) 94 + if ok && perr.Permanent() { 95 + slog.Error("can't upload image", "err", err) 96 + http.Error(w, err.Error(), http.StatusInternalServerError) 97 + return 98 + } 99 + 100 + t := time.NewTicker(interval) 101 + 102 + select { 103 + case <-ctx.Done(): 104 + t.Stop() 105 + slog.Error("can't upload image", "err", ctx.Error()) 106 + http.Error(w, err.Error(), http.StatusInternalServerError) 107 + return 108 + case <-t.C: 109 + t.Stop() 110 + interval = interval * 2 111 + } 112 + } else { 113 + done = true 114 + } 115 + } 116 + if err != nil { 117 + slog.Error("can't upload image", "err", ctx.Error()) 118 + http.Error(w, err.Error(), http.StatusInternalServerError) 119 + return 120 + } 121 + ``` 122 + 123 + <XeblogConv name="Aoi" mood="grin"> 124 + Still looks like a nerfed `try`/`catch` with extra steps to me, but I see 125 + where you're coming from. 126 + </XeblogConv> 127 + 128 + But for every step of the pipeline. The added benefits of exponential backoff being the default means that software that use `retry` blocks will be _instantly_ robust against transient failures. This would make software more reliable for everyone with little additional effort. 129 + 130 + The main downside is that we would need to have custom error types expose a `Permanent` method and potentially extra methods in package `fmt` for constructing anonymous permanent errors. This would make it easy to use, with something like: 131 + 132 + ```go 133 + return nil, fmt.PermanentErrorf("flymachines: server returned status code %d", resp.StatusCode) 134 + ``` 135 + 136 + I feel something like this needs to be a language-level construct because this is a very common pattern across tools and requires you to do a lot of annoying fiddly code that makes the code a lot harder to understand. Making it at the language level would also let each individual step that can fail be isolated and retried for you, reducing cognitive complexity. 137 + 138 + It would be cool if `retry` blocks automatically detected the scope-level `ctx` value, injecting context-awareness into the backoff retries too so that you don't need to add that manually like you do with the backoff package: 139 + 140 + ```go 141 + backoff.Retry(func() error { 142 + switch { 143 + case <-ctx.Done(): 144 + return ctx.Error() 145 + default: 146 + } 147 + 148 + return somethingThatCanFail() 149 + }, bo) 150 + ```