mirror of
https://github.com/openziti/ziti.git
synced 2026-10-23 20:26:05 +00:00
6c20e68038
- applies the router.link.v1 heartbeat settings (sendInterval, checkInterval, closeUnresponsiveTimeout) and payload/ack sender queue sizes at runtime, replacing the hardcoded 10s/1s link heartbeat intervals and the ignored fields; new links take them at bind time, established links are re-tuned in place through the channel HeartbeatControl, and queue sizes reach new links only - publishes heartbeat settings as an immutable generation in one atomic swap, so no reader pairs one generation's interval with another's timeout, and has each link enforce the timeout belonging to its own intervals - pushes a generation and its intervals as one step under the link's lock and ignores a generation at or below the one applied, so an overtaken update cannot roll a link back; reconciles every channel of a link, including a split link whose channels bind on either side of a config change, to the newest generation - adopts the first registered settings explicitly, since a router with no managed link config publishes its defaults at generation 0 - re-arms a link's liveness deadline when its generation changes, so age accumulated under the previous intervals cannot close a healthy link when a timeout is shortened - honors a closeUnresponsiveTimeout below 30s, which previously sat behind the unhealthy threshold; treats a zero timeout as closing nothing, and reports a closing link unhealthy - scales the unhealthy threshold with the intervals, to max(30s, 2 x (send + check)): reporting unhealthy poisons the latency metric the controller routes on, so a fixed 30s priced healthy links with a long send interval out of routing every cycle - validates heartbeat timing on the controller and the router: durations must be positive, their sum representable, and the timeout at least 10s and greater than sendInterval + checkInterval; a timeout under two heartbeat cycles is accepted with a warning - adds a per-config-type semantic validator registry to ConfigManager, run after schema validation, and moves the router.link config definition to common/config/routerlink so the controller applies the same rules - treats a link section in router YAML, of any content, as local config that takes precedence over the controller's for the whole router.link type, since the loader defaults every field and so now records presence separately; warns at startup when that section has no listeners or dialers - translates local heartbeat timing leniently, since a local failure stops the router from starting: keeps an explicit zero, raises a timeout below the floor to it, and falls back to the defaults for other invalid timing, each with a warning - stamps UpdateLinkListeners with the config generation it came from and has the controller drop superseded sets, which a multi-underlay control channel can deliver out of order and which would otherwise send peers to withdrawn addresses - logs every successful link config apply, including those that dispatch no work, and pins that a queue-size-only change triggers no GC pass - deprecates split links, documents the feature in the changelog and the router config design doc, and brings both up to date Builds on the channel/v5 HeartbeatControl from openziti/channel#265.
185 lines
5.6 KiB
Go
185 lines
5.6 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/openziti/ziti/v2/common/config/routerlink"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_cmd_pb"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/command"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
"github.com/openziti/ziti/v2/controller/fields"
|
|
"github.com/openziti/ziti/v2/controller/models"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
"go.etcd.io/bbolt"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// ConfigDataValidator applies rules to a config's data that its JSON schema
|
|
// can't express: relationships between fields, combinations of values, anything
|
|
// needing more than per-field checks. Registered against a config type name and
|
|
// run on create and update, after schema validation has passed.
|
|
//
|
|
// Return a field error (errorz.NewFieldError) to have the REST layer report the
|
|
// offending field; a plain error is reported against the data as a whole.
|
|
type ConfigDataValidator func(data map[string]interface{}) error
|
|
|
|
func NewConfigManager(env Env) *ConfigManager {
|
|
manager := &ConfigManager{
|
|
baseEntityManager: newBaseEntityManager[*Config, *db.Config](env, env.GetStores().Config),
|
|
dataValidators: map[string]ConfigDataValidator{},
|
|
}
|
|
manager.impl = manager
|
|
|
|
RegisterManagerDecoder[*Config](env, manager)
|
|
|
|
manager.RegisterDataValidator(routerlink.ConfigTypeV1, validateRouterLinkConfigData)
|
|
|
|
return manager
|
|
}
|
|
|
|
// validateRouterLinkConfigData applies the router.link rules its JSON schema
|
|
// can't express, notably heartbeat timing, using the router's own validation.
|
|
func validateRouterLinkConfigData(data map[string]interface{}) error {
|
|
js, err := json.Marshal(data)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to re-encode config data: %w", err)
|
|
}
|
|
cfg, err := routerlink.ParseConfig(string(js))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return cfg.Validate()
|
|
}
|
|
|
|
type ConfigManager struct {
|
|
baseEntityManager[*Config, *db.Config]
|
|
|
|
// dataValidators is keyed by config type name, which is stable across networks
|
|
// where ids are not. Written only during controller init, so reads need no lock.
|
|
dataValidators map[string]ConfigDataValidator
|
|
}
|
|
|
|
// RegisterDataValidator installs a semantic validator for the named config
|
|
// type, to run after schema validation on every create and update. Call during
|
|
// controller init; it is not safe to call once requests are being served.
|
|
// Registering a second validator for the same type replaces the first.
|
|
func (self *ConfigManager) RegisterDataValidator(configTypeName string, v ConfigDataValidator) {
|
|
self.dataValidators[configTypeName] = v
|
|
}
|
|
|
|
// validateData runs the validator registered for configTypeName, if any.
|
|
func (self *ConfigManager) validateData(configTypeName string, data map[string]interface{}) error {
|
|
if v, found := self.dataValidators[configTypeName]; found {
|
|
return v(data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *ConfigManager) NewModelEntity() *Config {
|
|
return &Config{}
|
|
}
|
|
|
|
func (self *ConfigManager) Create(entity *Config, ctx *change.Context) error {
|
|
return DispatchCreate[*Config](self, entity, ctx)
|
|
}
|
|
|
|
func (self *ConfigManager) ApplyCreate(cmd *command.CreateEntityCommand[*Config], ctx boltz.MutateContext) error {
|
|
_, err := self.createEntity(cmd.Entity, ctx)
|
|
return err
|
|
}
|
|
|
|
func (self *ConfigManager) Update(entity *Config, checker fields.UpdatedFields, ctx *change.Context) error {
|
|
return DispatchUpdate[*Config](self, entity, checker, ctx)
|
|
}
|
|
|
|
func (self *ConfigManager) ApplyUpdate(cmd *command.UpdateEntityCommand[*Config], ctx boltz.MutateContext) error {
|
|
var checker boltz.FieldChecker = self
|
|
if cmd.UpdatedFields != nil {
|
|
checker = &AndFieldChecker{first: self, second: cmd.UpdatedFields}
|
|
}
|
|
return self.updateEntity(cmd.Entity, checker, ctx)
|
|
}
|
|
|
|
func (self *ConfigManager) Read(id string) (*Config, error) {
|
|
modelEntity := &Config{}
|
|
if err := self.readEntity(id, modelEntity); err != nil {
|
|
return nil, err
|
|
}
|
|
return modelEntity, nil
|
|
}
|
|
|
|
func (self *ConfigManager) readInTx(tx *bbolt.Tx, id string) (*Config, error) {
|
|
modelEntity := &Config{}
|
|
if err := self.readEntityInTx(tx, id, modelEntity); err != nil {
|
|
return nil, err
|
|
}
|
|
return modelEntity, nil
|
|
}
|
|
|
|
func (self *ConfigManager) IsUpdated(field string) bool {
|
|
return !strings.EqualFold(field, "type")
|
|
}
|
|
|
|
func (self *ConfigManager) Marshall(entity *Config) ([]byte, error) {
|
|
tags, err := edge_cmd_pb.EncodeTags(entity.Tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := json.Marshal(entity.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg := &edge_cmd_pb.Config{
|
|
Id: entity.Id,
|
|
Name: entity.Name,
|
|
ConfigTypeId: entity.TypeId,
|
|
Data: data,
|
|
Tags: tags,
|
|
}
|
|
|
|
return proto.Marshal(msg)
|
|
}
|
|
|
|
func (self *ConfigManager) Unmarshall(bytes []byte) (*Config, error) {
|
|
msg := &edge_cmd_pb.Config{}
|
|
if err := proto.Unmarshal(bytes, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := map[string]interface{}{}
|
|
if err := json.Unmarshal(msg.Data, &data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Config{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: msg.Id,
|
|
Tags: edge_cmd_pb.DecodeTags(msg.Tags),
|
|
},
|
|
Name: msg.Name,
|
|
TypeId: msg.ConfigTypeId,
|
|
Data: data,
|
|
}, nil
|
|
}
|