Watch config files and hot reload

This commit is contained in:
Jon Staab
2025-09-26 16:37:50 -07:00
parent 07a5a07583
commit 7eb6d22362
6 changed files with 95 additions and 28 deletions
+1 -2
View File
@@ -15,7 +15,7 @@ Zooid supports a few environment variables, which configure shared resources lik
## Configuration
Configuration files are written using [toml](https://toml.io). The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections:
Configuration files are written using [toml](https://toml.io) files placed in the `./config` directory. The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections:
### `[self]`
@@ -108,6 +108,5 @@ See `justfile` for defined commands.
## TODO
- [ ] Watch configuration files and hot reload
- [ ] Free up resources after instance inactivity
- [ ] Admin/member lists
+2
View File
@@ -31,6 +31,8 @@ func main() {
}
}()
go zooid.MonitorInstances()
<-shutdown
fmt.Println("\nShutting down gracefully...")
+2 -4
View File
@@ -6,10 +6,10 @@ require (
fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd
github.com/BurntSushi/toml v1.5.0
github.com/Masterminds/squirrel v1.5.4
github.com/fsnotify/fsnotify v1.9.0
github.com/gosimple/slug v1.15.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.10.0
)
require (
@@ -19,7 +19,6 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/coder/websocket v1.8.13 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/fasthttp/websocket v1.5.12 // indirect
@@ -33,7 +32,6 @@ require (
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
@@ -44,6 +42,6 @@ require (
github.com/valyala/fasthttp v1.59.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+4 -1
View File
@@ -28,6 +28,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjY
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -91,9 +93,10 @@ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+1 -1
View File
@@ -49,7 +49,7 @@ type Config struct {
}
func LoadConfig(hostname string) (*Config, error) {
path := filepath.Join("configs", hostname)
path := filepath.Join("config", hostname)
var config Config
if _, err := toml.DecodeFile(path, &config); err != nil {
+85 -20
View File
@@ -5,6 +5,7 @@ import (
"iter"
"log"
"net/http"
"path/filepath"
"slices"
"strings"
"sync"
@@ -13,9 +14,86 @@ import (
"fiatjaf.com/nostr/eventstore"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr/nip29"
"github.com/fsnotify/fsnotify"
"github.com/gosimple/slug"
)
// Top level instance creation/destruction
var (
instances map[string]*Instance
instancesOnce sync.Once
instancesMux sync.RWMutex
)
func GetInstance(hostname string) (*Instance, error) {
instancesMux.RLock()
defer instancesMux.RUnlock()
instancesOnce.Do(func() {
instances = make(map[string]*Instance)
})
instance, exists := instances[hostname]
if !exists {
newInstance, err := MakeInstance(hostname)
if err != nil {
return nil, err
}
instances[hostname] = newInstance
instance = newInstance
}
return instance, nil
}
func MonitorInstances() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("Failed to create file watcher: %v", err)
return
}
defer watcher.Close()
if err := watcher.Add("./config"); err != nil {
log.Printf("Failed to watch config directory: %v", err)
return
}
log.Printf("Watching config directory for changes")
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
hostname := filepath.Base(event.Name)
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) {
log.Printf("Config file changed/deleted: %s", event.Name)
instancesMux.Lock()
if instance, exists := instances[hostname]; exists {
instance.Cleanup()
}
instancesMux.Unlock()
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Printf("File watcher error: %v", err)
}
}
}
// Instance struct
type Instance struct {
Config *Config
Events eventstore.Store
@@ -60,6 +138,7 @@ func MakeInstance(hostname string) (*Instance, error) {
Relay: khatru.NewRelay(),
}
instance.Relay.Negentropy = true
instance.Relay.Info.Name = config.Self.Name
instance.Relay.Info.Icon = config.Self.Icon
instance.Relay.Info.PubKey = &pubkey
@@ -103,28 +182,14 @@ func MakeInstance(hostname string) (*Instance, error) {
return instance, nil
}
var (
instances map[string]*Instance
instanceOnce sync.Once
)
func (instance *Instance) Cleanup() bool {
// Close the event store
instance.Events.Close()
func GetInstance(hostname string) (*Instance, error) {
instanceOnce.Do(func() {
instances = make(map[string]*Instance)
})
// Remove from instances map
delete(instances, instance.Config.Host)
instance, exists := instances[hostname]
if !exists {
newInstance, err := MakeInstance(hostname)
if err != nil {
return nil, err
}
instances[hostname] = newInstance
instance = newInstance
}
return instance, nil
return true
}
// Utility methods