Go: reloading configuration on the fly - responding to HUP signal

Reloading your application’s configuration while it is running is a relatively easy task in Go. The only real worry is about providing concurrent access to your configuration object; and that is easily solved with the sync package’s sync.Mutex struct.

When it is embedded into a struct anonymously it provides the Lock() and Unlock() methods which will protect against concurrent access.

Our sample program consists of three core parts:

  • Configuration management
  • Main execution loop
  • Signal handling

Our demo program will list “files” that are available (for whatever purpose) to standard output. But it could instead use the list as part of a web server to provide an access list for files to be served, or a chat server that uses these files as room names. Whatever purpose the application serves, configuration is defined via a source, and the program needs to be able to reload this configuration while it is running.

We will be using a configuration file that is stored on the local file system; it is very simple format and consists of one entry per line.

Example configuration file

$ cat config
file1
file2

Configuration management

Create a struct to hold your configuration information and provide concurrent access methods (provided by sync.Mutex).

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

Define some initilization methods to the struct. The important thing is to provide concurrent access to it, and this is done by locking before, and then locking after when you write to the variable.

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())
  }
}

Main execution loop

This is the workhorse of our program, and it will print out the current file list every 5 seconds. In a proper application; here we will be using the list to serve the relevant files via the web, or provide access to the correct chat rooms. More intensive operations with the configuration variable, with many Go routines will need to use a RWMutex to provide read locking as well.

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

Note: access to config.files here is not concurrent safe (you will get a incomplete copy of it when reading at the same time as an update is happening), but it is used here purely for simplicities sake. It is unlikely your application will need to constantly read the configuration data in like this and will instead use it one time after it has been read in to start your main application. But you can make it concurrent safe by providing a getter method to the struct and wrapping around RLock() and RUnlock(). This however will block until the configuration reload has completed.

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

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())
  }
}

Signal handling

All that is left now is to monitor and respond to signals. This is easily done in a new Go routine; we listen for and respond to the HUP signal and reinitialize Config when receieved. The main execution loop continues on as normal, but is now working with the new configuration.

go func() {
  sigHUP := make(chan os.Signal, 1)
  signal.Notify(sigHUP, syscall.SIGHUP)
 
  for {
    select {
    case <-sigHUP:
      config.Init()
    }
  }
}()

The full program

Building, running and sending the HUP signal

$ go build sighup.go
$ chmod +x sighup
$ create config file
$ ./sighup

Update the configuration file:

$ update config file

Request the program reloads its configuration:

$ kill -s hup $(ps u | grep -v grep | grep sighup | awk '{print $2}')

Update from Enric:

$ pkill -HUP sighup

More in depth information

A follow up to this article has been posted: Go: reloading configuration on the fly take two. It covers more in depth the use of a read/write mutex, the blocking that will cause on a slow reload, and how to work around that blocking.

Adam Bell-Hanssen

maybe, someday .. just another code and ops guy

Oslo, Norway