155 lines
6.2 KiB
Go
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
|
|
}
|