T10 §D/§F.4: T9 no definía el eslabón cuando no hay ruta al presupuesto
(partición). Ahora:
- router::RebalanceResult { Routed{edges} | Unreachable }. rebalance_to es
TRANSACCIONAL: si las vecinas directas cubren el déficit aplica el flujo
(Routed); si no, no mueve nada y devuelve Unreachable. Nunca a medias, nunca
bloquea.
- SpendOutcome + spend_with_rebalance: gasta local, o repone por rebalanceo, o
ante Unreachable RECHAZA con spendable = presupuesto local (fail-safe). Nunca
sobregira ni cuelga.
Tests: el 60/60 particionado con [50,50] → Unreachable → Rejected{spendable:50}
(gasta ≤50, valor≥0); con ruta se cubre por rebalanceo. Tests de T9b migrados a
la nueva firma.
AVAILABILITY.md (§F.6): el precio honesto del escrow — bajo partición la réplica
se limita a su presupuesto local (CAP: safety > availability); diffuse lo mitiga,
no lo elimina; sin promesas de liveness, solo safety. 61 tests, clippy limpio.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2.8 KiB
AVAILABILITY — el precio honesto del escrow (T10b, §D/§F.6)
El escrow cambia el falso-verde del detector (T8: "particionado → libre") por un límite de disponibilidad real y seguro. Este archivo lo nombra, en vez de fingir disponibilidad ilimitada. Un consejero de coordinación soberano debe poder decir "bajo esta partición, este nodo puede gastar hasta X y no más".
Respaldado por router::tests::particion_sin_ruta_es_fail_safe y
gasto_con_ruta_se_cubre_por_rebalanceo.
La regla
try_spend/spend_with_rebalancenunca sobregiran y nunca cuelgan.- Si el gasto excede el presupuesto local y hay ruta a excedente en las
vecinas, se cubre por rebalanceo edge-local →
Spent. - Si no hay ruta (partición),
rebalance_todevuelveUnreachable(sin mover nada) y el gasto se rechaza conRejected { spendable }: la réplica puede gastar como mucho su presupuesto local, garantizandovalor ≥ K.
El límite, en números
Cuenta de 100, saldo ≥ 0, reparto [50, 50]:
| Grafo | Réplica 0 pide | Resultado | Disponibilidad efectiva de 0 |
|---|---|---|---|
0—1 (conectada) |
60 | Spent (50 local + 10 de 1) |
hasta 100 (todo el slack, si 1 cede) |
∅ (particionada) |
60 | Rejected { spendable: 50 } |
exactamente 50 (su presupuesto local) |
∅ (particionada) |
50 | Spent |
50 |
Bajo partición, la réplica está limitada a su presupuesto local. No es un bug: es el precio correcto de no sobregirar jamás. El detector de T8 "resolvía" esto mintiendo (decía libre y sobregiraba); el escrow lo resuelve diciendo la verdad.
Qué mitiga diffuse (y qué no)
BudgetRouter::diffuse suaviza el presupuesto antes de que llegue la
partición: reparte el slack para que ninguna réplica quede con presupuesto muy por
debajo de su demanda esperada. Reduce la probabilidad y la severidad de un
Rejected — pero no lo elimina:
- Si una réplica particionada demanda más que todo el slack que le tocó, ni el mejor reparto la salva: no hay derechos que gastar sin coordinar.
diffuseconservaΣ b(no crea disponibilidad, solo la redistribuye).
Ningún mecanismo seguro puede eliminar el límite. Es el teorema CAP en su forma cotidiana: bajo partición, o sacrificas consistencia (sobregiras, el bug de T8) o sacrificas disponibilidad (te limitas a tu presupuesto, el escrow). El escrow elige lo segundo, explícitamente.
Sin promesas de liveness
Este documento no promete que un Rejected se convierta en Spent en un
tiempo acotado: eso depende de que la partición sane y de que haya excedente que
rutear, cosas fuera del control del mecanismo. Lo que sí se garantiza es
safety: nunca un sobregiro, nunca un cuelgue. Liveness es best-effort vía
diffuse proactivo y rebalanceo al reconectar.