Orchestrateur go pour lancer la démo et mettre en place plusieurs instances docker
This commit is contained in:
177
demo/orchestrator/sessions.go
Normal file
177
demo/orchestrator/sessions.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionStatus reflete l'etat du cycle de vie d'un trio de session.
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
StatusStarting SessionStatus = "starting"
|
||||
StatusReady SessionStatus = "ready"
|
||||
StatusFailed SessionStatus = "failed"
|
||||
)
|
||||
|
||||
// Session represente une demo isolee pour un visiteur.
|
||||
// CoreHost est le hostname Docker interne du conteneur core de cette session
|
||||
// (ex: "demo-abc123-core"), vers lequel l'orchestrateur proxifie les /api/*.
|
||||
type Session struct {
|
||||
ID string
|
||||
CreatedAt time.Time
|
||||
Status SessionStatus
|
||||
CoreHost string
|
||||
Err string
|
||||
// proxy et proxyOnce : reverse-proxy cache, cree au plus une fois via
|
||||
// sync.Once (evite la race entre deux requetes concurrentes sur la meme
|
||||
// session). proxy est typee any pour ne pas contraindre sessions.go a
|
||||
// importer net/http/httputil.
|
||||
proxy any
|
||||
proxyOnce sync.Once
|
||||
}
|
||||
|
||||
// Manager gere le cycle de vie des sessions (creation, acces, cleanup).
|
||||
// Thread-safe : le mutex protege la map contre les acces concurrents (HTTP
|
||||
// handlers + goroutine de GC).
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
sessions map[string]*Session
|
||||
docker *DockerClient
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
func newManager(docker *DockerClient, cfg *Config) *Manager {
|
||||
return &Manager{
|
||||
sessions: make(map[string]*Session),
|
||||
docker: docker,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCapacity est retournee quand MAX_SESSIONS est atteint.
|
||||
var ErrCapacity = errors.New("demo at capacity")
|
||||
|
||||
// Create reserve un slot et lance le spawn des conteneurs en arriere-plan.
|
||||
// Retourne immediatement avec Status=starting. L'etat bascule a "ready" quand
|
||||
// les conteneurs sont up et que core repond a /api/config.
|
||||
func (m *Manager) Create(ctx context.Context) (*Session, error) {
|
||||
m.mu.Lock()
|
||||
if len(m.sessions) >= m.cfg.MaxSessions {
|
||||
m.mu.Unlock()
|
||||
return nil, ErrCapacity
|
||||
}
|
||||
id := newShortID()
|
||||
sess := &Session{
|
||||
ID: id,
|
||||
CreatedAt: time.Now(),
|
||||
Status: StatusStarting,
|
||||
CoreHost: "demo-" + id + "-core",
|
||||
}
|
||||
m.sessions[id] = sess
|
||||
m.mu.Unlock()
|
||||
|
||||
// Spawn asynchrone : l'utilisateur voit immediatement la page "preparation".
|
||||
go func() {
|
||||
spawnCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
if err := m.docker.SpawnTrio(spawnCtx, id, m.cfg); err != nil {
|
||||
log.Printf("session %s spawn failed: %v", id, err)
|
||||
m.mu.Lock()
|
||||
sess.Status = StatusFailed
|
||||
sess.Err = err.Error()
|
||||
m.mu.Unlock()
|
||||
// Nettoyage best-effort des conteneurs partiellement crees.
|
||||
_ = m.docker.KillTrio(context.Background(), id)
|
||||
return
|
||||
}
|
||||
// Attente que core reponde (sinon proxy retourne 502 aux premieres requetes).
|
||||
if m.docker.WaitReady(spawnCtx, id, 90*time.Second) {
|
||||
m.mu.Lock()
|
||||
sess.Status = StatusReady
|
||||
m.mu.Unlock()
|
||||
log.Printf("session %s ready", id)
|
||||
} else {
|
||||
log.Printf("session %s never became ready", id)
|
||||
m.mu.Lock()
|
||||
sess.Status = StatusFailed
|
||||
sess.Err = "timeout waiting for core"
|
||||
m.mu.Unlock()
|
||||
_ = m.docker.KillTrio(context.Background(), id)
|
||||
}
|
||||
}()
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
// Get renvoie la session associee a un ID, ou nil si elle n'existe plus.
|
||||
func (m *Manager) Get(id string) *Session {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.sessions[id]
|
||||
}
|
||||
|
||||
// RunGC boucle toutes les minutes pour supprimer les sessions expirees.
|
||||
// A lancer en goroutine au demarrage.
|
||||
func (m *Manager) RunGC(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.gcOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) gcOnce() {
|
||||
cutoff := time.Now().Add(-m.cfg.SessionTTL)
|
||||
m.mu.Lock()
|
||||
var expired []string
|
||||
for id, s := range m.sessions {
|
||||
if s.CreatedAt.Before(cutoff) {
|
||||
expired = append(expired, id)
|
||||
}
|
||||
}
|
||||
for _, id := range expired {
|
||||
delete(m.sessions, id)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, id := range expired {
|
||||
log.Printf("session %s expired, killing containers", id)
|
||||
if err := m.docker.KillTrio(context.Background(), id); err != nil {
|
||||
log.Printf("kill %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupOrphans tue les conteneurs demo-* qui ne correspondent a aucune
|
||||
// session en memoire. Appele au demarrage pour gerer un redemarrage brutal.
|
||||
func (m *Manager) CleanupOrphans(ctx context.Context) {
|
||||
ids, err := m.docker.ListSessionIDs(ctx)
|
||||
if err != nil {
|
||||
log.Printf("list orphans: %v", err)
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
log.Printf("cleaning orphan session %s", id)
|
||||
_ = m.docker.KillTrio(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
// newShortID genere un identifiant hexadecimal de 32 caracteres (128 bits).
|
||||
// 128 bits d'entropie rendent les collisions et le brute-force statistiquement
|
||||
// impossibles, meme si un attaquant pouvait tenter des millions de cookies.
|
||||
func newShortID() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user