194 lines
9.1 KiB
Go
194 lines
9.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
defaultCPAHeartbeatTimeout = 3 * time.Second
|
|
defaultCPACancelBound = 5 * time.Second
|
|
defaultReclaimGrace = 5 * time.Second
|
|
defaultCleanupInterval = 5 * time.Second
|
|
defaultReleaseFlushInterval = 250 * time.Millisecond
|
|
defaultReleaseMaxBackoff = 2 * time.Second
|
|
defaultBusyRetryMin = 250 * time.Millisecond
|
|
defaultBusyRetryMax = time.Second
|
|
maxCredentialConcurrencyLimit int64 = 1_000_000
|
|
)
|
|
|
|
// CredentialConcurrencyConfig controls the credential concurrency lifecycle managed by Home.
|
|
type CredentialConcurrencyConfig struct {
|
|
LifecycleConfigRevision int64 `yaml:"lifecycle-config-revision" json:"lifecycle-config-revision"`
|
|
ObservationBarrierRevision int64 `yaml:"observation-barrier-revision" json:"observation-barrier-revision"`
|
|
CPAHeartbeatTimeout time.Duration `yaml:"cpa-heartbeat-timeout" json:"cpa-heartbeat-timeout"`
|
|
CPACancelBound time.Duration `yaml:"cpa-cancel-bound" json:"cpa-cancel-bound"`
|
|
ReclaimGrace time.Duration `yaml:"reclaim-grace" json:"reclaim-grace"`
|
|
CleanupInterval time.Duration `yaml:"cleanup-interval" json:"cleanup-interval"`
|
|
ReleaseFlushInterval time.Duration `yaml:"release-flush-interval" json:"release-flush-interval"`
|
|
ReleaseMaxBackoff time.Duration `yaml:"release-max-backoff" json:"release-max-backoff"`
|
|
BusyRetryMin time.Duration `yaml:"busy-retry-min" json:"busy-retry-min"`
|
|
BusyRetryMax time.Duration `yaml:"busy-retry-max" json:"busy-retry-max"`
|
|
MaxLimit int64 `yaml:"max-limit" json:"max-limit"`
|
|
|
|
lifecycleConfigRevisionPresent bool
|
|
observationBarrierRevisionPresent bool
|
|
cpaHeartbeatTimeoutPresent bool
|
|
cpaCancelBoundPresent bool
|
|
reclaimGracePresent bool
|
|
cleanupIntervalPresent bool
|
|
releaseFlushIntervalPresent bool
|
|
releaseMaxBackoffPresent bool
|
|
busyRetryMinPresent bool
|
|
busyRetryMaxPresent bool
|
|
maxLimitPresent bool
|
|
}
|
|
|
|
// UnmarshalYAML preserves field presence so only absent lifecycle values receive legacy defaults.
|
|
func (c *CredentialConcurrencyConfig) UnmarshalYAML(value *yaml.Node) error {
|
|
type rawCredentialConcurrencyConfig struct {
|
|
LifecycleConfigRevision int64 `yaml:"lifecycle-config-revision"`
|
|
ObservationBarrierRevision int64 `yaml:"observation-barrier-revision"`
|
|
CPAHeartbeatTimeout time.Duration `yaml:"cpa-heartbeat-timeout"`
|
|
CPACancelBound time.Duration `yaml:"cpa-cancel-bound"`
|
|
ReclaimGrace time.Duration `yaml:"reclaim-grace"`
|
|
CleanupInterval time.Duration `yaml:"cleanup-interval"`
|
|
ReleaseFlushInterval time.Duration `yaml:"release-flush-interval"`
|
|
ReleaseMaxBackoff time.Duration `yaml:"release-max-backoff"`
|
|
BusyRetryMin time.Duration `yaml:"busy-retry-min"`
|
|
BusyRetryMax time.Duration `yaml:"busy-retry-max"`
|
|
MaxLimit int64 `yaml:"max-limit"`
|
|
}
|
|
|
|
var raw rawCredentialConcurrencyConfig
|
|
if errDecode := value.Decode(&raw); errDecode != nil {
|
|
return errDecode
|
|
}
|
|
|
|
*c = CredentialConcurrencyConfig{
|
|
LifecycleConfigRevision: raw.LifecycleConfigRevision,
|
|
ObservationBarrierRevision: raw.ObservationBarrierRevision,
|
|
CPAHeartbeatTimeout: raw.CPAHeartbeatTimeout,
|
|
CPACancelBound: raw.CPACancelBound,
|
|
ReclaimGrace: raw.ReclaimGrace,
|
|
CleanupInterval: raw.CleanupInterval,
|
|
ReleaseFlushInterval: raw.ReleaseFlushInterval,
|
|
ReleaseMaxBackoff: raw.ReleaseMaxBackoff,
|
|
BusyRetryMin: raw.BusyRetryMin,
|
|
BusyRetryMax: raw.BusyRetryMax,
|
|
MaxLimit: raw.MaxLimit,
|
|
lifecycleConfigRevisionPresent: credentialConcurrencyFieldPresent(value, "lifecycle-config-revision"),
|
|
observationBarrierRevisionPresent: credentialConcurrencyFieldPresent(value, "observation-barrier-revision"),
|
|
cpaHeartbeatTimeoutPresent: credentialConcurrencyFieldPresent(value, "cpa-heartbeat-timeout"),
|
|
cpaCancelBoundPresent: credentialConcurrencyFieldPresent(value, "cpa-cancel-bound"),
|
|
reclaimGracePresent: credentialConcurrencyFieldPresent(value, "reclaim-grace"),
|
|
cleanupIntervalPresent: credentialConcurrencyFieldPresent(value, "cleanup-interval"),
|
|
releaseFlushIntervalPresent: credentialConcurrencyFieldPresent(value, "release-flush-interval"),
|
|
releaseMaxBackoffPresent: credentialConcurrencyFieldPresent(value, "release-max-backoff"),
|
|
busyRetryMinPresent: credentialConcurrencyFieldPresent(value, "busy-retry-min"),
|
|
busyRetryMaxPresent: credentialConcurrencyFieldPresent(value, "busy-retry-max"),
|
|
maxLimitPresent: credentialConcurrencyFieldPresent(value, "max-limit"),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func credentialConcurrencyFieldPresent(value *yaml.Node, field string) bool {
|
|
if value == nil || value.Kind != yaml.MappingNode {
|
|
return false
|
|
}
|
|
for index := 0; index+1 < len(value.Content); index += 2 {
|
|
if value.Content[index].Value == field {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WithDefaults applies the lifecycle defaults required for compatibility with older Home versions.
|
|
func (c CredentialConcurrencyConfig) WithDefaults() CredentialConcurrencyConfig {
|
|
if !c.cpaHeartbeatTimeoutPresent && c.CPAHeartbeatTimeout == 0 {
|
|
c.CPAHeartbeatTimeout = defaultCPAHeartbeatTimeout
|
|
}
|
|
if !c.cpaCancelBoundPresent && c.CPACancelBound == 0 {
|
|
c.CPACancelBound = defaultCPACancelBound
|
|
}
|
|
if !c.reclaimGracePresent && c.ReclaimGrace == 0 {
|
|
c.ReclaimGrace = defaultReclaimGrace
|
|
}
|
|
if !c.cleanupIntervalPresent && c.CleanupInterval == 0 {
|
|
c.CleanupInterval = defaultCleanupInterval
|
|
}
|
|
if !c.releaseFlushIntervalPresent && c.ReleaseFlushInterval == 0 {
|
|
c.ReleaseFlushInterval = defaultReleaseFlushInterval
|
|
}
|
|
if !c.releaseMaxBackoffPresent && c.ReleaseMaxBackoff == 0 {
|
|
c.ReleaseMaxBackoff = defaultReleaseMaxBackoff
|
|
}
|
|
if !c.busyRetryMinPresent && c.BusyRetryMin == 0 {
|
|
c.BusyRetryMin = defaultBusyRetryMin
|
|
}
|
|
if !c.busyRetryMaxPresent && c.BusyRetryMax == 0 {
|
|
c.BusyRetryMax = defaultBusyRetryMax
|
|
}
|
|
if !c.maxLimitPresent && c.MaxLimit == 0 {
|
|
c.MaxLimit = maxCredentialConcurrencyLimit
|
|
}
|
|
return c
|
|
}
|
|
|
|
// ValidateCredentialConcurrency validates values intrinsic to a credential concurrency configuration.
|
|
func ValidateCredentialConcurrency(cfg CredentialConcurrencyConfig) error {
|
|
if cfg.LifecycleConfigRevision < 0 || (cfg.lifecycleConfigRevisionPresent && cfg.LifecycleConfigRevision == 0) {
|
|
return fmt.Errorf("lifecycle configuration revision must be positive when present")
|
|
}
|
|
if cfg.ObservationBarrierRevision < 0 {
|
|
return fmt.Errorf("observation barrier revision must not be negative")
|
|
}
|
|
if cfg.CPAHeartbeatTimeout <= 0 || cfg.CPACancelBound <= 0 || cfg.ReclaimGrace <= 0 || cfg.CleanupInterval <= 0 {
|
|
return fmt.Errorf("credential concurrency lifecycle durations must be positive")
|
|
}
|
|
if cfg.ReleaseFlushInterval <= 0 || cfg.ReleaseMaxBackoff <= 0 || cfg.BusyRetryMin <= 0 || cfg.BusyRetryMax <= 0 {
|
|
return fmt.Errorf("credential concurrency limiter durations must be positive")
|
|
}
|
|
if cfg.ReleaseMaxBackoff < cfg.ReleaseFlushInterval {
|
|
return fmt.Errorf("credential concurrency release max backoff must not be less than release flush interval")
|
|
}
|
|
if cfg.BusyRetryMin%time.Millisecond != 0 || cfg.BusyRetryMax%time.Millisecond != 0 {
|
|
return fmt.Errorf("credential concurrency busy retry durations must be whole milliseconds")
|
|
}
|
|
if cfg.BusyRetryMax < cfg.BusyRetryMin {
|
|
return fmt.Errorf("credential concurrency busy retry max must not be less than busy retry min")
|
|
}
|
|
if cfg.MaxLimit < 1 || cfg.MaxLimit > maxCredentialConcurrencyLimit {
|
|
return fmt.Errorf("credential concurrency max limit must be between 1 and %d", maxCredentialConcurrencyLimit)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateCredentialConcurrencyLifecycle verifies the Home lifecycle timing safety invariant.
|
|
func ValidateCredentialConcurrencyLifecycle(nodeHeartbeatTimeout time.Duration, cfg CredentialConcurrencyConfig) error {
|
|
if nodeHeartbeatTimeout <= 0 {
|
|
return fmt.Errorf("credential concurrency lifecycle durations must be positive")
|
|
}
|
|
if errValidate := ValidateCredentialConcurrency(cfg); errValidate != nil {
|
|
return errValidate
|
|
}
|
|
left, leftOverflow := addCredentialConcurrencyDuration(nodeHeartbeatTimeout, cfg.ReclaimGrace)
|
|
right, rightOverflow := addCredentialConcurrencyDuration(cfg.CPAHeartbeatTimeout, cfg.CPACancelBound)
|
|
if leftOverflow || rightOverflow {
|
|
return fmt.Errorf("credential concurrency lifecycle timing safety invariant overflows")
|
|
}
|
|
if left <= right {
|
|
return fmt.Errorf("node heartbeat timeout plus reclaim grace must exceed CPA heartbeat timeout plus cancel bound")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addCredentialConcurrencyDuration(left time.Duration, right time.Duration) (time.Duration, bool) {
|
|
if right > 0 && left > time.Duration(1<<63-1)-right {
|
|
return 0, true
|
|
}
|
|
return left + right, false
|
|
}
|