120 lines
5.1 KiB
Go
120 lines
5.1 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
var ErrCurrentSIPPending = errors.New("new SIP revision is pending drain or verified load")
|
|
|
|
// NoteSIPChange durably closes admission before the control queue is ACKed.
|
|
// Superseded notifications cannot roll back an already applied revision.
|
|
func (s *CurrentStore) NoteSIPChange(dispatcherID string, revision int64) error {
|
|
if dispatcherID == "" || revision <= 0 {
|
|
return errors.New("SIP notification requires Dispatcher identity and positive revision")
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err := tx.Exec(`INSERT OR IGNORE INTO dispatcher_state(dispatcher_id) VALUES(?)`, dispatcherID); err != nil {
|
|
return fmt.Errorf("persist SIP notification owner: %w", err)
|
|
}
|
|
var applied, pending int64
|
|
if err := tx.QueryRow(`SELECT applied_sip_revision,pending_sip_revision FROM dispatcher_state WHERE dispatcher_id=?`, dispatcherID).Scan(&applied, &pending); err != nil {
|
|
return fmt.Errorf("read SIP notification state: %w", err)
|
|
}
|
|
if revision <= applied && pending == 0 {
|
|
return tx.Commit()
|
|
}
|
|
if revision > pending {
|
|
pending = revision
|
|
}
|
|
if _, err := tx.Exec(`UPDATE dispatcher_state SET discovery_ready=0,pending_sip_revision=? WHERE dispatcher_id=?`, pending, dispatcherID); err != nil {
|
|
return fmt.Errorf("close admission for pending SIP revision: %w", err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit pending SIP revision: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *CurrentStore) SIPState(dispatcherID string) (applied, pending int64, err error) {
|
|
err = s.db.QueryRow(`SELECT applied_sip_revision,pending_sip_revision FROM dispatcher_state WHERE dispatcher_id=?`, dispatcherID).Scan(&applied, &pending)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("read durable SIP revision state: %w", err)
|
|
}
|
|
return applied, pending, nil
|
|
}
|
|
|
|
// OccupiedCalls includes dispatching, dispatched, and unknown executions.
|
|
// No lease expiry, SIP reload, or broker restart releases an uncertain call.
|
|
func (s *CurrentStore) OccupiedCalls(dispatcherID string) (int64, error) {
|
|
var count int64
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM dispatcher_inbox WHERE dispatcher_id=? AND status IN ('dispatching','dispatched','unknown')`, dispatcherID).Scan(&count); err != nil {
|
|
return 0, fmt.Errorf("count calls before SIP reload: %w", err)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// MarkReadyForSIP opens admission only after all present task bindings match
|
|
// this exact verified SIP revision and any prior SIP calls have drained.
|
|
// The loaded Agent/Asterisk state is independently checked by the caller.
|
|
func (s *CurrentStore) MarkReadyForSIP(dispatcherID string, revision int64) error {
|
|
if dispatcherID == "" || revision <= 0 {
|
|
return errors.New("admission requires a verified SIP revision")
|
|
}
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var applied, pending int64
|
|
err = tx.QueryRow(`SELECT applied_sip_revision,pending_sip_revision FROM dispatcher_state WHERE dispatcher_id=?`, dispatcherID).Scan(&applied, &pending)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return errors.New("cannot open admission without committed discovery")
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("read admission SIP revision: %w", err)
|
|
}
|
|
if revision < applied {
|
|
return fmt.Errorf("SIP revision %d regresses from applied %d", revision, applied)
|
|
}
|
|
if revision < pending {
|
|
return fmt.Errorf("%w: verified revision %d is behind pending %d", ErrCurrentSIPPending, revision, pending)
|
|
}
|
|
var missing int64
|
|
if err := tx.QueryRow(`SELECT COUNT(*) FROM dispatcher_tasks t LEFT JOIN dispatcher_configs c
|
|
ON c.dispatcher_id=t.dispatcher_id AND c.tenant_id=t.tenant_id AND c.task_id=t.task_id
|
|
WHERE t.dispatcher_id=? AND t.present=1 AND t.status<>'stopped'
|
|
AND t.control_state NOT IN ('stopped','stopping')
|
|
AND (c.task_revision IS NULL OR c.task_revision<>t.task_revision OR c.sip_revision<>?)`, dispatcherID, revision).Scan(&missing); err != nil {
|
|
return fmt.Errorf("verify all task bindings for SIP admission: %w", err)
|
|
}
|
|
if missing != 0 {
|
|
return fmt.Errorf("%d assigned tasks lack the verified SIP revision %d", missing, revision)
|
|
}
|
|
if pending != 0 || (applied != 0 && applied != revision) {
|
|
var occupied int64
|
|
if err := tx.QueryRow(`SELECT COUNT(*) FROM dispatcher_inbox WHERE dispatcher_id=? AND status IN ('dispatching','dispatched','unknown')`, dispatcherID).Scan(&occupied); err != nil {
|
|
return fmt.Errorf("check old calls before SIP admission: %w", err)
|
|
}
|
|
if occupied != 0 {
|
|
return fmt.Errorf("%w: %d old or unknown calls have not drained for revision %d", ErrCurrentSIPPending, occupied, revision)
|
|
}
|
|
}
|
|
result, err := tx.Exec(`UPDATE dispatcher_state SET discovery_ready=1,applied_sip_revision=?,pending_sip_revision=0 WHERE dispatcher_id=?`, revision, dispatcherID)
|
|
if err != nil {
|
|
return fmt.Errorf("commit applied SIP revision: %w", err)
|
|
}
|
|
if n, err := result.RowsAffected(); err != nil || n != 1 {
|
|
return fmt.Errorf("cannot open SIP admission without committed discovery: rows=%d %v", n, err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit SIP admission: %w", err)
|
|
}
|
|
return nil
|
|
}
|