reload config file on the fly

บางครั้งเวลาที่เราเขียนโปรแกรมมาใช้งานแล้วมีไฟล์คอนฟิก เอาไว้เก็บพวก url  หรือค่าต่างๆที่ต้องการให้เปลี่ยนไปตาม environment หรือจะด้วยเหตุผลอะไรก็แล้วแต่
หลายๆครั้งที่เราเขียนการโหลดข้อมูลเป็นแบบ lazy load แล้วมันก็จะจำค่านั้นไว้จนกว่าโปรแกรมจะหยุด
มีโจทย์มาอีกว่า ทำอย่างไรถึงจะ reload ค่าเหล่านั้นโดยไม่ต้อง stop/start โปรแกรมใหม่ วันนี้มีเทคนิคหนึ่งมานำเสนอครับ

เทคนิคที่ว่าคือการส่ง signal ครับ โดยเราจะใช้ signal ที่ชื่อว่า SIGUSR1 เรามาดูตัวอย่างโค้ดกันเลยดีกว่า

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/signal"
	"strconv"
	"syscall"

	yaml "gopkg.in/yaml.v2"
)

const (
	filename = "config.yml"
)

var conf config
var refresh = make(chan struct{})

func main() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGUSR1)
	go handleSIGUSR1(c)
	go reload()

	fmt.Printf("Reload yml file   : kill -SIGUSR1 %s\n", strconv.Itoa(os.Getpid()))

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGKILL)
	<-quit
}

type config struct {
	URL string `yaml:"url"`
}

func reload() {
	for range refresh {
		load()
	}
}

func load() error {
	var c config
	b, err := ioutil.ReadFile(filename)
	if err != nil {
		return err
	}
	err = yaml.Unmarshal(b, &c)
	if err != nil {
		return err
	}

	fmt.Println(c)

	conf = c
	return nil
}

func handleSIGUSR1(c chan os.Signal) {
	for {
		<-c
		fmt.Println("got signal SIGUSR1")
		refresh <- struct{}{}
	}
}

พอรันโปรแกรมจะพิมพ์ออกมาทำนองนี้ครับ

Reload yml file   : kill -SIGUSR1 3833

ตัวเลขด้านหลังจะเป็นเลขของ process id ของโปรแกรมเราเอง ซึ่งแต่ละคนจะได้ไม่เหมือนกันนะครับ

ทีนี้เราลองเปิดอีก terminal นึงแล้วเอาคำสั่ง kill มารันดูเลย มันจะพิมพ์ของใน config.yml ออกมา
หลังจากนั้นทดลองแก้ไขค่าใน config.yml แล้วสั่ง kill อีกที มันจะพิมพ์ค่าที่เปลี่ยนไปออกมาได้ครับ ตามตัวอย่างนี้

got signal SIGUSR1
{http://google.com}
got signal SIGUSR1
{http://facebook.com}

หวังว่าจะมีประโยชน์นะครับ