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 }