refactor!: separate runtime releases from panel source

This commit is contained in:
2026-10-14 15:05:10 +08:00
parent 20635ffaec
commit a2dd6ee27d
51 changed files with 221 additions and 2017 deletions
+1 -95
View File
@@ -7,29 +7,10 @@ import (
)
var ErrInstanceLimit = errors.New("instance limit reached")
var ErrIntentState = errors.New("intent is no longer waiting")
type Intent struct {
ID string `json:"id"`
UID string `json:"uid"`
Name string `json:"name"`
Version string `json:"version"`
BuildID string `json:"build_id"`
InstanceID string `json:"instance_id"`
Status string `json:"status"`
Error string `json:"error"`
Host string `json:"-"`
Created int64 `json:"created_at"`
}
func (s *Store) EnsureIntents() error {
_, err := s.DB.Exec(`CREATE TABLE IF NOT EXISTS creation_intents(id TEXT PRIMARY KEY,uid TEXT NOT NULL,name TEXT NOT NULL,version TEXT NOT NULL,build_id TEXT NOT NULL DEFAULT '',instance_id TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,error TEXT NOT NULL DEFAULT '',host TEXT NOT NULL,created INTEGER NOT NULL); CREATE INDEX IF NOT EXISTS intents_owner ON creation_intents(uid,status);`)
return err
}
func reserveSlot(tx *sql.Tx, uid string) error {
var count int
if err := tx.QueryRow(`SELECT (SELECT count(*) FROM instances WHERE uid=?) + (SELECT count(*) FROM creation_intents WHERE uid=? AND status='waiting')`, uid, uid).Scan(&count); err != nil {
if err := tx.QueryRow(`SELECT count(*) FROM instances WHERE uid=?`, uid).Scan(&count); err != nil {
return err
}
if count >= 2 {
@@ -38,22 +19,6 @@ func reserveSlot(tx *sql.Tx, uid string) error {
return nil
}
func (s *Store) ReserveIntent(i Intent) error {
tx, err := s.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err = reserveSlot(tx, i.UID); err != nil {
return err
}
_, err = tx.Exec(`INSERT INTO creation_intents(id,uid,name,version,status,host,created) VALUES(?,?,?,?,'waiting',?,?)`, i.ID, i.UID, i.Name, i.Version, i.Host, time.Now().Unix())
if err != nil {
return err
}
return tx.Commit()
}
func (s *Store) ReserveInstance(i Instance) error {
tx, err := s.DB.Begin()
if err != nil {
@@ -72,62 +37,3 @@ func insertInstance(tx *sql.Tx, i Instance) error {
_, err := tx.Exec(`INSERT INTO instances VALUES(?,?,?,?,?,?,?,?)`, i.ID, i.UID, i.Name, i.Version, i.ImageID, i.Status, i.ContainerID, time.Now().Unix())
return err
}
func (s *Store) ClaimIntent(id string, i Instance) error {
tx, err := s.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
result, err := tx.Exec(`UPDATE creation_intents SET status='creating',instance_id=? WHERE id=? AND uid=? AND version=? AND status='waiting'`, i.ID, id, i.UID, i.Version)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return ErrIntentState
}
if err = insertInstance(tx, i); err != nil {
return err
}
return tx.Commit()
}
func (s *Store) CancelIntent(id, uid string) error {
r, err := s.DB.Exec(`UPDATE creation_intents SET status='cancelled' WHERE id=? AND uid=? AND status='waiting'`, id, uid)
if err != nil {
return err
}
n, err := r.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return ErrIntentState
}
return nil
}
func (s *Store) Intents(uid string) ([]Intent, error) {
query := `SELECT id,uid,name,version,build_id,instance_id,status,error,host,created FROM creation_intents`
var args []any
if uid != "" {
query += ` WHERE uid=?`
args = append(args, uid)
}
query += ` ORDER BY created,id`
rows, err := s.DB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := []Intent{}
for rows.Next() {
var i Intent
if err = rows.Scan(&i.ID, &i.UID, &i.Name, &i.Version, &i.BuildID, &i.InstanceID, &i.Status, &i.Error, &i.Host, &i.Created); err != nil {
return nil, err
}
out = append(out, i)
}
return out, rows.Err()
}
+11 -39
View File
@@ -7,26 +7,19 @@ import (
"testing"
)
func TestIntentReservationConcurrencyAndRestart(t *testing.T) {
func TestInstanceReservationConcurrencyAndRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "store.db")
s, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err = s.EnsureIntents(); err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
result := make(chan error, 30)
for n := 0; n < 30; n++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
if n%2 == 0 {
result <- s.ReserveIntent(Intent{ID: ID(), UID: "u", Name: "test", Version: "1.2.3"})
} else {
result <- s.ReserveInstance(Instance{ID: ID(), UID: "u"})
}
result <- s.ReserveInstance(Instance{ID: ID(), UID: "u"})
}(n)
}
wg.Wait()
@@ -48,47 +41,26 @@ func TestIntentReservationConcurrencyAndRestart(t *testing.T) {
t.Fatal(err)
}
defer s.Close()
if err = s.ReserveIntent(Intent{ID: ID(), UID: "u"}); !errors.Is(err, ErrInstanceLimit) {
if err = s.ReserveInstance(Instance{ID: ID(), UID: "u"}); !errors.Is(err, ErrInstanceLimit) {
t.Fatalf("restart lost reservation: %v", err)
}
}
func TestIntentClaimCancelAndVersion(t *testing.T) {
func TestRetiredIntentsDoNotReserveInstanceSlots(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "store.db"))
if err != nil {
t.Fatal(err)
}
defer s.Close()
if err = s.EnsureIntents(); err != nil {
if _, err := s.DB.Exec("CREATE TABLE creation_intents(uid TEXT,status TEXT); INSERT INTO creation_intents VALUES('u','waiting'),('u','creating')"); err != nil {
t.Fatal(err)
}
in := Intent{ID: ID(), UID: "u", Version: "1.2.3"}
if err = s.ReserveIntent(in); err != nil {
t.Fatal(err)
for n := 0; n < 2; n++ {
if err := s.ReserveInstance(Instance{ID: ID(), UID: "u"}); err != nil {
t.Fatal(err)
}
}
i := Instance{ID: ID(), UID: "u", Version: "1.2.4"}
if err = s.ClaimIntent(in.ID, i); !errors.Is(err, ErrIntentState) {
t.Fatal(err)
}
if err = s.CancelIntent(in.ID, "other"); !errors.Is(err, ErrIntentState) {
t.Fatal(err)
}
i.Version = in.Version
if err = s.ClaimIntent(in.ID, i); err != nil {
t.Fatal(err)
}
if err = s.CancelIntent(in.ID, in.UID); !errors.Is(err, ErrIntentState) {
t.Fatal(err)
}
if err = s.ReserveIntent(Intent{ID: "second", UID: "u"}); err != nil {
t.Fatal(err)
}
if err = s.ReserveIntent(Intent{ID: "third", UID: "u"}); !errors.Is(err, ErrInstanceLimit) {
t.Fatal(err)
}
if err = s.CancelIntent("second", "u"); err != nil {
t.Fatal(err)
}
if err = s.ReserveIntent(Intent{ID: "third", UID: "u"}); err != nil {
if err := s.ReserveInstance(Instance{ID: ID(), UID: "u"}); !errors.Is(err, ErrInstanceLimit) {
t.Fatal(err)
}
}