298 lines
8.8 KiB
Go
298 lines
8.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"errors"
|
|
"linkport/internal/model"
|
|
"net"
|
|
"reflect"
|
|
)
|
|
|
|
// ErrConfigChanged reports a failed live-configuration compare-and-swap.
|
|
var ErrConfigChanged = errors.New("gateway configuration changed; reload and retry")
|
|
|
|
// ConfigUpdate carries the snapshot used to construct Next. The From methods
|
|
// compare Current with the live snapshot before performing any persistence.
|
|
type ConfigUpdate struct {
|
|
Current model.Proxy
|
|
Next model.Proxy
|
|
}
|
|
|
|
type preparedConfigUpdate struct {
|
|
instance *instance
|
|
config model.Proxy
|
|
handler model.Handler
|
|
replacement net.Listener
|
|
}
|
|
|
|
// StartFrom loads and persists the latest enabled configuration while holding
|
|
// the lifecycle lock. The listener is published only after persistence succeeds.
|
|
func (m *Manager) StartFrom(load func() (model.Proxy, error), persist func(model.Proxy) error) (model.Proxy, error) {
|
|
if load == nil || persist == nil {
|
|
return model.Proxy{}, errors.New("load and persist callbacks are required")
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
p, err := load()
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
p.Enabled = true
|
|
prepared, err := m.prepareStartLocked(p)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
if err = persist(p); err != nil {
|
|
prepared.listener.Close()
|
|
if prepared.managementListener != nil {
|
|
prepared.managementListener.Close()
|
|
}
|
|
return p, err
|
|
}
|
|
m.publishStartLocked(prepared)
|
|
return p, nil
|
|
}
|
|
|
|
// StopAndPersist persists the latest disabled snapshot before stopping. A
|
|
// persistence failure leaves a running gateway untouched.
|
|
func (m *Manager) StopAndPersist(id string, load func() (model.Proxy, error), persist func(model.Proxy) error) (model.Proxy, error) {
|
|
if persist == nil {
|
|
return model.Proxy{}, errors.New("persist callback is required")
|
|
}
|
|
m.mu.Lock()
|
|
inst := m.instances[id]
|
|
var p model.Proxy
|
|
var err error
|
|
if inst == nil {
|
|
if load == nil {
|
|
m.mu.Unlock()
|
|
return model.Proxy{}, errors.New("load callback is required for a stopped gateway")
|
|
}
|
|
p, err = load()
|
|
if err != nil {
|
|
m.mu.Unlock()
|
|
return p, err
|
|
}
|
|
} else {
|
|
if inst.stopping {
|
|
m.mu.Unlock()
|
|
return model.Proxy{}, errors.New("gateway is already stopping")
|
|
}
|
|
p = inst.config
|
|
}
|
|
if p.ID != id {
|
|
m.mu.Unlock()
|
|
return p, errors.New("loaded gateway ID does not match")
|
|
}
|
|
p.Enabled = false
|
|
if err = persist(p); err != nil {
|
|
m.mu.Unlock()
|
|
return p, err
|
|
}
|
|
m.snapshots[id] = p
|
|
if inst == nil {
|
|
delete(m.failures, id)
|
|
m.mu.Unlock()
|
|
return p, nil
|
|
}
|
|
inst.config = p
|
|
inst.stopLocked()
|
|
m.mu.Unlock()
|
|
|
|
inst.waitStopped()
|
|
m.mu.Lock()
|
|
if m.instances[id] == inst {
|
|
delete(m.instances, id)
|
|
}
|
|
delete(m.failures, id)
|
|
m.mu.Unlock()
|
|
return p, nil
|
|
}
|
|
|
|
// UpdateConfig persists and publishes one gateway configuration. A stopped
|
|
// gateway is only persisted; a running gateway is updated without revoking
|
|
// connections that already captured the previous configuration snapshot.
|
|
func (m *Manager) UpdateConfig(next model.Proxy, persist func() error) error {
|
|
return m.updateConfigs([]ConfigUpdate{{Next: next}}, false, persist)
|
|
}
|
|
|
|
// UpdateConfigs atomically publishes resolved configuration snapshots after a
|
|
// single persistence commit. Listener changes are bound before persistence, so
|
|
// a bind or persistence failure leaves every running gateway unchanged.
|
|
func (m *Manager) UpdateConfigs(next []model.Proxy, persist func() error) error {
|
|
updates := make([]ConfigUpdate, len(next))
|
|
for idx := range next {
|
|
updates[idx].Next = next[idx]
|
|
}
|
|
return m.updateConfigs(updates, false, persist)
|
|
}
|
|
|
|
// UpdateConfigFrom applies a compare-and-swap update for one gateway.
|
|
func (m *Manager) UpdateConfigFrom(current, next model.Proxy, persist func() error) error {
|
|
return m.UpdateConfigsFrom([]ConfigUpdate{{Current: current, Next: next}}, persist)
|
|
}
|
|
|
|
// UpdateConfigsFrom atomically rejects the entire batch when any running
|
|
// gateway changed after its candidate configuration was constructed.
|
|
func (m *Manager) UpdateConfigsFrom(updates []ConfigUpdate, persist func() error) error {
|
|
return m.updateConfigs(updates, true, persist)
|
|
}
|
|
|
|
func comparableConfig(config model.Proxy) model.Proxy {
|
|
// Enabled controls restart persistence rather than a live connection. A
|
|
// manual Start publishes the pre-commit false value, so it cannot be a CAS
|
|
// token. Hooks are attached to per-connection copies at accept time.
|
|
config.Enabled = false
|
|
config.TargetDialer = nil
|
|
config.PreparedTarget = nil
|
|
// OutboundID is a persistence indirection. Saving an inline non-direct
|
|
// outbound creates an ID, while the resolved transport remains identical.
|
|
config.OutboundID = ""
|
|
if config.Targets != nil {
|
|
targets := make(map[string]model.Target, len(config.Targets))
|
|
for id, target := range config.Targets {
|
|
target.OutboundID = ""
|
|
targets[id] = target
|
|
}
|
|
config.Targets = targets
|
|
}
|
|
return config
|
|
}
|
|
|
|
func (m *Manager) updateConfigs(changes []ConfigUpdate, compareCurrent bool, persist func() error) error {
|
|
if persist == nil {
|
|
return errors.New("persist callback is required")
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
seen := make(map[string]struct{}, len(changes))
|
|
updates := make([]preparedConfigUpdate, 0, len(changes))
|
|
closeReplacements := func() {
|
|
for idx := range updates {
|
|
if updates[idx].replacement != nil {
|
|
updates[idx].replacement.Close()
|
|
updates[idx].replacement = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, change := range changes {
|
|
config := change.Next
|
|
if config.ID == "" {
|
|
return errors.New("gateway ID is required")
|
|
}
|
|
if compareCurrent && change.Current.ID != config.ID {
|
|
return errors.New("current and next gateway IDs must match")
|
|
}
|
|
if _, ok := seen[config.ID]; ok {
|
|
return errors.New("duplicate gateway configuration")
|
|
}
|
|
seen[config.ID] = struct{}{}
|
|
inst := m.instances[config.ID]
|
|
if compareCurrent {
|
|
observed, ok := m.snapshots[config.ID]
|
|
if inst != nil {
|
|
observed, ok = inst.config, true
|
|
}
|
|
if ok {
|
|
if inst == nil {
|
|
// Full configuration replacement is only allowed while every
|
|
// gateway is stopped, so the store may legitimately be newer
|
|
// than this lifecycle snapshot. Enabled still distinguishes a
|
|
// stale update that was queued before StopAndPersist.
|
|
if observed.Enabled != change.Current.Enabled {
|
|
return ErrConfigChanged
|
|
}
|
|
} else if !reflect.DeepEqual(comparableConfig(observed), comparableConfig(change.Current)) {
|
|
return ErrConfigChanged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, change := range changes {
|
|
config := change.Next
|
|
inst := m.instances[config.ID]
|
|
if inst == nil {
|
|
continue
|
|
}
|
|
if inst.stopping {
|
|
closeReplacements()
|
|
return errors.New("gateway is stopping; retry after it stops")
|
|
}
|
|
if config.Type != inst.config.Type {
|
|
closeReplacements()
|
|
return errors.New("gateway protocol cannot change while running")
|
|
}
|
|
if config.ManagementListenPort != inst.config.ManagementListenPort || (inst.managementListener != nil && config.ListenHost != inst.config.ListenHost) {
|
|
closeReplacements()
|
|
return errors.New("stop the gateway before changing RabbitMQ management listen settings")
|
|
}
|
|
if config.ManagementListenPort != 0 && config.ManagementListenPort == config.ListenPort {
|
|
closeReplacements()
|
|
return errors.New("RabbitMQ management and AMQP listen ports must differ")
|
|
}
|
|
handler := m.handlers[config.Type]
|
|
if handler == nil {
|
|
closeReplacements()
|
|
return errors.New("unsupported protocol")
|
|
}
|
|
if !config.HasDestination() {
|
|
closeReplacements()
|
|
return errors.New("select a target before updating the running gateway")
|
|
}
|
|
if err := config.ValidateTargetSelection(); err != nil {
|
|
closeReplacements()
|
|
return err
|
|
}
|
|
if err := config.ValidateGatewayHostKeys(); err != nil {
|
|
closeReplacements()
|
|
return err
|
|
}
|
|
address, err := listenAddress(config.ListenHost, config.ListenPort)
|
|
if err != nil {
|
|
closeReplacements()
|
|
return err
|
|
}
|
|
update := preparedConfigUpdate{instance: inst, config: config, handler: handler}
|
|
if config.ListenHost != inst.config.ListenHost || config.ListenPort != inst.config.ListenPort {
|
|
update.replacement, err = net.Listen("tcp", address)
|
|
if err != nil {
|
|
closeReplacements()
|
|
return listenStartError(address, err)
|
|
}
|
|
}
|
|
updates = append(updates, update)
|
|
}
|
|
|
|
if err := persist(); err != nil {
|
|
closeReplacements()
|
|
return err
|
|
}
|
|
for _, change := range changes {
|
|
if m.instances[change.Next.ID] == nil {
|
|
m.snapshots[change.Next.ID] = change.Next
|
|
}
|
|
}
|
|
for idx := range updates {
|
|
update := &updates[idx]
|
|
inst := update.instance
|
|
published := update.config
|
|
published.Enabled = inst.config.Enabled
|
|
inst.config = published
|
|
m.snapshots[published.ID] = published
|
|
inst.persistent.Reconcile(published)
|
|
delete(m.failures, update.config.ID)
|
|
if update.replacement == nil {
|
|
continue
|
|
}
|
|
old := inst.listener
|
|
inst.listener = update.replacement
|
|
inst.wg.Add(1)
|
|
go m.accept(inst.ctx, inst, update.replacement, update.handler)
|
|
update.replacement = nil
|
|
old.Close()
|
|
}
|
|
return nil
|
|
}
|