Files
go-sip/internal/store/sip_snapshot.go
T

104 lines
3.5 KiB
Go

package store
import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math"
"git.ipao.vip/rogee/go-sip/internal/configread"
"git.ipao.vip/rogee/go-sip/internal/contract"
)
// PrepareSIP assigns a durable LOCAL load generation. SaaS revision is optional
// and never rejects a newer fetched snapshot. Identical retries reuse the same
// pending generation; changed content gets a new generation, including restart.
func (s *Store) PrepareSIP(sip configread.SIP) (configread.SIP, error) {
if sip.DispatcherID == "" {
return sip, errors.New("SIP snapshot has no owner")
}
sourceRevision := sip.Revision
sip.Revision = 0
content, err := json.Marshal(sip)
if err != nil {
return sip, err
}
if err = contract.ValidateCurrent("config-read", content); err != nil {
return sip, errors.New("SIP snapshot violates current contract")
}
tx, err := s.db.Begin()
if err != nil {
return sip, err
}
defer tx.Rollback()
var foreign string
identityErr := tx.QueryRow(`SELECT dispatcher_id FROM dispatcher_state WHERE dispatcher_id<>? LIMIT 1`, sip.DispatcherID).Scan(&foreign)
if identityErr == nil {
return sip, errors.New("SQLite is bound to another Dispatcher")
}
if !errors.Is(identityErr, sql.ErrNoRows) {
return sip, identityErr
}
if _, err = tx.Exec(`INSERT OR IGNORE INTO dispatcher_state(dispatcher_id) VALUES(?)`, sip.DispatcherID); err != nil {
return sip, err
}
var applied, pending, previous int64
var old []byte
if err = tx.QueryRow(`SELECT applied_sip_revision,pending_sip_revision FROM dispatcher_state WHERE dispatcher_id=?`, sip.DispatcherID).Scan(&applied, &pending); err != nil {
return sip, err
}
err = tx.QueryRow(`SELECT revision,content_json FROM dispatcher_sip_snapshots WHERE dispatcher_id=?`, sip.DispatcherID).Scan(&previous, &old)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return sip, err
}
if errors.Is(err, sql.ErrNoRows) && applied > 0 {
// A pre-existing task binding is an authoritative loaded snapshot, not a
// guessed default. No existing execution/recovery data is cleared or rewritten.
var raw []byte
e := tx.QueryRow(`SELECT snapshot_json FROM dispatcher_configs WHERE dispatcher_id=? LIMIT 1`, sip.DispatcherID).Scan(&raw)
if e != nil && !errors.Is(e, sql.ErrNoRows) {
return sip, e
}
if e == nil {
var saved struct {
SIP configread.SIP `json:"sip"`
}
if json.Unmarshal(raw, &saved) != nil {
return sip, errors.New("saved SIP snapshot is malformed")
}
saved.SIP.Revision = 0
old, e = json.Marshal(saved.SIP)
if e != nil {
return sip, e
}
previous = applied
}
}
generation := max(previous, applied)
if generation == 0 {
generation = max(int64(1), sourceRevision)
} else if !bytes.Equal(old, content) {
if generation == math.MaxInt64 {
return sip, errors.New("local SIP generation exhausted")
}
generation++
}
generation = max(generation, pending)
if _, err = tx.Exec(`INSERT INTO dispatcher_sip_snapshots(dispatcher_id,revision,content_json) VALUES(?,?,?) ON CONFLICT(dispatcher_id) DO UPDATE SET revision=excluded.revision,content_json=excluded.content_json`, sip.DispatcherID, generation, content); err != nil {
return sip, fmt.Errorf("persist local SIP snapshot: %w", err)
}
if generation > applied {
pending = max(pending, generation)
}
if _, err = tx.Exec(`UPDATE dispatcher_state SET discovery_ready=0,pending_sip_revision=? WHERE dispatcher_id=?`, pending, sip.DispatcherID); err != nil {
return sip, err
}
if err = tx.Commit(); err != nil {
return sip, err
}
sip.Revision = generation
return sip, nil
}