Watch config files and hot reload
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -31,6 +31,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
go zooid.MonitorInstances()
|
||||
|
||||
<-shutdown
|
||||
|
||||
fmt.Println("\nShutting down gracefully...")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user