@@ -0,0 +1,127 @@
|
||||
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:])
|
||||
}
|
||||
Reference in New Issue
Block a user