Files
Loki/internal/loki/web_internal_lease.go
T
Claude 27ce303d7f API interne du bail du moteur pour la conversation tenue par Rust
Le verrou de génération (conv.Generating) est partagé par le chat, les
tâches planifiées, le bench et l'optimiseur. Une conversation tenue par la
façade doit prendre ce verrou-là : sinon une tâche partirait vers le même
moteur en plein tour.

/internal/v1/engine/lease prend le verrou (409 avec la raison du refus et
la tâche en cours), /renew le prolonge et remonte un stop passé par le Go,
/release le rend (seulement s'il le tient encore), /engine/stop arrête un
détenteur Go. Sans renouvellement le bail expire : une façade tuée en plein
tour ne garde pas le moteur. Reset et les bascules de discussion rompent le
bail comme ils libèrent un tour Go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-15 09:59:16 +00:00

197 lines
5.6 KiB
Go

package loki
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
// API interne du bail du moteur (lot 6 de la migration Rust).
//
// Le verrou de génération (conv.Generating) est partagé : tour de chat, tâche
// planifiée (RunAutonomous), bench et optimiseur (measureLease), et quatre
// sondes isGenerating (optimiseur, VRAM, projets, liste des discussions). Une
// conversation tenue par la façade Rust doit prendre CE verrou-là, sans quoi
// une tâche planifiée partirait en plein tour vers le même moteur.
//
// POST /internal/v1/engine/lease {"ttl_ms":N} → 200 {"lease":id,"ttl_ms":N}
// 409 {"error":…,"task_id":…,"task_name":…}
// POST /internal/v1/engine/lease/renew {"lease":id} → 200 {"stop":bool} · 410 bail perdu
// POST /internal/v1/engine/lease/release {"lease":id} → 204 (idempotent)
// POST /internal/v1/engine/stop → 204 : arrête le détenteur Go
// (tâche, bench, optimiseur)
//
// Le bail expire s'il n'est pas renouvelé : une façade tuée en plein tour ne
// laisse pas le moteur réservé pour toujours. Reset et les bascules de
// discussion côté Go le rompent (renew → 410), comme ils libèrent un tour Go.
// « stop » : le bouton stop est passé par le Go (conv.Stop) — la façade arrête
// son tour.
const (
extLeaseDefaultTTL = 30 * time.Second
extLeaseMinTTL = 5 * time.Second
extLeaseMaxTTL = 5 * time.Minute
internalLeaseMax = 4 << 10
)
// extLeaseState : le bail tenu par la façade. Protégé par conv.mu.
type extLeaseState struct {
id string
stop bool
ttl time.Duration
timer *time.Timer
}
// extLeaseClearLocked oublie le bail de la façade. c.mu détenu.
func (c *Conversation) extLeaseClearLocked() {
if c.extLease != nil {
c.extLease.timer.Stop()
c.extLease = nil
}
}
// extLeaseAcquire prend le verrou de génération pour la façade.
func (c *Conversation) extLeaseAcquire(ttl time.Duration) (string, error) {
if err := tuneGuard(); err != nil {
return "", err
}
c.mu.Lock()
defer c.mu.Unlock()
if c.Generating {
return "", ErrBusy
}
b, err := randBytes(16)
if err != nil {
return "", err
}
st := &extLeaseState{id: fmt.Sprintf("%x", b), ttl: ttl}
st.timer = time.AfterFunc(ttl, func() { c.extLeaseExpire(st) })
c.Generating = true
c.cancel = func() {
c.mu.Lock()
st.stop = true
c.mu.Unlock()
}
c.extLease = st
return st.id, nil
}
// extLeaseExpire rend le verrou d'un bail que la façade n'a pas renouvelé.
func (c *Conversation) extLeaseExpire(st *extLeaseState) {
c.mu.Lock()
defer c.mu.Unlock()
if c.extLease != st {
return
}
c.extLease = nil
c.Generating, c.cancel = false, nil
fmt.Fprintf(os.Stderr, "[lease] bail de la façade expiré sans renouvellement (%s) : verrou du moteur rendu\n", st.ttl)
}
// extLeaseRenew prolonge le bail ; ok=false s'il n'est plus le bail courant.
func (c *Conversation) extLeaseRenew(id string) (stop, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
st := c.extLease
if st == nil || st.id != id {
return false, false
}
st.timer.Reset(st.ttl)
return st.stop, true
}
// extLeaseRelease rend le verrou, si ce bail le tient encore.
func (c *Conversation) extLeaseRelease(id string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.extLease == nil || c.extLease.id != id {
return
}
c.extLeaseClearLocked()
c.Generating, c.cancel = false, nil
}
func readLeaseBody(w http.ResponseWriter, r *http.Request, v any) bool {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
return false
}
raw, err := io.ReadAll(io.LimitReader(r.Body, internalLeaseMax+1))
if err != nil || len(raw) > internalLeaseMax {
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "corps illisible"})
return false
}
if len(raw) > 0 && json.Unmarshal(raw, v) != nil {
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "JSON illisible"})
return false
}
return true
}
func handleInternalLease(w http.ResponseWriter, r *http.Request) {
var in struct {
TTLms int64 `json:"ttl_ms"`
}
if !readLeaseBody(w, r, &in) {
return
}
ttl := extLeaseDefaultTTL
if in.TTLms > 0 {
ttl = min(max(time.Duration(in.TTLms)*time.Millisecond, extLeaseMinTTL), extLeaseMaxTTL)
}
id, err := conv.extLeaseAcquire(ttl)
if err != nil {
if err == ErrBusy {
err = conv.busyReason() // dit QUI occupe le moteur
}
out := map[string]any{"error": err.Error()}
conv.mu.Lock()
if conv.runningTaskID != "" {
out["task_id"], out["task_name"] = conv.runningTaskID, conv.runningTaskName
}
conv.mu.Unlock()
sendJSON(w, http.StatusConflict, out)
return
}
sendJSON(w, http.StatusOK, map[string]any{"lease": id, "ttl_ms": ttl.Milliseconds()})
}
func handleInternalLeaseRenew(w http.ResponseWriter, r *http.Request) {
var in struct {
Lease string `json:"lease"`
}
if !readLeaseBody(w, r, &in) {
return
}
stop, ok := conv.extLeaseRenew(in.Lease)
if !ok {
sendJSON(w, http.StatusGone, map[string]any{"error": "bail perdu"})
return
}
sendJSON(w, http.StatusOK, map[string]any{"stop": stop})
}
func handleInternalLeaseRelease(w http.ResponseWriter, r *http.Request) {
var in struct {
Lease string `json:"lease"`
}
if !readLeaseBody(w, r, &in) {
return
}
conv.extLeaseRelease(in.Lease)
w.WriteHeader(http.StatusNoContent)
}
func handleInternalEngineStop(w http.ResponseWriter, r *http.Request) {
var in struct{}
if !readLeaseBody(w, r, &in) {
return
}
conv.Stop()
w.WriteHeader(http.StatusNoContent)
}