Skip to content

Commit 4120621

Browse files
committed
Added hot config reloading
This diff adds the ability to reload the config with a `kill -hup` on the process. If the config is not valid the old config will be used and an error will be logged for you.
1 parent 803029a commit 4120621

File tree

3 files changed

+85
-39
lines changed

3 files changed

+85
-39
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
vendor
2-
test
32
config.yaml
3+
git-sync

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module cgit.xrt0x.com/xrt0x/test
1+
module cgit.xrt0x.com/xrt0x/git-sync
22

33
require (
44
github.com/google/go-github/v21 v21.0.0

main.go

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import (
99
"io/ioutil"
1010
"net/http"
1111
"os"
12+
"os/signal"
1213
"path"
1314
"regexp"
1415
"strings"
1516
"sync"
17+
"syscall"
1618
"time"
1719

1820
// Sorry the naming here got kinda bad of note gconfig is for configuration
@@ -52,6 +54,11 @@ var (
5254
SshProtocolRegex, _ = regexp.Compile("^ssh://|.*:.*")
5355
)
5456

57+
var (
58+
globalConf = Config{}
59+
configLock = sync.RWMutex{}
60+
)
61+
5562
// Config is the parent level of our YAML data that
5663
// all other config should tie back into.
5764
type Config struct {
@@ -187,7 +194,6 @@ func (r GithubConfig) toRepos(c Config) []repo {
187194
Username: r.Username,
188195
CgitSection: r.Username,
189196
CgitOwner: r.Username,
190-
Description: *v.Description,
191197
}
192198

193199
// Need to add support here for using different kinds of urls but currently
@@ -503,8 +509,13 @@ func repoWorker(wg *sync.WaitGroup, rc <-chan repo) {
503509
}
504510

505511
// Feed the channel all the information that it needs
506-
func feedChannel(jobs chan repo, c Config, oneshot bool) {
512+
func feedChannel(jobs chan repo, oneshot bool) {
507513
for {
514+
// Get the latest config that might have been updated
515+
configLock.RLock()
516+
c := globalConf
517+
configLock.RUnlock()
518+
508519
for _, v := range c.Repos {
509520
jobs <- v.toRepo(c)
510521
}
@@ -532,6 +543,66 @@ func launchWorkers(workers int, wg *sync.WaitGroup, rc <-chan repo) {
532543
}
533544
}
534545

546+
// I'm not in love with what is going on here but in practice it should work just fine.
547+
// I think the one edge case is that this will make it so we can't validate a required
548+
// field that could be empty. I can't think of any reason I would need that though. So
549+
// what we are doing, is taking YAML, something that is half sane for human config and
550+
// populating the Config struct. We then turn it into JSON so we can pass it to the JSON
551+
// schema validator. Modern problems require modern solutions.
552+
func loadConfig(config string) (Config, error) {
553+
c := Config{}
554+
555+
// Load our config into memory and do some error checking
556+
dat, err := ioutil.ReadFile(config)
557+
if err != nil {
558+
return c, err
559+
}
560+
561+
err = yaml.Unmarshal(dat, &c)
562+
if err != nil {
563+
return c, err
564+
}
565+
566+
b, err := json.Marshal(c)
567+
if err != nil {
568+
return c, err
569+
}
570+
571+
compiler := jsonschema.NewCompiler()
572+
if err := compiler.AddResource("schema.json", strings.NewReader(schema)); err != nil {
573+
return c, err
574+
}
575+
576+
schema, err := compiler.Compile("schema.json")
577+
if err != nil {
578+
return c, err
579+
}
580+
581+
if err := schema.Validate(strings.NewReader(string(b))); err != nil {
582+
return c, err
583+
}
584+
585+
return c, nil
586+
}
587+
588+
func handleSignals(signals <-chan os.Signal, config string) {
589+
for signal := range signals {
590+
switch signal {
591+
case syscall.SIGHUP:
592+
log.Warn("Config reload requested")
593+
newConfig, err := loadConfig(config)
594+
if err != nil {
595+
log.Error("Config not reloaded :", err.Error())
596+
} else {
597+
configLock.RLock()
598+
globalConf = newConfig
599+
configLock.RUnlock()
600+
log.Warn("Config reload finished")
601+
}
602+
}
603+
}
604+
}
605+
535606
func main() {
536607
conf := flag.String("config", "config.yaml", "Config file to start the application with")
537608
workers := flag.Int("workers", 1, "The number of workers trying to update repos")
@@ -568,43 +639,13 @@ func main() {
568639
log.SetLevel(log.WarnLevel)
569640
}
570641

571-
// Load our config into memory and do some error checking
572-
dat, err := ioutil.ReadFile(*conf)
573-
if err != nil {
574-
log.Fatal(err)
575-
}
576-
577-
// I'm not in love with what is going on here but in practice it should work just fine.
578-
// I think the one edge case is that this will make it so we can't validate a required
579-
// field that could be empty. I can't think of any reason I would need that though. So
580-
// what we are doing, is taking YAML, something that is half sane for human config and
581-
// populating the Config struct. We then turn it into JSON so we can pass it to the JSON
582-
// schema validator. Modern problems require modern solutions.
583-
config := Config{}
584-
err = yaml.Unmarshal(dat, &config)
585-
if err != nil {
586-
log.Fatal(err)
587-
}
588-
589-
b, err := json.Marshal(config)
642+
config, err := loadConfig(*conf)
590643
if err != nil {
591-
fmt.Println(err)
592-
return
644+
log.Fatal(err.Error())
593645
}
594646

595-
compiler := jsonschema.NewCompiler()
596-
if err := compiler.AddResource("schema.json", strings.NewReader(schema)); err != nil {
597-
log.Fatal(err)
598-
}
599-
600-
schema, err := compiler.Compile("schema.json")
601-
if err != nil {
602-
log.Fatal(err)
603-
}
604-
605-
if err := schema.Validate(strings.NewReader(string(b))); err != nil {
606-
log.Fatal(err)
607-
}
647+
// Set the global conf no mutex is needed at this time
648+
globalConf = config
608649

609650
// If we have gotten this far and not failed we think it's all good
610651
log.Info("Configuration has been validated")
@@ -621,9 +662,14 @@ func main() {
621662
// as a one time service so you can use it in a cron if desired
622663
var wg sync.WaitGroup
623664
queue := make(chan repo, *workers*2)
624-
go feedChannel(queue, config, *oneshot)
665+
go feedChannel(queue, *oneshot)
625666
launchWorkers(*workers, &wg, queue)
626667

668+
// Handle hot config reloads on SIGHUP
669+
signals := make(chan os.Signal, 1)
670+
signal.Notify(signals, syscall.SIGHUP)
671+
go handleSignals(signals, *conf)
672+
627673
// Hang for now but later this should do some checking for
628674
// signals that may be sent to the processes as well as managing
629675
// when the queue should have jobs pushed onto it again

0 commit comments

Comments
 (0)