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
|
||||||
|
|
||||||
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]`
|
### `[self]`
|
||||||
|
|
||||||
@@ -108,6 +108,5 @@ See `justfile` for defined commands.
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Watch configuration files and hot reload
|
|
||||||
- [ ] Free up resources after instance inactivity
|
- [ ] Free up resources after instance inactivity
|
||||||
- [ ] Admin/member lists
|
- [ ] Admin/member lists
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go zooid.MonitorInstances()
|
||||||
|
|
||||||
<-shutdown
|
<-shutdown
|
||||||
|
|
||||||
fmt.Println("\nShutting down gracefully...")
|
fmt.Println("\nShutting down gracefully...")
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ require (
|
|||||||
fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd
|
fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gosimple/slug v1.15.0
|
github.com/gosimple/slug v1.15.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -19,7 +19,6 @@ require (
|
|||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/coder/websocket v1.8.13 // 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/crypto/blake256 v1.1.0 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||||
@@ -33,7 +32,6 @@ require (
|
|||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
@@ -44,6 +42,6 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/net v0.41.0 // 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
|
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/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 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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=
|
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/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 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
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/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
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) {
|
func LoadConfig(hostname string) (*Config, error) {
|
||||||
path := filepath.Join("configs", hostname)
|
path := filepath.Join("config", hostname)
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||||
|
|||||||
+85
-20
@@ -5,6 +5,7 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -13,9 +14,86 @@ import (
|
|||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
"fiatjaf.com/nostr/khatru"
|
"fiatjaf.com/nostr/khatru"
|
||||||
"fiatjaf.com/nostr/nip29"
|
"fiatjaf.com/nostr/nip29"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/gosimple/slug"
|
"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 {
|
type Instance struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
Events eventstore.Store
|
Events eventstore.Store
|
||||||
@@ -60,6 +138,7 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
Relay: khatru.NewRelay(),
|
Relay: khatru.NewRelay(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance.Relay.Negentropy = true
|
||||||
instance.Relay.Info.Name = config.Self.Name
|
instance.Relay.Info.Name = config.Self.Name
|
||||||
instance.Relay.Info.Icon = config.Self.Icon
|
instance.Relay.Info.Icon = config.Self.Icon
|
||||||
instance.Relay.Info.PubKey = &pubkey
|
instance.Relay.Info.PubKey = &pubkey
|
||||||
@@ -103,28 +182,14 @@ func MakeInstance(hostname string) (*Instance, error) {
|
|||||||
return instance, nil
|
return instance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func (instance *Instance) Cleanup() bool {
|
||||||
instances map[string]*Instance
|
// Close the event store
|
||||||
instanceOnce sync.Once
|
instance.Events.Close()
|
||||||
)
|
|
||||||
|
|
||||||
func GetInstance(hostname string) (*Instance, error) {
|
// Remove from instances map
|
||||||
instanceOnce.Do(func() {
|
delete(instances, instance.Config.Host)
|
||||||
instances = make(map[string]*Instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
instance, exists := instances[hostname]
|
return true
|
||||||
if !exists {
|
|
||||||
newInstance, err := MakeInstance(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
instances[hostname] = newInstance
|
|
||||||
instance = newInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|||||||
Reference in New Issue
Block a user