128 lines
3.2 KiB
Go
128 lines
3.2 KiB
Go
package demos
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// ErrPoolExhausted : plus de slot libre pour au moins un service requis.
|
|
var ErrPoolExhausted = errors.New("pool de ressources externes épuisé")
|
|
|
|
// Pool : allocation des ressources externes pré-provisionnées.
|
|
type Pool interface {
|
|
// Borrow réserve un slot par service requis, de façon atomique.
|
|
// Rend ErrPoolExhausted si un service n'a plus de slot libre.
|
|
Borrow(demoID string) ([]ExternalResource, error)
|
|
// Return libère tous les slots d'une démo.
|
|
Return(demoID string) error
|
|
// FreeCount renvoie le nombre de slots libres par service.
|
|
FreeCount() (map[string]int, error)
|
|
}
|
|
|
|
// GormPool : Pool adossé à PostgreSQL via GORM (verrou transactionnel).
|
|
type GormPool struct{ db *gorm.DB }
|
|
|
|
func NewGormPool(db *gorm.DB) *GormPool { return &GormPool{db: db} }
|
|
|
|
func (p *GormPool) Borrow(demoID string) ([]ExternalResource, error) {
|
|
var borrowed []ExternalResource
|
|
err := p.db.Transaction(func(tx *gorm.DB) error {
|
|
for _, svc := range PooledServices {
|
|
var r ExternalResource
|
|
// SELECT ... FOR UPDATE SKIP LOCKED : évite la course entre deux démos.
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
|
|
Where("service = ? AND status = ?", svc, ResourceFree).
|
|
First(&r).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrPoolExhausted
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Status = ResourceBorrowed
|
|
r.DemoID = demoID
|
|
if err := tx.Save(&r).Error; err != nil {
|
|
return err
|
|
}
|
|
borrowed = append(borrowed, r)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return borrowed, nil
|
|
}
|
|
|
|
func (p *GormPool) Return(demoID string) error {
|
|
return p.db.Model(&ExternalResource{}).
|
|
Where("demo_id = ?", demoID).
|
|
Updates(map[string]any{"status": ResourceFree, "demo_id": ""}).Error
|
|
}
|
|
|
|
func (p *GormPool) FreeCount() (map[string]int, error) {
|
|
type row struct {
|
|
Service string
|
|
N int
|
|
}
|
|
var rows []row
|
|
err := p.db.Model(&ExternalResource{}).
|
|
Select("service, count(*) as n").
|
|
Where("status = ?", ResourceFree).
|
|
Group("service").Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]int{}
|
|
for _, svc := range PooledServices {
|
|
out[svc] = 0
|
|
}
|
|
for _, r := range rows {
|
|
out[r.Service] = r.N
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// SeedPool crée les slots manquants pour atteindre PoolSizePerService par service.
|
|
// Idempotent : n'ajoute que ce qui manque. SecretRef pointe vers le secret manager.
|
|
func SeedPool(db *gorm.DB) error {
|
|
for _, svc := range PooledServices {
|
|
var n int64
|
|
if err := db.Model(&ExternalResource{}).Where("service = ?", svc).Count(&n).Error; err != nil {
|
|
return err
|
|
}
|
|
for i := int(n) + 1; i <= PoolSizePerService; i++ {
|
|
res := ExternalResource{
|
|
Service: svc,
|
|
Label: placeholderLabel(svc, i),
|
|
SecretRef: "secretref://" + svc + "/slot-" + itoa(i),
|
|
Status: ResourceFree,
|
|
}
|
|
if err := db.Create(&res).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func placeholderLabel(svc string, i int) string {
|
|
return svc + "-slot-" + itoa(i)
|
|
}
|
|
|
|
func itoa(i int) string {
|
|
if i == 0 {
|
|
return "0"
|
|
}
|
|
var b [20]byte
|
|
pos := len(b)
|
|
for i > 0 {
|
|
pos--
|
|
b[pos] = byte('0' + i%10)
|
|
i /= 10
|
|
}
|
|
return string(b[pos:])
|
|
}
|