mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-13 04:36:37 +08:00
fix: protect shared rules across delivery and recovery (#560)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/parsing"
|
||||
@@ -30,6 +32,34 @@ func Load(cfg *config.Config) error {
|
||||
return defaultLoader.Load(cfg)
|
||||
}
|
||||
|
||||
// Reload replaces the runtime and commits the config only after it starts.
|
||||
// The caller must hold config.LockMutation, including while parsing cfg, so the
|
||||
// rollback snapshot includes every previously acknowledged runtime command.
|
||||
// Failed loads can partially replace registries and bind listeners; always
|
||||
// rebuild the last successful snapshot before returning an error.
|
||||
func Reload(cfg *config.Config, run func(*config.Config) error) error {
|
||||
previous := config.Global()
|
||||
if err := apply(cfg, run); err != nil {
|
||||
// A failed partial load may have left both old and new listeners behind.
|
||||
for name := range registry.ServiceRegistry().GetAll() {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
if rollbackErr := apply(previous, run); rollbackErr != nil {
|
||||
return fmt.Errorf("reload failed: %w; restore previous config failed: %v", err, rollbackErr)
|
||||
}
|
||||
return fmt.Errorf("reload failed (previous config restored): %w", err)
|
||||
}
|
||||
config.Set(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func apply(cfg *config.Config, run func(*config.Config) error) error {
|
||||
if err := Load(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return run(cfg)
|
||||
}
|
||||
|
||||
type loader struct{}
|
||||
|
||||
func (l *loader) Load(cfg *config.Config) error {
|
||||
@@ -217,12 +247,19 @@ func register(cfg *config.Config) error {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
for _, svcCfg := range cfg.Services {
|
||||
if svcCfg == nil {
|
||||
return fmt.Errorf("service config is nil")
|
||||
}
|
||||
if paused, _ := svcCfg.Metadata["paused"].(bool); paused {
|
||||
continue
|
||||
}
|
||||
svc, err := service_parser.ParseService(svcCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if svc != nil {
|
||||
if err := registry.ServiceRegistry().Register(svcCfg.Name, svc); err != nil {
|
||||
svc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package loader_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/loader"
|
||||
_ "github.com/go-gost/x/handler/auto"
|
||||
_ "github.com/go-gost/x/listener/tcp"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
func TestReloadRestoresPreviousRuntime(t *testing.T) {
|
||||
for _, failure := range []string{"listener", "handler", "partial", "run"} {
|
||||
t.Run(failure, func(t *testing.T) {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
original := config.Global()
|
||||
defer config.Set(original)
|
||||
defer func() {
|
||||
for name := range registry.ServiceRegistry().GetAll() {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
}()
|
||||
old := &config.Config{Services: []*config.ServiceConfig{
|
||||
{Name: "shared-rule", Addr: "127.0.0.1:0", Handler: &config.HandlerConfig{Type: "auto"}, Listener: &config.ListenerConfig{Type: "tcp"}},
|
||||
{Name: "paused-rule", Addr: "127.0.0.1:0", Metadata: map[string]any{"paused": true}},
|
||||
}}
|
||||
if err := loader.Load(old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr := registry.ServiceRegistry().Get("shared-rule").Addr().String()
|
||||
old.Services[0].Addr = addr
|
||||
config.Set(old)
|
||||
serve := func(*config.Config) error {
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
go svc.Serve()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
serve(old)
|
||||
replacement := config.Global()
|
||||
replacement.Services = replacement.Services[:1]
|
||||
switch failure {
|
||||
case "listener":
|
||||
replacement.Services[0].Listener.Type = "invalid-listener"
|
||||
case "handler":
|
||||
replacement.Services[0].Handler.Type = "invalid-handler"
|
||||
case "partial":
|
||||
replacement.Services = append(replacement.Services, &config.ServiceConfig{Name: "broken", Listener: &config.ListenerConfig{Type: "invalid-listener"}})
|
||||
}
|
||||
run := serve
|
||||
if failure == "run" {
|
||||
run = func(cfg *config.Config) error {
|
||||
if cfg == replacement {
|
||||
return &net.AddrError{Err: "auxiliary listener failed", Addr: "test"}
|
||||
}
|
||||
return serve(cfg)
|
||||
}
|
||||
}
|
||||
if err := loader.Reload(replacement, run); err == nil {
|
||||
t.Fatal("expected reload failure")
|
||||
}
|
||||
restored := registry.ServiceRegistry().Get("shared-rule")
|
||||
if restored == nil || restored.Addr().String() != addr {
|
||||
t.Fatal("previous service was not restored on its original port")
|
||||
}
|
||||
if registry.ServiceRegistry().Get("paused-rule") != nil {
|
||||
t.Fatal("rollback resumed a paused service")
|
||||
}
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("restored listener unavailable: %v", err)
|
||||
}
|
||||
conn.Close()
|
||||
got := config.Global()
|
||||
if len(got.Services) != 2 || got.Services[0].Listener.Type != "tcp" || got.Services[0].Handler.Type != "auto" {
|
||||
t.Fatalf("failed config was committed: %+v", got.Services)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user