mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-12 12:16:37 +08:00
fix: protect shared rules across delivery and recovery (#560)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestConfigCleanupPreservesSharedDependencies(t *testing.T) {
|
||||
a := newCleanupAgent(t)
|
||||
a.addRuntime(t, "70_1_0", 1, 1, 1, time.Now())
|
||||
a.h.cleanNodeConfigs(1, `{
|
||||
"services": [{"name":"70_1_0_tcp", "handler":{"chain":"chains_88"}, "limiter":"13, rule_traffic_limit_70"}],
|
||||
"chains": [{"name":"fed_chain_17"}, {"name":"chains_88"}, {"name":"chains_999"}],
|
||||
"limiters": [{"name":"13"}, {"name":"rule_traffic_limit_70"}, {"name":"99"}]
|
||||
}`)
|
||||
a.probe(t)
|
||||
if commands := a.commandsOfType("DeleteService"); len(commands) != 0 {
|
||||
t.Fatalf("shared service was deleted: %+v", commands)
|
||||
}
|
||||
assertCleanupDependency(t, a, "DeleteChains", "chain", "chains_999")
|
||||
assertCleanupDependency(t, a, "DeleteLimiters", "limiter", "99")
|
||||
}
|
||||
|
||||
func TestConfigCleanupRemovesOrphanedForwardLimiter(t *testing.T) {
|
||||
a := newCleanupAgent(t)
|
||||
a.h.cleanNodeConfigs(1, `{"limiters":[{"name":"rule_traffic_limit_70"}]}`)
|
||||
a.probe(t)
|
||||
assertCleanupDependency(t, a, "DeleteLimiters", "limiter", "rule_traffic_limit_70")
|
||||
}
|
||||
|
||||
func TestConfigCleanupProtectsPendingSharedDependencies(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
age time.Duration
|
||||
keep bool
|
||||
}{
|
||||
{name: "pending", age: time.Minute, keep: true},
|
||||
{name: "expired-reservation", age: 11 * time.Minute},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := newCleanupAgent(t)
|
||||
a.addRuntime(t, "", 1, 1, 0, time.Now().Add(-tc.age))
|
||||
// Dependencies may arrive before the service and its runtime binding.
|
||||
a.h.cleanNodeConfigs(1, `{"chains":[{"name":"chains_88"}],"limiters":[{"name":"13"}]}`)
|
||||
a.probe(t)
|
||||
if tc.keep {
|
||||
for _, commandType := range []string{"DeleteChains", "DeleteLimiters"} {
|
||||
if commands := a.commandsOfType(commandType); len(commands) != 0 {
|
||||
t.Fatalf("pending shared dependencies deleted: %+v", commands)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
assertCleanupDependency(t, a, "DeleteChains", "chain", "chains_88")
|
||||
assertCleanupDependency(t, a, "DeleteLimiters", "limiter", "13")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCleanupPreservesDependenciesOnLookupFailure(t *testing.T) {
|
||||
for _, table := range []string{"peer_share_runtime", "tunnel", "speed_limit", "forward"} {
|
||||
t.Run(table, func(t *testing.T) {
|
||||
a := newCleanupAgent(t)
|
||||
callback := "test:config-cleanup-query-failure"
|
||||
injected := false
|
||||
if err := a.h.repo.DB().Callback().Query().Before("gorm:query").Register(callback, func(tx *gorm.DB) {
|
||||
if tx.Statement.Table == table {
|
||||
injected = true
|
||||
tx.AddError(errors.New("injected dependency lookup failure"))
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = a.h.repo.DB().Callback().Query().Remove(callback) })
|
||||
configs := map[string]string{
|
||||
"peer_share_runtime": `{"chains":[{"name":"chains_88"}],"limiters":[{"name":"13"}]}`,
|
||||
"tunnel": `{"chains":[{"name":"chains_88"}]}`,
|
||||
"speed_limit": `{"limiters":[{"name":"13"}]}`,
|
||||
"forward": `{"limiters":[{"name":"rule_traffic_limit_70"}]}`,
|
||||
}
|
||||
a.h.cleanNodeConfigs(1, configs[table])
|
||||
a.probe(t)
|
||||
if !injected {
|
||||
t.Fatal("expected dependency lookup failure to be injected")
|
||||
}
|
||||
for _, commandType := range []string{"DeleteChains", "DeleteLimiters"} {
|
||||
if commands := a.commandsOfType(commandType); len(commands) != 0 {
|
||||
t.Fatalf("lookup failure must not authorize cleanup: %+v", commands)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedForwardCleanupDuringRuntimeBinding(t *testing.T) {
|
||||
for _, mode := range []string{"single", "batch", "config"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
a := newCleanupAgent(t)
|
||||
a.addRuntime(t, "", 1, 1, 0, time.Now())
|
||||
callback := "test:bind-during-cleanup"
|
||||
bound := false
|
||||
if err := a.h.repo.DB().Callback().Query().After("gorm:query").Register(callback, func(tx *gorm.DB) {
|
||||
if tx.Statement.Table != "peer_share_runtime" || bound {
|
||||
return
|
||||
}
|
||||
bound = true
|
||||
// Reproduce binding immediately after the first ownership read.
|
||||
// Separate name/unbound queries would both miss this runtime.
|
||||
if err := a.h.repo.DB().Exec("UPDATE peer_share_runtime SET service_name = ?, applied = 1", "70_1_0").Error; err != nil {
|
||||
t.Errorf("bind shared runtime: %v", err)
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = a.h.repo.DB().Callback().Query().Remove(callback) })
|
||||
runCleanupPath(a.h, mode, []string{"70_1_0_tcp"})
|
||||
a.probe(t)
|
||||
if !bound {
|
||||
t.Fatal("binding transition did not run")
|
||||
}
|
||||
if commands := a.commandsOfType("DeleteService"); len(commands) != 0 {
|
||||
t.Fatalf("service was deleted during binding: %+v", commands)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCleanupDependency(t *testing.T, a *cleanupAgent, commandType, key, want string) {
|
||||
t.Helper()
|
||||
commands := a.commandsOfType(commandType)
|
||||
if len(commands) != 1 {
|
||||
t.Fatalf("expected one %s for %s, got %+v", commandType, want, commands)
|
||||
}
|
||||
var data map[string]string
|
||||
if err := json.Unmarshal(commands[0].Data, &data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if data[key] != want {
|
||||
t.Fatalf("unexpected %s target: got %q, want %q", commandType, data[key], want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user