Files
orchard/internal/worker/endpoint/set.go
T

110 lines
2.1 KiB
Go

package endpoint
import (
"slices"
"sync"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"go.uber.org/zap"
)
// Set owns the endpoint listeners associated with one VM.
type Set struct {
logger *zap.SugaredLogger
endpoints map[string]*endpoint
lastDesired []v1.EndpointSpec
started bool
mtx sync.Mutex
}
func NewSet(logger *zap.SugaredLogger) *Set {
return &Set{
logger: logger,
endpoints: make(map[string]*endpoint),
}
}
func (set *Set) Start() {
set.mtx.Lock()
defer set.mtx.Unlock()
set.stopLocked()
set.started = true
}
func (set *Set) Reconcile(
desired []v1.EndpointSpec,
bindTarget BindTarget,
) []v1.EndpointStatus {
set.mtx.Lock()
defer set.mtx.Unlock()
// Ignore reconciliation until the owning VM starts the endpoint set
if !set.started {
return nil
}
// Recreate the entire endpoint set on any change to avoid conflicts
// between current and desired worker-port assignments
if !v1.SemanticallyEqual(set.lastDesired, desired) {
set.stopLocked()
set.lastDesired = slices.Clone(desired)
}
if len(desired) == 0 {
return nil
}
// Reuse healthy unchanged endpoints and recreate every other endpoint
statuses := make([]v1.EndpointStatus, 0, len(desired))
for _, spec := range desired {
name := spec.Name
if current := set.endpoints[name]; current != nil {
status := current.status()
if status.State == v1.EndpointStateListening {
statuses = append(statuses, status)
continue
}
delete(set.endpoints, name)
current.close()
}
current, err := newEndpoint(spec, bindTarget, set.logger)
if err != nil {
statuses = append(statuses, v1.EndpointStatus{
Name: name,
Protocol: spec.Protocol,
State: v1.EndpointStateError,
Message: err.Error(),
})
continue
}
set.endpoints[name] = current
statuses = append(statuses, current.status())
}
return statuses
}
func (set *Set) Stop() {
set.mtx.Lock()
defer set.mtx.Unlock()
set.started = false
set.stopLocked()
}
func (set *Set) stopLocked() {
for _, current := range set.endpoints {
current.close()
}
clear(set.endpoints)
}