fix: reduce runtime sync disruptions (#484)

This commit is contained in:
sagit
2026-04-30 14:36:26 +08:00
committed by GitHub
parent 60311d3e47
commit 3c0e833cfc
11 changed files with 409 additions and 122 deletions
@@ -484,13 +484,39 @@ func (h *Handler) forwardServiceBaseCandidates(forward *forwardRecord) ([]string
}
func (h *Handler) deleteForwardServiceBasesOnNode(nodeID int64, bases []string) error {
return deleteForwardServiceCandidates(bases, func(name string) error {
payload := map[string]interface{}{
"services": []string{name},
names := buildForwardServiceDeleteNames(bases)
if len(names) == 0 {
return nil
}
payload := map[string]interface{}{"services": names}
_, err := h.sendNodeCommand(nodeID, "DeleteService", payload, false, true)
return err
}
func buildForwardServiceDeleteNames(bases []string) []string {
names := make([]string, 0, len(bases)*3)
seen := make(map[string]struct{}, len(bases)*3)
appendName := func(name string) {
name = strings.TrimSpace(name)
if name == "" {
return
}
_, err := h.sendNodeCommand(nodeID, "DeleteService", payload, false, false)
return err
})
if _, ok := seen[name]; ok {
return
}
seen[name] = struct{}{}
names = append(names, name)
}
for _, base := range bases {
base = strings.TrimSpace(base)
if base == "" {
continue
}
appendName(base + "_tcp")
appendName(base + "_udp")
appendName(base)
}
return names
}
func (h *Handler) controlForwardServices(forward *forwardRecord, commandType string, tolerateNotFound bool) error {
@@ -520,6 +546,25 @@ func (h *Handler) controlForwardServices(forward *forwardRecord, commandType str
candidateTunnelIDs = append(candidateTunnelIDs, userTunnelIDs...)
candidateTunnelIDs = append(candidateTunnelIDs, allUserTunnelIDs...)
bases := buildForwardServiceBaseCandidates(forward.ID, forward.UserID, userTunnelID, candidateTunnelIDs)
if strings.EqualFold(strings.TrimSpace(commandType), "DeleteService") {
seen := map[int64]struct{}{}
for _, fp := range ports {
if _, ok := seen[fp.NodeID]; ok {
continue
}
seen[fp.NodeID] = struct{}{}
if err := h.deleteForwardServiceBasesOnNode(fp.NodeID, bases); err != nil {
if isNodeOfflineOrTimeoutError(err) {
continue
}
if tolerateNotFound && isNotFoundError(err) {
continue
}
return err
}
}
return nil
}
seen := map[int64]struct{}{}
healed := false
for _, fp := range ports {
@@ -201,6 +201,52 @@ func TestDeleteForwardServiceCandidatesDeletesAllMatchingVariants(t *testing.T)
}
}
func TestBuildForwardServiceDeleteNamesBatchesAndDeduplicatesVariants(t *testing.T) {
bases := []string{"57_7_7", "57_7_0", "57_7_7"}
got := buildForwardServiceDeleteNames(bases)
want := []string{"57_7_7_tcp", "57_7_7_udp", "57_7_7", "57_7_0_tcp", "57_7_0_udp", "57_7_0"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("expected %v, got %v", want, got)
}
}
func TestRemovedTunnelRuntimeNodeIDsSeparatesChainAndServiceRoles(t *testing.T) {
oldRows := []chainNodeRecord{
{NodeID: 1, ChainType: 1},
{NodeID: 2, ChainType: 2},
{NodeID: 3, ChainType: 3},
{NodeID: 5, ChainType: 2},
{NodeID: 6, ChainType: 3},
}
newRows := []chainNodeRecord{
{NodeID: 2, ChainType: 3},
{NodeID: 3, ChainType: 3},
{NodeID: 5, ChainType: 1},
}
removedChains := removedTunnelRuntimeNodeIDs(oldRows, newRows, tunnelRuntimeNeedsChain)
if want := []int64{1, 2}; !reflect.DeepEqual(removedChains, want) {
t.Fatalf("expected removed chains %v, got %v", want, removedChains)
}
removedServices := removedTunnelRuntimeNodeIDs(oldRows, newRows, tunnelRuntimeNeedsService)
if want := []int64{5, 6}; !reflect.DeepEqual(removedServices, want) {
t.Fatalf("expected removed services %v, got %v", want, removedServices)
}
}
func TestTunnelForwardRuntimeNeedsSyncOnlyWhenTypeOrEntriesChange(t *testing.T) {
if tunnelForwardRuntimeNeedsSync(2, 2, []int64{1, 2}, []int64{2, 1}) {
t.Fatalf("same tunnel type and same entry set should not resync forwards")
}
if !tunnelForwardRuntimeNeedsSync(1, 2, []int64{1}, []int64{1}) {
t.Fatalf("type change should resync forwards")
}
if !tunnelForwardRuntimeNeedsSync(2, 2, []int64{1}, []int64{1, 2}) {
t.Fatalf("entry set change should resync forwards")
}
}
func TestValidateForwardPortAvailabilityRejectsOtherForwardOccupancy(t *testing.T) {
h := &Handler{repo: nil}
node := &nodeRecord{ID: 9, Name: "test-node"}
+141 -35
View File
@@ -748,21 +748,8 @@ func (h *Handler) cleanupTunnelRuntime(tunnelID int64) {
return
}
protocol := strings.TrimSpace(tunnel.Protocol)
if protocol == "" {
protocol = "tls"
}
chainName := fmt.Sprintf("chains_%d", tunnelID)
serviceNames := []string{
fmt.Sprintf("tunnel_%d", tunnelID),
fmt.Sprintf("%d_tls", tunnelID),
fmt.Sprintf("%d_kcp", tunnelID),
fmt.Sprintf("%d_wss", tunnelID),
fmt.Sprintf("%d_mtls", tunnelID),
fmt.Sprintf("%d_mwss", tunnelID),
fmt.Sprintf("%d_tcp", tunnelID),
fmt.Sprintf("%d_mtcp", tunnelID),
}
serviceNames := tunnelRuntimeServiceNames(tunnelID)
for _, row := range chainRows {
if row.ChainType == 1 {
@@ -776,6 +763,70 @@ func (h *Handler) cleanupTunnelRuntime(tunnelID int64) {
}
}
func tunnelRuntimeServiceNames(tunnelID int64) []string {
return []string{
fmt.Sprintf("tunnel_%d", tunnelID),
fmt.Sprintf("%d_tls", tunnelID),
fmt.Sprintf("%d_kcp", tunnelID),
fmt.Sprintf("%d_wss", tunnelID),
fmt.Sprintf("%d_mtls", tunnelID),
fmt.Sprintf("%d_mwss", tunnelID),
fmt.Sprintf("%d_tcp", tunnelID),
fmt.Sprintf("%d_mtcp", tunnelID),
}
}
func tunnelRuntimeNeedsChain(row chainNodeRecord) bool {
return row.ChainType == 1 || row.ChainType == 2
}
func tunnelRuntimeNeedsService(row chainNodeRecord) bool {
return row.ChainType == 2 || row.ChainType == 3
}
func removedTunnelRuntimeNodeIDs(oldRows, newRows []chainNodeRecord, needsRuntime func(chainNodeRecord) bool) []int64 {
if len(oldRows) == 0 || needsRuntime == nil {
return nil
}
newRuntimeNodes := make(map[int64]struct{}, len(newRows))
for _, row := range newRows {
if row.NodeID <= 0 || !needsRuntime(row) {
continue
}
newRuntimeNodes[row.NodeID] = struct{}{}
}
seen := make(map[int64]struct{}, len(oldRows))
removed := make([]int64, 0)
for _, row := range oldRows {
if row.NodeID <= 0 || !needsRuntime(row) {
continue
}
if _, ok := seen[row.NodeID]; ok {
continue
}
seen[row.NodeID] = struct{}{}
if _, stillNeeded := newRuntimeNodes[row.NodeID]; stillNeeded {
continue
}
removed = append(removed, row.NodeID)
}
return removed
}
func (h *Handler) cleanupObsoleteTunnelRuntime(tunnelID int64, oldRows, newRows []chainNodeRecord) {
if h == nil || tunnelID <= 0 || len(oldRows) == 0 {
return
}
chainName := fmt.Sprintf("chains_%d", tunnelID)
for _, nodeID := range removedTunnelRuntimeNodeIDs(oldRows, newRows, tunnelRuntimeNeedsChain) {
_, _ = h.sendNodeCommand(nodeID, "DeleteChains", map[string]interface{}{"chain": chainName}, false, true)
}
serviceNames := tunnelRuntimeServiceNames(tunnelID)
for _, nodeID := range removedTunnelRuntimeNodeIDs(oldRows, newRows, tunnelRuntimeNeedsService) {
_, _ = h.sendNodeCommand(nodeID, "DeleteService", map[string]interface{}{"services": serviceNames}, false, true)
}
}
func (h *Handler) tunnelGet(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
response.WriteJSON(w, response.ErrDefault("请求失败"))
@@ -819,12 +870,15 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
return
}
oldEntryNodeIDs, _ := h.tunnelEntryNodeIDs(id)
h.cleanupTunnelRuntime(id)
typeVal := asInt(req["type"], 1)
oldTunnel, _ := h.getTunnelRecord(id)
oldChainRows, _ := h.listChainNodesForTunnel(id)
if oldTunnel != nil && oldTunnel.Type == 2 && typeVal != 2 {
h.cleanupTunnelRuntime(id)
}
h.cleanupFederationRuntime(id)
now := time.Now().UnixMilli()
typeVal := asInt(req["type"], 1)
ipPreference := asString(req["ipPreference"])
localDomain := h.federationLocalDomain()
@@ -917,13 +971,19 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
}
if typeVal == 2 {
createdChains, createdServices, applyErr := h.applyTunnelRuntime(runtimeState)
applyRuntime := h.applyTunnelRuntime
if oldTunnel != nil && oldTunnel.Type == 2 {
applyRuntime = h.applyTunnelRuntimeUpsert
}
createdChains, createdServices, applyErr := applyRuntime(runtimeState)
if applyErr != nil {
updateProtocol := "tls"
if len(runtimeState.InNodes) > 0 && strings.TrimSpace(runtimeState.InNodes[0].Protocol) != "" {
updateProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
}
h.rollbackTunnelRuntime(createdChains, createdServices, id, updateProtocol)
if oldTunnel == nil || oldTunnel.Type != 2 {
h.rollbackTunnelRuntime(createdChains, createdServices, id, updateProtocol)
}
h.releaseFederationRuntimeRefs(federationReleaseRefs)
_ = h.repo.DeleteFederationTunnelBindingsByTunnel(id)
if len(federationReleaseRefs) == 0 && shouldDeferTunnelRuntimeApplyError(applyErr) {
@@ -933,9 +993,20 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault(applyErr.Error()))
return
}
newChainRows, _ := h.listChainNodesForTunnel(id)
h.cleanupObsoleteTunnelRuntime(id, oldChainRows, newChainRows)
}
if forwards, fwdErr := h.listForwardsByTunnel(id); fwdErr == nil {
oldType := 0
if oldTunnel != nil {
oldType = oldTunnel.Type
}
if tunnelForwardRuntimeNeedsSync(oldType, typeVal, oldEntryNodeIDs, newEntryNodeIDs) {
forwards, fwdErr := h.listForwardsByTunnel(id)
if fwdErr != nil {
response.WriteJSON(w, response.OKEmpty())
return
}
for i := range forwards {
_ = h.syncForwardServices(&forwards[i], "UpdateService", true)
}
@@ -944,6 +1015,13 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.OKEmpty())
}
func tunnelForwardRuntimeNeedsSync(oldType, newType int, oldEntryNodeIDs, newEntryNodeIDs []int64) bool {
if oldType != newType {
return true
}
return !sameInt64Set(oldEntryNodeIDs, newEntryNodeIDs)
}
func sameInt64Set(a, b []int64) bool {
if len(a) != len(b) {
return false
@@ -3358,6 +3436,14 @@ func (h *Handler) cleanupFederationRuntime(tunnelID int64) {
}
func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64, error) {
return h.applyTunnelRuntimeWithMode(state, false)
}
func (h *Handler) applyTunnelRuntimeUpsert(state *tunnelCreateState) ([]int64, []int64, error) {
return h.applyTunnelRuntimeWithMode(state, true)
}
func (h *Handler) applyTunnelRuntimeWithMode(state *tunnelCreateState, upsert bool) ([]int64, []int64, error) {
if h == nil || state == nil {
return nil, nil, errors.New("invalid tunnel runtime state")
}
@@ -3376,7 +3462,7 @@ func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64
if err != nil {
return createdChains, createdServices, err
}
if _, err := h.sendNodeCommand(inNode.NodeID, "AddChains", chainData, true, false); err != nil {
if err := h.applyTunnelChainOnNode(inNode.NodeID, chainData, upsert); err != nil {
if shouldDeferTunnelRuntimeApplyError(err) {
continue
}
@@ -3399,7 +3485,7 @@ func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64
if err != nil {
return createdChains, createdServices, err
}
if _, err := h.sendNodeCommand(chainNode.NodeID, "AddChains", chainData, true, false); err != nil {
if err := h.applyTunnelChainOnNode(chainNode.NodeID, chainData, upsert); err != nil {
if shouldDeferTunnelRuntimeApplyError(err) {
continue
}
@@ -3408,7 +3494,7 @@ func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64
createdChains = append(createdChains, chainNode.NodeID)
serviceData := buildTunnelChainServiceConfig(state.TunnelID, chainNode, state.Nodes[chainNode.NodeID], len(nextTargets))
if err := h.addTunnelServiceOnNode(chainNode.NodeID, state.TunnelID, serviceData); err != nil {
if err := h.addTunnelServiceOnNodeWithMode(chainNode.NodeID, state.TunnelID, serviceData, upsert); err != nil {
if shouldDeferTunnelRuntimeApplyError(err) {
continue
}
@@ -3424,7 +3510,7 @@ func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64
continue
}
serviceData := buildTunnelChainServiceConfig(state.TunnelID, outNode, state.Nodes[outNode.NodeID], 1)
if err := h.addTunnelServiceOnNode(outNode.NodeID, state.TunnelID, serviceData); err != nil {
if err := h.addTunnelServiceOnNodeWithMode(outNode.NodeID, state.TunnelID, serviceData, upsert); err != nil {
if shouldDeferTunnelRuntimeApplyError(err) {
continue
}
@@ -3436,6 +3522,27 @@ func (h *Handler) applyTunnelRuntime(state *tunnelCreateState) ([]int64, []int64
return createdChains, createdServices, nil
}
func (h *Handler) applyTunnelChainOnNode(nodeID int64, chainData map[string]interface{}, upsert bool) error {
if upsert {
return h.upsertTunnelChainOnNode(nodeID, chainData)
}
_, err := h.sendNodeCommand(nodeID, "AddChains", chainData, true, false)
return err
}
func (h *Handler) upsertTunnelChainOnNode(nodeID int64, chainData map[string]interface{}) error {
if h == nil {
return errors.New("invalid tunnel chain context")
}
chainName := asString(chainData["name"])
if strings.TrimSpace(chainName) == "" {
return errors.New("转发链名称不能为空")
}
payload := map[string]interface{}{"chain": chainName, "data": chainData}
_, err := h.sendNodeCommand(nodeID, "UpdateChains", payload, true, false)
return err
}
func retryTunnelServiceAddWithCleanup(add func() error, cleanup func() error, wait time.Duration) error {
if add == nil {
return errors.New("invalid tunnel service add callback")
@@ -3457,6 +3564,10 @@ func retryTunnelServiceAddWithCleanup(add func() error, cleanup func() error, wa
}
func (h *Handler) addTunnelServiceOnNode(nodeID, tunnelID int64, serviceData []map[string]interface{}) error {
return h.addTunnelServiceOnNodeWithMode(nodeID, tunnelID, serviceData, false)
}
func (h *Handler) addTunnelServiceOnNodeWithMode(nodeID, tunnelID int64, serviceData []map[string]interface{}, upsert bool) error {
if h == nil {
return errors.New("invalid tunnel service context")
}
@@ -3466,9 +3577,13 @@ func (h *Handler) addTunnelServiceOnNode(nodeID, tunnelID int64, serviceData []m
serviceName = strings.TrimSpace(name)
}
}
command := "AddService"
if upsert {
command = "UpdateService"
}
return retryTunnelServiceAddWithCleanup(
func() error {
_, err := h.sendNodeCommand(nodeID, "AddService", serviceData, true, false)
_, err := h.sendNodeCommand(nodeID, command, serviceData, true, false)
return err
},
func() error {
@@ -3487,16 +3602,7 @@ func (h *Handler) rollbackTunnelRuntime(chainNodeIDs, serviceNodeIDs []int64, tu
protocol = "tls"
}
seenServices := make(map[int64]struct{})
serviceNames := []string{
fmt.Sprintf("tunnel_%d", tunnelID),
fmt.Sprintf("%d_tls", tunnelID),
fmt.Sprintf("%d_kcp", tunnelID),
fmt.Sprintf("%d_wss", tunnelID),
fmt.Sprintf("%d_mtls", tunnelID),
fmt.Sprintf("%d_mwss", tunnelID),
fmt.Sprintf("%d_tcp", tunnelID),
fmt.Sprintf("%d_mtcp", tunnelID),
}
serviceNames := tunnelRuntimeServiceNames(tunnelID)
for i := len(serviceNodeIDs) - 1; i >= 0; i-- {
nodeID := serviceNodeIDs[i]
if _, ok := seenServices[nodeID]; ok {
+1 -1
View File
@@ -48,7 +48,7 @@ func OnUpdate(f func(c *Config) error) error {
globalMux.Unlock()
if err == nil {
persist()
err = persist()
}
return err
+9 -8
View File
@@ -40,19 +40,19 @@ func EnablePersist() {
}
// persist writes the current global config to the configured file atomically.
func persist() {
func persist() error {
persistMu.Lock()
path := persistPath
enabled := persistEnable
persistMu.Unlock()
if !enabled || path == "" {
return
return nil
}
cfg := Global()
if cfg == nil {
return
return nil
}
var buf bytes.Buffer
@@ -60,7 +60,7 @@ func persist() {
enc.SetIndent("", " ")
if err := enc.Encode(cfg); err != nil {
fmt.Printf("⚠️ config persist: marshal failed: %v\n", err)
return
return fmt.Errorf("config persist: marshal failed: %w", err)
}
// Atomic write: write to temp file then rename
@@ -68,7 +68,7 @@ func persist() {
tmp, err := os.CreateTemp(dir, ".gost-*.tmp")
if err != nil {
fmt.Printf("⚠️ config persist: create temp file failed: %v\n", err)
return
return fmt.Errorf("config persist: create temp file failed: %w", err)
}
tmpName := tmp.Name()
@@ -76,19 +76,20 @@ func persist() {
tmp.Close()
os.Remove(tmpName)
fmt.Printf("⚠️ config persist: write failed: %v\n", err)
return
return fmt.Errorf("config persist: write failed: %w", err)
}
if err := tmp.Close(); err != nil {
os.Remove(tmpName)
fmt.Printf("⚠️ config persist: close temp file failed: %v\n", err)
return
return fmt.Errorf("config persist: close temp file failed: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
os.Remove(tmpName)
fmt.Printf("⚠️ config persist: rename failed: %v\n", err)
return
return fmt.Errorf("config persist: rename failed: %w", err)
}
fmt.Printf("💾 节点配置已持久化到 %s\n", path)
return nil
}
+3 -9
View File
@@ -31,12 +31,10 @@ func createChain(req createChainRequest) error {
return errors.New("chain " + name + " already exists")
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
c.Chains = append(c.Chains, &req.Data)
return nil
})
return nil
}
func updateChain(req updateChainRequest) error {
@@ -58,7 +56,7 @@ func updateChain(req updateChainRequest) error {
return errors.New("chain " + name + " already exists")
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
found := false
for i := range c.Chains {
if c.Chains[i].Name == name {
@@ -72,8 +70,6 @@ func updateChain(req updateChainRequest) error {
}
return nil
})
return nil
}
func deleteChain(req deleteChainRequest) error {
@@ -84,7 +80,7 @@ func deleteChain(req deleteChainRequest) error {
registry.ChainRegistry().Unregister(name)
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
chains := c.Chains
c.Chains = nil
for _, s := range chains {
@@ -95,8 +91,6 @@ func deleteChain(req deleteChainRequest) error {
}
return nil
})
return nil
}
type createChainRequest struct {
+12 -18
View File
@@ -25,12 +25,10 @@ func createLimiter(req createLimiterRequest) error {
return errors.New("limiter " + name + " already exists")
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
c.Limiters = append(c.Limiters, &req.Data)
return nil
})
return nil
}
func updateLimiter(req updateLimiterRequest) error {
@@ -49,7 +47,7 @@ func updateLimiter(req updateLimiterRequest) error {
return errors.New("limiter " + name + " already exists")
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
found := false
for i := range c.Limiters {
if c.Limiters[i].Name == name {
@@ -63,8 +61,6 @@ func updateLimiter(req updateLimiterRequest) error {
}
return nil
})
return nil
}
func deleteLimiter(req deleteLimiterRequest) error {
@@ -75,7 +71,7 @@ func deleteLimiter(req deleteLimiterRequest) error {
registry.TrafficLimiterRegistry().Unregister(name)
}
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
limiteres := c.Limiters
c.Limiters = nil
for _, s := range limiteres {
@@ -86,8 +82,6 @@ func deleteLimiter(req deleteLimiterRequest) error {
}
return nil
})
return nil
}
type createLimiterRequest struct {
@@ -120,10 +114,10 @@ func createConnLimiter(req createLimiterRequest) error {
return errors.New("conn limiter " + name + " already exists")
}
if c := config.Global(); c != nil {
return config.OnUpdate(func(c *config.Config) error {
c.CLimiters = append(c.CLimiters, &req.Data)
}
return nil
return nil
})
}
func updateConnLimiter(req updateLimiterRequest) error {
@@ -139,7 +133,7 @@ func updateConnLimiter(req updateLimiterRequest) error {
return errors.New("conn limiter " + name + " already exists")
}
if c := config.Global(); c != nil {
return config.OnUpdate(func(c *config.Config) error {
for i := range c.CLimiters {
if c.CLimiters[i].Name == name {
c.CLimiters[i] = &req.Data
@@ -147,8 +141,8 @@ func updateConnLimiter(req updateLimiterRequest) error {
}
}
c.CLimiters = append(c.CLimiters, &req.Data)
}
return nil
return nil
})
}
func deleteConnLimiter(req deleteLimiterRequest) error {
@@ -158,7 +152,7 @@ func deleteConnLimiter(req deleteLimiterRequest) error {
registry.ConnLimiterRegistry().Unregister(name)
}
if c := config.Global(); c != nil {
return config.OnUpdate(func(c *config.Config) error {
limiteres := c.CLimiters
c.CLimiters = nil
for _, s := range limiteres {
@@ -167,6 +161,6 @@ func deleteConnLimiter(req deleteLimiterRequest) error {
}
c.CLimiters = append(c.CLimiters, s)
}
}
return nil
return nil
})
}
+52
View File
@@ -0,0 +1,52 @@
package socket
import (
"path/filepath"
"testing"
corelogger "github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
"github.com/go-gost/x/registry"
)
func TestCreateConnLimiterUpdatesGlobalConfig(t *testing.T) {
corelogger.SetDefault(xlogger.Nop())
name := "conn_limiter_tdd"
originalConfig := config.Global()
defer config.Set(originalConfig)
registry.ConnLimiterRegistry().Unregister(name)
defer registry.ConnLimiterRegistry().Unregister(name)
config.Set(&config.Config{})
err := createConnLimiter(createLimiterRequest{Data: config.LimiterConfig{Name: name, Limits: []string{"$ 1"}}})
if err != nil {
t.Fatalf("create conn limiter: %v", err)
}
cfg := config.Global()
if len(cfg.CLimiters) != 1 || cfg.CLimiters[0] == nil || cfg.CLimiters[0].Name != name {
t.Fatalf("expected conn limiter in global config, got %#v", cfg.CLimiters)
}
}
func TestCreateLimiterReportsPersistFailure(t *testing.T) {
corelogger.SetDefault(xlogger.Nop())
name := "traffic_limiter_persist_tdd"
originalConfig := config.Global()
originalPersistPath := config.PersistPath()
defer config.Set(originalConfig)
defer config.SetPersistPath(originalPersistPath)
registry.TrafficLimiterRegistry().Unregister(name)
defer registry.TrafficLimiterRegistry().Unregister(name)
config.Set(&config.Config{})
config.SetPersistPath(filepath.Join(t.TempDir(), "missing", "gost.json"))
config.EnablePersist()
err := createLimiter(createLimiterRequest{Data: config.LimiterConfig{Name: name, Limits: []string{"$ 1"}}})
if err == nil {
t.Fatalf("expected persist failure to be returned")
}
}
+43 -15
View File
@@ -3,6 +3,7 @@ package socket
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
@@ -54,9 +55,8 @@ func createServices(req createServicesRequest) error {
if err := registry.ServiceRegistry().Register(ps.config.Name, ps.service); err != nil {
// 如果注册失败,回滚已注册的服务
for _, regName := range registeredServices {
if svc := registry.ServiceRegistry().Get(regName); svc != nil {
if registry.ServiceRegistry().Get(regName) != nil {
registry.ServiceRegistry().Unregister(regName)
svc.Close()
}
}
return errors.New("service " + ps.config.Name + " already exists")
@@ -72,14 +72,12 @@ func createServices(req createServicesRequest) error {
}
// 第四阶段:更新配置
config.OnUpdate(func(c *config.Config) error {
return config.OnUpdate(func(c *config.Config) error {
for _, ps := range parsedServices {
c.Services = append(c.Services, &ps.config)
}
return nil
})
return nil
}
func updateServices(req updateServicesRequest) error {
@@ -98,17 +96,23 @@ func updateServices(req updateServicesRequest) error {
}
// 第二阶段:逐个更新服务(Upsert模式:存在则更新,不存在则创建)
changedServices := make([]struct {
config config.ServiceConfig
service service.Service
}, 0, len(req.Data))
for i := range req.Data {
serviceConfig := &req.Data[i]
name := serviceConfig.Name
if registry.ServiceRegistry().Get(name) != nil && serviceConfigUnchanged(name, *serviceConfig) {
continue
}
// 1. 获取旧服务
old := registry.ServiceRegistry().Get(name)
// 2. 关闭旧服务 (如果存在)
if old != nil {
old.Close()
// 3. 从注册表移除旧服务
// 3. 从注册表移除旧服务;registry 会负责关闭旧服务。
registry.ServiceRegistry().Unregister(name)
}
@@ -117,6 +121,10 @@ func updateServices(req updateServicesRequest) error {
if err != nil {
return errors.New("create service " + name + " failed: " + err.Error())
}
changedServices = append(changedServices, struct {
config config.ServiceConfig
service service.Service
}{*serviceConfig, svc})
// 5. 注册新服务
if err := registry.ServiceRegistry().Register(name, svc); err != nil {
@@ -127,12 +135,15 @@ func updateServices(req updateServicesRequest) error {
// 6. 启动新服务
go svc.Serve()
}
if len(changedServices) == 0 {
return nil
}
// 第三阶段:更新配置
config.OnUpdate(func(c *config.Config) error {
for i := range req.Data {
if err := config.OnUpdate(func(c *config.Config) error {
for i := range changedServices {
// 创建副本以确保指针安全
cfgCopy := req.Data[i]
cfgCopy := changedServices[i].config
found := false
for j := range c.Services {
if c.Services[j].Name == cfgCopy.Name {
@@ -146,11 +157,30 @@ func updateServices(req updateServicesRequest) error {
}
}
return nil
})
}); err != nil {
return err
}
return nil
}
func serviceConfigUnchanged(name string, next config.ServiceConfig) bool {
cfg := config.Global()
if cfg == nil {
return false
}
next.Status = nil
for _, current := range cfg.Services {
if current == nil || strings.TrimSpace(current.Name) != name {
continue
}
currentCopy := *current
currentCopy.Status = nil
return reflect.DeepEqual(currentCopy, next)
}
return false
}
func deleteServices(req deleteServicesRequest) error {
if len(req.Services) == 0 {
@@ -183,7 +213,6 @@ func deleteServices(req deleteServicesRequest) error {
// 第二阶段:删除所有服务
for _, std := range servicesToDelete {
registry.ServiceRegistry().Unregister(std.name)
std.service.Close()
}
// 确保所有请求删除的服务都从注册表中移除(即使之前未找到实例)
for _, name := range namesToRemove {
@@ -193,7 +222,7 @@ func deleteServices(req deleteServicesRequest) error {
}
// 第三阶段:更新配置
config.OnUpdate(func(c *config.Config) error {
err := config.OnUpdate(func(c *config.Config) error {
services := c.Services
c.Services = nil
for _, s := range services {
@@ -211,8 +240,7 @@ func deleteServices(req deleteServicesRequest) error {
return nil
})
xservice.GetGlobalTrafficManager().RemoveServices(namesToRemove...)
return nil
return err
}
func pauseServices(req pauseServicesRequest) error {
+51
View File
@@ -0,0 +1,51 @@
package socket
import (
"net"
"testing"
corelogger "github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
"github.com/go-gost/x/config"
xlogger "github.com/go-gost/x/logger"
"github.com/go-gost/x/registry"
)
type recordingService struct {
closed int
}
func (s *recordingService) Serve() error { return nil }
func (s *recordingService) Addr() net.Addr { return nil }
func (s *recordingService) Close() error {
s.closed++
return nil
}
func TestUpdateServicesSkipsUnchangedServiceWithoutRestart(t *testing.T) {
corelogger.SetDefault(xlogger.Nop())
name := "unchanged_service_tdd"
existing := &recordingService{}
registry.ServiceRegistry().Unregister(name)
defer registry.ServiceRegistry().Unregister(name)
if err := registry.ServiceRegistry().Register(name, service.Service(existing)); err != nil {
t.Fatalf("register existing service: %v", err)
}
originalConfig := config.Global()
defer config.Set(originalConfig)
serviceConfig := config.ServiceConfig{Name: name, Addr: "127.0.0.1:0"}
config.Set(&config.Config{Services: []*config.ServiceConfig{&serviceConfig}})
if err := updateServices(updateServicesRequest{Data: []config.ServiceConfig{serviceConfig}}); err != nil {
t.Fatalf("unchanged update should succeed without parsing/restarting: %v", err)
}
if existing.closed != 0 {
t.Fatalf("unchanged service was restarted, closed %d times", existing.closed)
}
if got := registry.ServiceRegistry().Get(name); got != service.Service(existing) {
t.Fatalf("expected existing service to remain registered")
}
}
-30
View File
@@ -782,7 +782,6 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
fmt.Println("🔔 收到命令: ", string(jsonBytes))
var err error
var response CommandResponse
var needSaveConfig bool // 标记是否需要保存配置(只有状态变更命令才需要)
// 传递 requestId
response.RequestId = cmd.RequestId
@@ -792,63 +791,49 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
case "AddService":
err = w.handleAddService(cmd.Data)
response.Type = "AddServiceResponse"
needSaveConfig = true
case "UpdateService":
err = w.handleUpdateService(cmd.Data)
response.Type = "UpdateServiceResponse"
needSaveConfig = true
case "DeleteService":
err = w.handleDeleteService(cmd.Data)
response.Type = "DeleteServiceResponse"
needSaveConfig = true
case "PauseService":
err = w.handlePauseService(cmd.Data)
response.Type = "PauseServiceResponse"
needSaveConfig = true
case "ResumeService":
err = w.handleResumeService(cmd.Data)
response.Type = "ResumeServiceResponse"
needSaveConfig = true
// Chain 相关命令
case "AddChains":
err = w.handleAddChain(cmd.Data)
response.Type = "AddChainsResponse"
needSaveConfig = true
case "UpdateChains":
err = w.handleUpdateChain(cmd.Data)
response.Type = "UpdateChainsResponse"
needSaveConfig = true
case "DeleteChains":
err = w.handleDeleteChain(cmd.Data)
response.Type = "DeleteChainsResponse"
needSaveConfig = true
// Limiter 相关命令
case "AddLimiters":
err = w.handleAddLimiter(cmd.Data)
response.Type = "AddLimitersResponse"
needSaveConfig = true
case "UpdateLimiters":
err = w.handleUpdateLimiter(cmd.Data)
response.Type = "UpdateLimitersResponse"
needSaveConfig = true
case "DeleteLimiters":
err = w.handleDeleteLimiter(cmd.Data)
response.Type = "DeleteLimitersResponse"
needSaveConfig = true
case "AddCLimiters":
err = w.handleAddCLimiter(cmd.Data)
response.Type = "AddCLimitersResponse"
needSaveConfig = true
case "UpdateCLimiters":
err = w.handleUpdateCLimiter(cmd.Data)
response.Type = "UpdateCLimitersResponse"
needSaveConfig = true
case "DeleteCLimiters":
err = w.handleDeleteCLimiter(cmd.Data)
response.Type = "DeleteCLimitersResponse"
needSaveConfig = true
// TCP Ping 诊断命令(只读,不需要保存配置)
case "TcpPing":
@@ -876,7 +861,6 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
case "SetProtocol":
err = w.handleSetProtocol(cmd.Data)
response.Type = "SetProtocolResponse"
needSaveConfig = true
// 升级 Agent 命令(异步执行,不需要保存配置)
case "UpgradeAgent":
@@ -895,20 +879,6 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
response.Type = "UnknownCommandResponse"
}
// 只有状态变更命令才保存配置
if needSaveConfig {
if saveErr := saveConfig(); saveErr != nil {
fmt.Printf("❌ 保存配置失败: %v\n", saveErr)
if err == nil {
err = fmt.Errorf("保存配置失败: %v", saveErr)
} else {
err = fmt.Errorf("%v; 保存配置失败: %v", err, saveErr)
}
} else {
fmt.Println("✅ 配置已保存到 gost.json")
}
}
// 发送响应
if err != nil {
response.Success = false