Go: reloading configuration on the fly - take two

This is a follow up, and perhaps a more in depth coverage to the previous article Go: reloading configuration on the fly. Several commentators pointed out that perhaps the example wasn’t as consise or correct as it should be, or that it was in general confusing. This was my fault as I originally meant to write a very quick article on the subject using a very similified version of an application I was writing. As I went on writing the article I realised a simple version wasn’t going to do, and used perhaps some unrealistic samples which lead to confusion. Note: this article uses code from the previous article.

Generally; for most types of applications the examples in the previous article are enough. That is: the type of application that does not need continious read access to the configuration variable. Here is some very generalized code:

func main() {
  app := App{}
  app.Init()
  app.Start()
}

func signals() {
  for {
    select {
    case <-syscall.SIGHUP:
      app.Stop()
      app.Init()
      app.Start()
    }
  }
}

Providing absolute concurrent access

If your application is going to be reading from the configuration variable often, then it needs a getter method with read locking protection. This is to ensure that you get access to the variable only when it is completely initialized; and not when only half the data is there (if you access the variable while it is being updated).

Read (and write) locking is provided by the sync package’s sync.RWMutex struct. We can update the code from the previous article like so:

type Config struct {
  sync.RWMutex
  files []string
}

func (c *Config) Init() {
  c.Lock()
  defer c.Unlock()
  f, err := os.Open("config")
  if err != nil {
    log.Fatal(err)
  }
  c.files = make([]string, 0)
  scanner := bufio.NewScanner(f)
  for scanner.Scan() {
    c.files = append(c.files, scanner.Text())
  }
}

func (c *Config) GetFiles() []string {
  c.RLock()
  defer RUnlock()
  return c.files
}

ticker := time.NewTicker(time.Second * 1)
for {
  select {
  case <-ticker.C:
    fmt.Println("Files available:", config.GetFiles())
  }
}

Now our application can be absolutely sure that the configuration variable is always the latest version. The issue now is that any calls to GetFiles() will block while Init() is running, and if Init() is slow then your application now becomes slow or even unresponsive.

Sample & demo

Here is some sample code to see the points in action. Init() has been updated to take 2.5 seconds to complete, while we have two processes attempting to access the configuration variable every second. One process uses a read lock, while the other accesses the variable directly.

When that is running you will see that the process which is accessing the configuration variable directly is printing out a half initialized variable (not good!). The process using the read lock is consistantly printing out the initialized variable (good!), but it is also blocking and causing our application to slow down (not good!).

Back to the previous article

And the missed points, and hopefully something of a more consise version. We want absolute concurrent access but without the blocking for slow configuration loading.

This is achieved by loading the configuration data into a second configuration variable and then copying that to the application for it to use, and regularly read from. We’ll use the same sample demo as before, with the relevant alterations.

// Configuration struct
type C struct {
  sync.RWMutex
  i     int
  files []string
}


// Application struct
// The application needs access to the configuration data, so that is provided by a pointer to the struct above.
type A struct {
  sync.Mutex
  c *C
}

C has the same Init() method from before, additionally the application A needs a method (loadConfiguration()) to make the configuration available for itself.

The application’s configuration is dereferenced to make a copy of it and provide access to C’s initialization method away from the old version that application will continue to use.

func (a *A) loadConfiguration() {
  c := *a.c
  c.Init()
  // Update the application's configuration copy with the new data
  a.Lock()
  a.c = &c
  a.Unlock()
}

Starting our application has been adjusted slightly - A needs a pointer to C and then we can read in the configuration.

app := &A{
  c: &C{},
}
app.loadConfiguration()

The rest of the application’s code is the same as before.

The full program

Once this is running; you can see there are the same two processes as before. One accessing the variable via read locking and one which accesses the variable directly.

The assignment of the configuration data is now done in one extremely fast assignment operation by loadConfiguration() that we do not have to worry about long blocks when trying to read the data.

This time the read locking process does not suffer from blocking, and it always has access to some valid configuration data.

The process accessing the configuration variable directly now also always prints out the correct configuration data, but for that one in a ? chance of a clash, it is best to use the read locking method.

Adam Bell-Hanssen

maybe, someday .. just another code and ops guy

Oslo, Norway