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:]) }