Go Background Automatic Retry Function

Credits to Łukasz Rawa (https://unsplash.com/photos/_4NF4Jppx-c)

Recently I created a simple Golang automatic “retrier” that will do automatic retry on our desired struct method with configurable maximum attempt.

Summarizing the automatic retrier flow there are several things that we need to implement:

  1. We need to create a struct that implements the “retriable” interface, which consist of “Exec() error” method.
  2. Then we can create a new retrier function with object based on the previous struct, configured with our desired maximum attempt and logger if needed (by New command)
  3. Invoke the action to do the execution and automatic retry upon failing the Exec method (by Start() command)

If we’re drawing the mechanism at its simplest form, more or less it should be like this

Let’s try on Our Code!

We’re going to simulate a case in which we need to save a user activity record that takes 100 milliseconds to perform (simulating elapsed time needed by DB / API / any operation).

First let us create the struct and write the Exec() error method

package main
import (
...
)
type UserActivitySaver struct { UserEmail string Activity string}func (u *UserActivitySaver) Exec() error { err := SaveToDB(u.UserEmail, u.Activity) if err != nil { return err } return nil}func SaveToDB(email, activity string) error { fmt.Println("SAVING to DB for email", email, " with Activity", activity) time.Sleep(100 * time.Millisecond) if email == "user@test.com" { return errors.New("Mocking an error") } return nil}

if condition for user@test email is just for covering an example of “retrying” case later on this experiment.

Now let us write the main function

func main() {  uas := &UserActivitySaver{    UserEmail: "john@gmail.com",    Activity:  "Login Success",  }  r, err := retrier.New(uas, 5, func(err error) { log.Println("User Activity Saver error", err) })  if err != nil {    log.Fatal("ERR", err)  }  r.Start()  fmt.Println("Start Invocation Done")  time.Sleep(2 * time.Second)  fmt.Println("DONE")}

Take a note that the retrier New constructor receive three parameters:

  1. Object that implement Exec() error method (object pointer, must not be nil)
  2. Max retry attempt (must be greater than zero)
  3. Optional logger function (function that receive an error and does not returning), this can be nil if you don’t need it

r.Start() is for starting the invocation method

Running that script will output logs like this

time sleep 2 seconds just to make sure the main function still alive for the background go routine to finish its job (just a simplification)

Since we’re using john@gmail.com, we don’t get any errors and our action (Exec() method for uas) don’t get retried, success at first try. Let’s change the UserEmail to user@test.com, re-run the program again, and this is what we got

As we can see, it is being retried 5 times (max attempt) until it finally gave up.

Summary

This simple automatic retry mechanism is quite suitable for process that needs to be ran in the background (fire and forget) and need automatic retry if it fails in the process.

There is one enchancement that i still need to add though, where as it needs to delayed backoff before doing the next retry process.

If you’re wondering why the first parameter is a struct that implements Exec() error method, the answer is to prevent a closure variable when running the go routine, in a nutshell like the above example, all needed variables will be contained in the struct so it’s safe from variable race condition between your real program and the background retry execution (unless you’re putting a pointer inside it and use it globally).

Fellow Software Engineer