Skip to content

Commit

Permalink
Reload config on update
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotpeele committed Oct 16, 2020
1 parent e3ba713 commit 6abd3b7
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 76 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ COPY . /go/src/github.com/hound-search/hound
COPY default-config.json /data/config.json

RUN apk update \
&& apk add go git subversion libc-dev mercurial bzr openssh \
&& apk add go git subversion libc-dev mercurial openssh \
&& go get github.com/fsnotify/fsnotify \
&& go install github.com/hound-search/hound/cmds/houndd \
&& apk del go \
&& rm -f /var/cache/apk/* \
Expand Down
102 changes: 36 additions & 66 deletions cmds/houndd/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package main

import (
"encoding/json"
"flag"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
Expand All @@ -13,10 +11,9 @@ import (
"strings"
"syscall"

"github.com/hound-search/hound/api"
"github.com/fsnotify/fsnotify"
"github.com/hound-search/hound/config"
"github.com/hound-search/hound/searcher"
"github.com/hound-search/hound/ui"
"github.com/hound-search/hound/web"
)

Expand All @@ -29,30 +26,30 @@ var (
basepath = filepath.Dir(b)
)

func makeSearchers(cfg *config.Config) (map[string]*searcher.Searcher, bool, error) {
func makeSearchers(cfg *config.Config, searchers map[string]*searcher.Searcher) (bool, error) {
// Ensure we have a dbpath
if _, err := os.Stat(cfg.DbPath); err != nil {
if err := os.MkdirAll(cfg.DbPath, os.ModePerm); err != nil {
return nil, false, err
return false, err
}
}

searchers, errs, err := searcher.MakeAll(cfg)
errs, err := searcher.MakeAll(cfg, searchers)
if err != nil {
return nil, false, err
return false, err
}

if len(errs) > 0 {
// NOTE: This mutates the original config so the repos
// are not even seen by other code paths.
for name, _ := range errs {
for name := range errs {
delete(cfg.Repos, name)
}

return searchers, false, nil
return false, nil
}

return searchers, true, nil
return true, nil
}

func handleShutdown(shutdownCh <-chan os.Signal, searchers map[string]*searcher.Searcher) {
Expand All @@ -77,42 +74,6 @@ func registerShutdownSignal() <-chan os.Signal {
return shutdownCh
}

func makeTemplateData(cfg *config.Config) (interface{}, error) {
var data struct {
ReposAsJson string
}

res := map[string]*config.Repo{}
for name, repo := range cfg.Repos {
res[name] = repo
}

b, err := json.Marshal(res)
if err != nil {
return nil, err
}

data.ReposAsJson = string(b)
return &data, nil
}

func runHttp(
addr string,
dev bool,
cfg *config.Config,
idx map[string]*searcher.Searcher) error {
m := http.DefaultServeMux

h, err := ui.Content(dev, cfg)
if err != nil {
return err
}

m.Handle("/", h)
api.Setup(m, idx)
return http.ListenAndServe(addr, m)
}

func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
info_log = log.New(os.Stdout, "", log.LstdFlags)
Expand All @@ -124,29 +85,39 @@ func main() {

flag.Parse()

idx := make(map[string]*searcher.Searcher)

var cfg config.Config
if err := cfg.LoadFromFile(*flagConf); err != nil {
panic(err)

loadConfig := func() {
if err := cfg.LoadFromFile(*flagConf); err != nil {
panic(err)
}
// It's not safe to be killed during makeSearchers, so register the
// shutdown signal here and defer processing it until we are ready.
shutdownCh := registerShutdownSignal()
ok, err := makeSearchers(&cfg, idx)
if err != nil {
log.Panic(err)
}
if !ok {
info_log.Println("Some repos failed to index, see output above")
} else {
info_log.Println("All indexes built!")
}
handleShutdown(shutdownCh, idx)
}
loadConfig()

// watch for config file changes
configWatcher := config.NewWatcher(*flagConf)
configWatcher.OnChange(func(fsnotify.Event) {
loadConfig()
})

// Start the web server on a background routine.
ws := web.Start(&cfg, *flagAddr, *flagDev)

// It's not safe to be killed during makeSearchers, so register the
// shutdown signal here and defer processing it until we are ready.
shutdownCh := registerShutdownSignal()
idx, ok, err := makeSearchers(&cfg)
if err != nil {
log.Panic(err)
}
if !ok {
info_log.Println("Some repos failed to index, see output above")
} else {
info_log.Println("All indexes built!")
}

handleShutdown(shutdownCh, idx)

host := *flagAddr
if strings.HasPrefix(host, ":") {
host = "localhost" + host
Expand All @@ -158,8 +129,7 @@ func main() {
webpack.Dir = basepath + "/../../"
webpack.Stdout = os.Stdout
webpack.Stderr = os.Stderr
err = webpack.Start()
if err != nil {
if err := webpack.Start(); err != nil {
error_log.Println(err)
}
}
Expand Down
79 changes: 79 additions & 0 deletions config/watcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package config

import (
"log"
"sync"

"github.com/fsnotify/fsnotify"
)

// WatcherListenerFunc defines the signature for listner functions
type WatcherListenerFunc func(fsnotify.Event)

// Watcher watches for configuration updates and provides hooks for
// triggering post events
type Watcher struct {
listeners []WatcherListenerFunc
}

// NewWatcher returns a new file watcher
func NewWatcher(cfgPath string) *Watcher {
log.Printf("setting up watcher for %s", cfgPath)
w := Watcher{}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Panic(err)
}
defer watcher.Close()
// Event listener setup
eventWG := sync.WaitGroup{}
eventWG.Add(1)
go func() {
defer eventWG.Done()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
// events channel is closed
log.Printf("error: events channel is closed\n")
return
}
// only trigger on creates and writes of the watched config file
if event.Name == cfgPath && event.Op&fsnotify.Write == fsnotify.Write {
log.Printf("change in config file (%s) detected\n", cfgPath)
for _, listener := range w.listeners {
listener(event)
}
}
case err, ok := <-watcher.Errors:
if !ok {
// errors channel is closed
log.Printf("error: errors channel is closed\n")
return
}
log.Println("error:", err)
return
}
}
}()
// add config file
if err := watcher.Add(cfgPath); err != nil {
log.Fatalf("failed to watch %s", cfgPath)
}
// setup is complete
wg.Done()
// wait for the event listener to complete before exiting
eventWG.Wait()
}()
// wait for watcher setup to complete
wg.Wait()
return &w
}

// OnChange registers a listener function to be called if a file changes
func (w *Watcher) OnChange(listener WatcherListenerFunc) {
w.listeners = append(w.listeners, listener)
}
20 changes: 14 additions & 6 deletions searcher/searcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,24 +282,32 @@ func init() {
// occurred and no other return values are valid. If an error occurs that is specific
// to a particular searcher, that searcher will not be present in the searcher map and
// will have an error entry in the error map.
func MakeAll(cfg *config.Config) (map[string]*Searcher, map[string]error, error) {
func MakeAll(cfg *config.Config, searchers map[string]*Searcher) (map[string]error, error) {
errs := map[string]error{}
searchers := map[string]*Searcher{}

refs, err := findExistingRefs(cfg.DbPath)
if err != nil {
return nil, nil, err
return nil, err
}

lim := makeLimiter(cfg.MaxConcurrentIndexers)

n := len(cfg.Repos)
n := 0
for name := range cfg.Repos {
if _, ok := searchers[name]; ok {
continue
}
n++
}
// Channel to receive the results from newSearcherConcurrent function.
resultCh := make(chan searcherResult, n)

// Start new searchers for all repos in different go routines while
// respecting cfg.MaxConcurrentIndexers.
for name, repo := range cfg.Repos {
if _, ok := searchers[name]; ok {
continue
}
go newSearcherConcurrent(cfg.DbPath, name, repo, refs, lim, resultCh)
}

Expand All @@ -315,15 +323,15 @@ func MakeAll(cfg *config.Config) (map[string]*Searcher, map[string]error, error)
}

if err := refs.removeUnclaimed(); err != nil {
return nil, nil, err
return nil, err
}

// after all the repos are in good shape, we start their polling
for _, s := range searchers {
s.begin()
}

return searchers, errs, nil
return errs, nil
}

// Creates a new Searcher that is available for searches as soon as this returns.
Expand Down
11 changes: 8 additions & 3 deletions ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (h *prdHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ct := h.content[p]
if ct != nil {
// if so, render it
if err := renderForPrd(w, ct, h.cfg, h.cfgJson, r); err != nil {
if err := renderForPrd(w, ct, h.cfg, r); err != nil {
log.Panic(err)
}
return
Expand All @@ -143,7 +143,7 @@ func (h *prdHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// Renders a templated asset in prd-mode. This strategy will embed
// the sources directly in a script tag on the templated page.
func renderForPrd(w io.Writer, c *content, cfg *config.Config, cfgJson string, r *http.Request) error {
func renderForPrd(w io.Writer, c *content, cfg *config.Config, r *http.Request) error {
var buf bytes.Buffer
buf.WriteString("<script>")
for _, src := range c.sources {
Expand All @@ -155,10 +155,15 @@ func renderForPrd(w io.Writer, c *content, cfg *config.Config, cfgJson string, r
}
buf.WriteString("</script>")

json, err := cfg.ToJsonString()
if err != nil {
return err
}

return c.tpl.Execute(w, map[string]interface{}{
"ReactVersion": ReactVersion,
"jQueryVersion": JQueryVersion,
"ReposAsJson": cfgJson,
"ReposAsJson": json,
"Title": cfg.Title,
"Source": html_template.HTML(buf.String()),
"Host": r.Host,
Expand Down

0 comments on commit 6abd3b7

Please sign in to comment.