I container muoiono quando capita; due ore di lavoro non devono morire con loro
Quarto articolo della serie sul sandbox per agenti. Si sposta l’avanzamento dalla memoria del container a un disco condiviso, poi si elimina di proposito il container che sta lavorando — un altro riprende da pagina 17. Compresa la parte onesta: questo «ripristino automatico» richiede ancora che qualcuno prema qualcosa.
Tre articoli più tardi: i task sono isolati fra loro e l’attesa è passata da 45 secondi a una frazione. Ma una cosa non è cambiata dal primo articolo — l’avanzamento vive ancora nella memoria del container.
Prima significava «se crasha, si perde». Ora è peggio: i container di un pool caldo vengono sostituiti per routine. Li sostituiscono gli aggiornamenti, i guasti delle macchine, la pressione sulle risorse. Sono materiale di consumo per progetto.
La domanda cambia forma: perché mai affidare a un materiale di consumo due ore del proprio lavoro?
Scriverlo altrove: la lavagna del cantiere, non la testa dell’operaio
Immagina un cantiere. L’avanzamento tenuto nella testa di un operaio finisce col cambio turno; l’avanzamento scritto col gesso sulla lavagna appesa al muro lo legge chiunque arrivi dopo.
Il codice fa esattamente questo — appende la lavagna al muro:
# si richiede un disco condiviso da 1GB
kind: PersistentVolumeClaim
spec:
resources:
requests:
storage: 1Gi
---
# lo si monta in ogni container del pool
volumeMounts:
- name: workspace-volume
mountPath: /workspace # è la directory che il container vede
In K8s un disco condiviso come questo si chiama volume persistente. La parola chiave è persistente: sopravvive al container, e il container successivo può montarlo.
Poi due modifiche al lavoro vero e proprio. Dopo ogni pagina, si scrive sulla lavagna:
for i in range(start_page, total_pages):
await asyncio.sleep(1.0) # scarica una pagina
with open(f"/workspace/{task_id}/checkpoint.json", "w") as f:
json.dump({"pages_crawled": i + 1, "total": total_pages}, f) # scrive subito
Prima di iniziare, si legge cosa ha lasciato il turno precedente:
start_page = 0
if os.path.exists(ckpt_file):
start_page = json.load(open(ckpt_file)).get("pages_crawled", 0)
print(f"checkpoint trovato, si riprende da pagina {start_page}")
Tutto qui. Niente transazioni distribuite, niente logiche di compensazione: un file JSON.
Ora si elimina di proposito il container che lavora
Questo passo si giudica solo rompendo qualcosa; leggere il codice non dimostra nulla.
A metà di un task, si elimina il container che sta lavorando:
kubectl delete pod <quello-che-sta-lavorando>
Si rilancia lo stesso task e si guarda il log del nuovo container:
checkpoint trovato, si riprende da pagina 17
[worker-b] task-a3f9 progress: 18/30
[worker-b] task-a3f9 progress: 19/30
Le prime diciassette pagine non vengono rifatte. Il container è cambiato, il lavoro è proseguito dov’era.
Una conseguenza che sfugge facilmente: l’endpoint di avanzamento ora può interrogare qualunque container vivo, perché montano tutti lo stesso disco. Lo stato non appartiene più a un container in particolare.
È questa la sensazione concreta della frase spesso citata: i container sono bestiame, i workspace sono animali domestici. Il bestiame è sostituibile; gli animali domestici hanno un nome e vanno accuditi.
Cosa manca ancora a questa versione (la parte onesta)
Uno, il «ripristino automatico» richiede una persona. Il codice lo dice chiaramente:
# nessun task_id significa task nuovo; con task_id significa ripresa
In altre parole, nulla sta sorvegliando i task che si sono fermati. Qualcuno deve accorgersene e rimandare la richiesta con l’ID originale. Non è failover automatico, è «ripristinabile» — e fra le due cose c’è di mezzo un intero meccanismo di monitoraggio e ritentativi. Colmare quel divario è il tema dei due articoli successivi.
Due, quel disco non si monta su tre container in un cluster vero. È richiesto come ReadWriteOnce, cioè una macchina alla volta. L’esperimento locale ha esattamente una macchina, quindi tre container che lo condividono non danno problemi; su un cluster multi-macchina reale, i container sulla seconda macchina non riescono a montarlo. Una condivisione vera richiede storage con accesso multi-macchina: NFS, o un servizio di file su cloud.
Tre, scrivere dopo ogni pagina è un compromesso, non un’impostazione predefinita. Scrivere più spesso perde meno in caso di crash e costa di più in I/O. In un sistema reale quella frequenza si ricava da quanto costa rifare un’unità di lavoro.
Cosa stabilisce questo passo
Una volta che l’avanzamento esce dal container, il container è davvero un esecutore sostituibile. Ma si noti cosa manca adesso: nessuno nel sistema sa se un task si è bloccato — il dispatcher consegna il lavoro e se ne dimentica.
E quando il pool è pieno, sa ancora dire soltanto «nessuna capacità disponibile».
Sono lo stesso vuoto: manca un posto che ricordi quale lavoro è ancora in sospeso. Il prossimo articolo lo colma — ed è lì che ho scoperto di non aver costruito la cosa che credevo di star costruendo.
In una riga: se lo stato sopravvive al container, il container può permettersi di morire.
Codice: agent-sandbox-oss/lab4.
- Kubernetes
- AI Agent
- Tolleranza ai guasti
- Gestione dello stato
- Architettura