Files
go-sip/internal/dispatcher/config_read.go
T

155 lines
6.2 KiB
Go

package dispatcher
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/store"
)
const (
projectConfigTTL = 60 * time.Second
taskDiscoveryTTL = 30 * time.Second
)
type projectConfigKey struct {
tenantID string
taskID string
}
type SIPConfigVerifier interface {
VerifyAppliedSIPConfig(context.Context, configread.Snapshot) error
}
type SIPConfigVerifierFunc func(context.Context, configread.Snapshot) error
func (f SIPConfigVerifierFunc) VerifyAppliedSIPConfig(ctx context.Context, snapshot configread.Snapshot) error {
return f(ctx, snapshot)
}
// LoadProjectConfig fetches a complete response set, persists it atomically,
// then publishes one immutable in-memory snapshot for admission decisions.
func (d *Dispatcher) LoadProjectConfig(ctx context.Context, client *configread.Client, verifier SIPConfigVerifier, taskID, tenantID string) error {
d.configRefreshMu.Lock()
defer d.configRefreshMu.Unlock()
fail := func(err error) error {
d.configMu.Lock()
d.projectConfigs = nil
d.configMu.Unlock()
return err
}
if client == nil || verifier == nil || d.store == nil {
return fail(errors.New("configuration client, applied-SIP verifier, and store are required"))
}
if d.dispatcherID != "" && client.DispatcherID() != d.dispatcherID {
return fail(errors.New("configuration client dispatcher identity mismatch"))
}
assigned, err := d.store.LocalDiscoveredTaskForConfig(d.dispatcherID, taskID, tenantID)
if err != nil {
return fail(fmt.Errorf("authorize task configuration from durable discovery: %w", err))
}
if !d.now().UTC().Before(assigned.DiscoveryObservedAt.Add(taskDiscoveryTTL)) {
return fail(errors.New("task discovery is stale; new configuration admission is closed"))
}
snapshot, err := client.ReadTask(ctx, taskID, tenantID)
if err != nil {
return fail(err)
}
if err := verifier.VerifyAppliedSIPConfig(ctx, snapshot); err != nil {
return fail(fmt.Errorf("verify Agent-applied SIP config: %w", err))
}
assigned, err = d.store.LocalDiscoveredTaskForConfig(d.dispatcherID, taskID, tenantID)
if err != nil {
return fail(fmt.Errorf("recheck task discovery after configuration read: %w", err))
}
if snapshot.TenantKey != assigned.TenantKey || snapshot.TaskRevision != assigned.TaskRevision || snapshot.TaskStatus != assigned.Status {
return fail(errors.New("task configuration and durable discovery disagree"))
}
now := d.now().UTC()
quotaExpiry := minTime(now.Add(projectConfigTTL), snapshot.QuotaValidUntil)
taskExpiry := minTime(now.Add(projectConfigTTL), snapshot.AgentAuthorizationExpiresAt)
sipExpiry := now.Add(projectConfigTTL)
discoveryExpiry := assigned.DiscoveryObservedAt.Add(taskDiscoveryTTL)
snapshot.FetchedAt = now
snapshot.ExpiresAt = minTime(quotaExpiry, taskExpiry, sipExpiry, discoveryExpiry)
if !snapshot.ExpiresAt.After(now) {
return fail(errors.New("configuration response is expired; new admission is closed"))
}
bundle := store.LocalConfigBundle{
DispatcherID: d.dispatcherID, TenantID: snapshot.TenantID, TenantKey: snapshot.TenantKey, TaskID: snapshot.TaskID,
TenantQuotaScope: store.LocalTenantQuotaScope(d.dispatcherID, snapshot.TenantID),
TaskQuotaScope: store.LocalTaskQuotaScope(d.dispatcherID, snapshot.TenantID, snapshot.TaskID),
TenantQuotaLimit: snapshot.TenantMaxConcurrentCalls, TaskQuotaLimit: snapshot.TaskMaxConcurrentCalls,
Snapshots: []store.LocalConfigSnapshotRecord{
{Resource: "sip", Revision: snapshot.SIPRevision, Version: strconv.FormatInt(snapshot.SIPRevision, 10), FetchedAt: now, ExpiresAt: sipExpiry, Body: snapshot.SIP},
{Resource: "task", TenantID: snapshot.TenantID, TaskID: snapshot.TaskID, Revision: snapshot.TaskRevision, Version: strconv.FormatInt(snapshot.TaskRevision, 10), FetchedAt: now, ExpiresAt: taskExpiry, Body: snapshot.Task},
{Resource: "tenant_quota", TenantID: snapshot.TenantID, Revision: snapshot.QuotaRevision, Version: strconv.FormatInt(snapshot.QuotaRevision, 10), FetchedAt: now, ExpiresAt: quotaExpiry, Body: snapshot.TenantQuota},
},
}
if err := d.store.SaveLocalConfigBundle(bundle); err != nil {
return fail(fmt.Errorf("persist configuration response bundle: %w", err))
}
d.configMu.Lock()
if d.projectConfigs == nil {
d.projectConfigs = make(map[projectConfigKey]configread.Snapshot)
}
sharedExpiry := minTime(sipExpiry, discoveryExpiry)
for key, current := range d.projectConfigs {
current.SIP = append(current.SIP[:0], snapshot.SIP...)
current.SIPRevision = snapshot.SIPRevision
current.ExpiresAt = minTime(current.ExpiresAt, sharedExpiry)
if key.tenantID == snapshot.TenantID {
current.TenantQuota = append(current.TenantQuota[:0], snapshot.TenantQuota...)
current.QuotaRevision = snapshot.QuotaRevision
current.TenantMaxConcurrentCalls = snapshot.TenantMaxConcurrentCalls
current.QuotaValidUntil = snapshot.QuotaValidUntil
current.ExpiresAt = minTime(current.ExpiresAt, quotaExpiry)
}
d.projectConfigs[key] = current
}
key := projectConfigKey{tenantID: snapshot.TenantID, taskID: snapshot.TaskID}
d.projectConfigs[key] = cloneConfigSnapshot(snapshot)
d.configMu.Unlock()
return nil
}
func (d *Dispatcher) ProjectConfigSnapshot(taskID, tenantID string) (configread.Snapshot, bool) {
d.configMu.RLock()
snapshot, loaded := d.projectConfigs[projectConfigKey{tenantID: tenantID, taskID: taskID}]
d.configMu.RUnlock()
if !loaded {
return configread.Snapshot{}, false
}
now := d.now()
if !now.Before(snapshot.ExpiresAt) || !now.Before(snapshot.QuotaValidUntil) || !now.Before(snapshot.AgentAuthorizationExpiresAt) {
return configread.Snapshot{}, false
}
return cloneConfigSnapshot(snapshot), true
}
func cloneConfigSnapshot(snapshot configread.Snapshot) configread.Snapshot {
snapshot.SIP = append(snapshot.SIP[:0:0], snapshot.SIP...)
snapshot.Task = append(snapshot.Task[:0:0], snapshot.Task...)
snapshot.TenantQuota = append(snapshot.TenantQuota[:0:0], snapshot.TenantQuota...)
snapshot.TaskAllowedTrunkIDs = append([]string(nil), snapshot.TaskAllowedTrunkIDs...)
return snapshot
}
func minTime(times ...time.Time) time.Time {
if len(times) == 0 {
return time.Time{}
}
minimum := times[0]
for _, candidate := range times[1:] {
if candidate.Before(minimum) {
minimum = candidate
}
}
return minimum
}