Un cerrojo casi nunca se escribe: se hereda. Alguien vio dos escrituras al mismo sitio, puso un mutex, y a partir de ahí el mutex se convirtió en parte del paisaje. Nadie lo quita porque nadie sabe qué se rompería, y no lo sabe porque en ningún sitio está escrito qué propiedad protege. La consecuencia es un sistema que paga latencia y pierde paralelismo por una razón que ya no consta.
Hay dos resultados que convierten esa pregunta en una decisión con respuesta. El primero es el teorema CALM, que traslada la pregunta del almacenamiento al programa: «A program has a consistent, coordination-free distributed implementation if and only if it is monotonic» (Hellerstein y Alvaro, 2019). El segundo es la invariant confluence de Bailis y colaboradores, que hace lo mismo desde la base de datos y con la ventaja de operar sobre invariantes que un desarrollador puede enunciar: «A globally I-valid system can execute a set of transactions T with coordination-freedom, transactional availability, convergence if and only if T is ℐ-confluent with respect to I» (Bailis et al., 2015).
La tesis de este ensayo es la lectura práctica de ambos: la necesidad de coordinar no es una propiedad del dato, sino del invariante que se quiere mantener sobre él. Dos escrituras concurrentes al mismo sitio pueden ser inofensivas; una escritura a un sitio y una lectura de otro pueden no serlo. La pregunta correcta nunca es «¿escriben los dos aquí?», sino «¿qué afirmación deja de ser cierta si estos dos avanzan sin mirarse?». Cuando esa afirmación no existe, el cerrojo es folclore y se puede borrar.
Conviene enunciar también los límites, porque las dos fuentes los enuncian solas. CALM «falls short of being a constructive result—it does not actually tell us how to write consistent, coordination-free distributed systems»: dice cuándo se puede, no cómo. Y la I-confluencia va acompañada de una advertencia que corta de raíz la lectura triunfalista: «simply because these invariants are ℐ-confluent does not mean that all execution strategies will scale well: for example, using locking would not be coordination-free». Ser I-confluente habilita una implementación sin coordinación; no la escribe.
ALLAN serializa en tres sitios visibles. Se examinan aquí uno por uno con el mismo método: nombrar el invariante, clasificarlo, y concluir.
El cerrojo de workspace del enjambre
El motor swarm_v3 mantiene un gestor de cerrojos por ejecución
(swarm_loop_v3.go:2611-2614) construido dentro de Run
(swarm_loop_v3.go:415). Antes de ejecutar una tool, el llamante pide
lockFor(writeLocks, mutates). Si mutates es falso no se toma nada; si es
cierto y no hay claves declaradas, se toma una única clave literal:
keys = []string{"workspace"} (swarm_loop_v3.go:2625-2627). Las claves se
ordenan globalmente antes de adquirirse (normalizedLockKeys,
swarm_loop_v3.go:2649-2662), lo que hace imposible el interbloqueo por orden
inverso de adquisición. Esa parte está bien y no se discute.
Lo que se discute es el predicado. mutates lo decide toolMutates
(swarm_loop_v3.go:2664-2676): serializa todo lo que no esté anotado como
solo-lectura por su servidor. El índice se construye en
readOnlyToolIndexFor a partir de Tool.ReadOnly(), que es exactamente
t.Annotations != nil && t.Annotations.ReadOnlyHint (mcp/protocol.go:41-43).
Ese predicado divide el mundo en «lee» y «escribe». CALM lo divide en otro sitio: en si la conclusión de una rama puede ser retractada por otra. Escribir no es el problema; retractar lo es. En álgebra relacional, la línea la traza la diferencia de conjuntos: «we can allow each machine to employ selection, projection, intersection, join and transitive closure (the monotonic operators of relational algebra), but not set-difference (the sole non-monotonic operator)». Añadir un hecho es monótono aunque sea una escritura; borrarlo o sobrescribirlo no lo es.
Y resulta que el propio protocolo MCP publica el eje correcto. En la definición
de ToolAnnotations, destructiveHint significa: «If true, the tool may
perform destructive updates to its environment. If false, the tool performs only
additive updates.» Additive updates es, literalmente, monotonía. ALLAN declara
ese campo en su struct (mcp/protocol.go:33) y no lo lee en ninguna parte:
la única aparición fuera de la declaración es la tool mail/sendEmail, que lo
fija a true (mcp/builtin.go:362-364). El campo que responde a la pregunta
está parseado y muerto; el que se usa responde a otra pregunta.
El coste se puede contar. De las 19 tools nativas del motor, 9 no llevan
anotación alguna (edit/createDirectory, edit/createFile, edit/editFiles,
execute/runInTerminal, todo/create, todo/update, todo/delete,
agent/create, agent/generate), y por tanto las 9 serializan, más
mail/sendEmail. Con conectores de terceros la proporción no se ha medido, pero
solo puede ir en la misma dirección: la anotación es opcional y su ausencia,
según el criterio del motor, equivale a «serializa».
Aplicando el criterio una a una:
todo/create inserta un elemento en un conjunto con identificador generado
localmente: NewID mezcla un nonce de crypto/rand con SHA-256
(todo/todo.go:60-65). Eso es exactamente el caso que la tabla 2 de Bailis
clasifica como «Uniqueness (choose some value) → Yes»: si el sistema elige el
valor, la unicidad se mantiene sin coordinación. Dos todo/create concurrentes
conmutan y no pueden invalidarse. Serializarlos es folclore. Demostrable.
edit/createFile es el caso opuesto y por poco: su descripción dice «Fails if
the file already exists» (mcp/builtin.go:313). Es una inserción bajo un
invariante de unicidad con valor elegido por el cliente, la fila que Bailis
marca «Uniqueness (choose specific value) → No». Necesita coordinación de
verdad. Pero la necesita sobre esa ruta, no sobre el workspace entero: dos
createFile a rutas distintas son tan independientes como dos todo/create.
edit/editFiles («Create or replace», mcp/builtin.go:321) es una
sobreescritura ciega: retracta el contenido anterior. No monótona, sin matices.
execute/runInTerminal tiene efecto arbitrario y desconocido, y ahí el
conservadurismo es la única respuesta honesta —además coincide con el default
del propio protocolo, que es destructiveHint: true.
De ahí sale la primera conclusión, y es de las que quitan código: el predicado
de serialización debe ser destructiveHint != false en lugar de
readOnlyHint != true, y la clave del cerrojo debe salir de los argumentos de
la llamada, no de su nombre. La monotonía no es una propiedad de la tool: es una
propiedad de la tool aplicada a un argumento. El cambio es una relajación
estricta —los no anotados siguen serializando, porque el default del protocolo
los da por destructivos— y libera exactamente a los servidores que se molestan
en declarar que solo añaden.
Falta la parte incómoda. El gestor de cerrojos es un map[string]*sync.Mutex
creado por invocación de Run (swarm_loop_v3.go:415), es decir, en memoria y
por ejecución. No excluye a otra ejecución del mismo proceso, ni a otra réplica,
ni a un run anidado: tenant.agent/delegate no lleva anotaciones
(app/tenant_mcp.go:46), luego cuenta como mutante, luego el llamante retiene
"workspace" durante toda la delegación síncrona mientras el run anidado —con
su propio gestor de cerrojos— hace lo que quiera sobre el mismo directorio.
Dentro de un run, además, el root nunca compite con sus workers: runSpawnBatch
lanza el lote y espera en wg.Wait() antes de devolver el control
(swarm_loop_v3.go:1526), de modo que la contención real se da únicamente entre
workers del mismo lote.
Un cerrojo que se llama "workspace" y no protege el workspace es peor que no
tenerlo: induce a razonar como si el recurso estuviera protegido. Y la
maquinaria para hacerlo bien está escrita a medias. swarmV3SpawnSpec lleva
WriteLocks (swarm_loop_v3.go:239), el prompt del spawn se lo anuncia al
modelo —«Declare write_locks for any shared resource the worker may mutate;
workers holding overlapping locks serialize» (swarm_loop_v3.go:2270)— y
FlowStepIR también lo declara (flows/types.go:93). Pero el rediseño del
contrato de ejecución constata que «write_locks existe en FlowStepIR, pero
no llega automáticamente al swarmV3SpawnSpec que gobierna los locks reales del
worker» y lo clasifica como «Concurrencia mutante sin la serialización
declarada» (swarm-v3-execution-contract-redesign.md:182-184 y :277). Hoy las
únicas claves efectivas son las que el modelo se inventa en su spawn.
El SELECT FOR UPDATE de la cartera
En Economics, cada mutación de la cartera pasa por _lock_state, que toma la
fila (tenant, period) con .with_for_update() (wallet.py:181-188). Hay cinco
llamantes: reserve, extend, settle, release y record_platform_work
(wallet.py:406, :475, :541, :564, :583). El plan de implementación lo
enuncia como el corazón de la transacción de apertura de misión
(economics-implementation-plan.md:217-226), y el modelo lo repite en el
docstring de la tabla: «This is THE row that every mission opening locks»
(db/models.py:86).
Aquí la teoría dice que sí, y decirlo es tan parte del ejercicio como decir que
no. El invariante de reserve es un techo: consumed + reserved ≤ granted,
comprobado en wallet.py:409 y :420, y el efecto es un incremento
(wallet.py:427). En la tabla 2 de Bailis eso es la fila «< (Counter) |
Increment | No», y el texto lo enuncia sin ambigüedad: «a row-level
"greater-than" (>) threshold invariant is ℐ-confluent for counter increment and
assign (←) but not decrement (Claims 11, 13), while a row-level "less-than" (<)
threshold invariant is ℐ-confluent for counter decrement and assign but not
increment». Dos reservas concurrentes que localmente caben pueden no caber
juntas; su merge es inválido. El cerrojo no es folclore: es el caso canónico.
Ahora, la precisión importa, porque reserve y extend son dos de los cinco
llamantes. Los otros tres no están del mismo lado.
release decrementa reserved (wallet.py:565): un decremento bajo un techo
< es I-confluente por la misma frase citada. settle mueve reserved a
consumed (wallet.py:542-543) y no comprueba nada: cuando el consumo real
supera lo retenido, lo debita igualmente y registra el exceso en
overshoot_wu_milli, decisión documentada y deliberada (wallet.py:508-517).
Esto tiene una consecuencia que conviene enunciar en voz alta: el sistema no
mantiene consumed + reserved ≤ granted como invariante global. Lo prueba la
existencia de overage_wu_milli, que es precisamente el reporte de los estados
donde esa desigualdad se ha roto (wallet.py:162-172). Lo que el cerrojo
protege no es el techo, sino un enunciado más estrecho: ninguna reserva se
admite si no cabe en el momento de admitirla.
Lo que settle y release sí necesitan es que la transición de estado de la
reserva ocurra a lo sumo una vez —held → settled | released, comprobada hoy en
Python leyendo reservation.status (wallet.py:527-530, :561-562)—. Eso es
un CAS sobre la fila de la reserva, no un cerrojo sobre la fila de la cartera.
Con el guardián ganado, el ajuste de los contadores es un delta conmutativo que
no requiere leer el estado anterior: UPDATE wallet_state SET reserved = reserved - :held, consumed = consumed + :billable. La diferencia es medible,
porque Postgres documenta la duración: «FOR UPDATE causes the rows retrieved by
the SELECT statement to be locked as though for update. This prevents them from
being locked, modified or deleted by other transactions until the current
transaction ends.» Hoy la fila caliente del tenant queda retenida desde el
SELECT hasta el COMMIT, incluyendo la agregación de grants de
_live_grants_wu_milli (wallet.py:211-217) que ni settle ni release
necesitan. En un tenant activo, con retención de 1 WC por turno según el propio
docstring (wallet.py:508-509), esa es la ruta más transitada del producto.
El tercero es más simple: record_platform_work incrementa platform_wu_milli,
un contador que —dice el comentario— «Never affects the balance»
(wallet.py:576). Un contador que no participa de ningún invariante admite
cualquier operación sin coordinación; es el extremo trivial que la propia
literatura nombra: «with no invariants, a user can safely perform any operations
she likes». Toma el cerrojo de tenant sin comprarse nada con él. Y hay un
agravante verificable: en todo el repositorio de Economics la función solo se
invoca desde un test (tests/test_wallet.py:197). Ningún camino de producción
escribe wallet_state.platform_wu_milli, y sin embargo cuatro respuestas de API
lo publican (routers/wallet.py:42, routers/missions.py:223,
routers/admin.py:759, y con otro origen routers/ledger.py:92).
Queda la pregunta interesante: ¿se puede quitar coordinación de reserve, que
es donde de verdad hace falta? La respuesta clásica es el escrow, y su versión
moderna son los bounded counters: «Our approach adapts ideas from escrow
transactions to devise a solution that is decentralized, fault-tolerant and
fast» (Balegas et al., 2015). La idea es repartir el margen del techo en cuotas
locales; mientras la cuota alcanza, la operación es local. ALLAN ya tiene la
forma: personal_minimum_remaining_wu_milli entra en reserve y se materializa
en floor_hold_wu_milli (wallet.py:416-417, :441), y la cascada de topes lo
calcula fuera del cerrojo (ceilings.py:600-652). Lo que no tiene es la
partición: floor_consumed_wu_milli es un único número por tenant
(wallet.py:544), de modo que el gasto contra el mínimo de cada persona sigue
pasando por la misma fila.
Conviene no vender esto como gratis, y menos porque el diseño ya lo pensó y
decidió lo contrario: derivar el mínimo por persona honestamente «significaría
sumar min(spent_i, minimum_i) sobre cada miembro en la ruta de autorización de
cada turno», y ese barrido «es un check que se borra la primera vez que aparece
en una gráfica de latencia» (db/models.py:107-113). La réplica posible es que
un contador por miembro no es un barrido —el total comprometido depende del plan
y del censo, que cambian rara vez, y puede materializarse—, pero eso es una
propuesta, no un hecho, y quien la implemente hereda la carga de la prueba.
También conviene recordar el límite: el escrow no elimina la coordinación,
la localiza; el invariante gasto_i ≤ mínimo_i sigue siendo un techo bajo
incrementos, solo que el conjunto de transacciones en conflicto pasa a ser el de
una persona en vez del de un tenant.
Un último apunte de honestidad sobre esta sección: la fila lleva un campo
version que se incrementa en cuatro sitios (wallet.py:428, :490, :545,
:566) y no se lee jamás en ninguna condición. Es un testigo de concurrencia
optimista instalado y desconectado —justo la alternativa que permitiría cambiar
el cerrojo pesimista por un reintento— y hoy es peso muerto que confunde.
El -p 1 de los tests
El tercer punto no está en el motor sino en cómo se prueba. Los planes de diseño
lo consagran como regla de trabajo: «Tests contra Dragonfly real: ejecutar
paquetes combinados con -p 1 antes de culpar a un cambio (falsos rojos por
carrera de FlushDB entre paquetes son conocidos)»
(event-triggering-implementation-plan.md:17-19), y otro diseño lo repite
(chat-narration-design.md:297-298). La documentación de Go describe la
bandera: «-p n · the number of programs, such as build commands or test
binaries, that can be run in parallel. The default is GOMAXPROCS, normally the
number of CPUs available.» Es decir: la regla renuncia al paralelismo de toda la
suite.
La causa está en dos ficheros. internal/app/dfly_test_helper_test.go y
cmd/allan-server/dispatcher_test.go definen cada uno su newTestDflyClient,
ambos con testDB := 15 por defecto (:33 y :78) y ambos con un FlushDB al
inicio de cada test (:52 y :96). Son unos 69 y unos 26 puntos de llamada
respectivamente contra la misma base lógica. Como go test lanza un binario por
paquete en paralelo, el FlushDB del paquete B borra el estado del test en
vuelo del paquete A.
El invariante aquí es «cada test observa únicamente lo que él mismo escribió», y
el mecanismo elegido para garantizarlo es un borrado. FlushDB es diferencia de
conjuntos: el único operador no monótono. La conclusión de CALM es inmediata y
no admite negociación: mientras el aislamiento se construya por retracción, el
paralelismo entre paquetes exige coordinación, y -p 1 es la coordinación más
burda disponible.
El caso concreto se puede leer entero. En
internal/app/activity_events_integration_test.go, el test invoca
PeekOutboxDragonfly(ctx, client, 50) —que lee el stream global del outbox,
sin prefijo de tenant (responses/outbox_dfly.go:100-101)— y afirma
if len(got) != 3 (:49). Una cuenta exacta sobre una colección compartida es
una consulta no monótona: cualquier escritura ajena la invalida. El comentario
del propio test lo dice sin darse cuenta de lo que está diciendo:
«newTestDflyClient flushes the whole test Dragonfly before this runs, so a plain
session id is enough to isolate this test's events from any other test's
leftovers on the shared outbox stream» (:24-26).
La versión monótona ya existe, en el mismo repositorio, con su justificación
escrita: cmd/allan/main_test.go:46-48 —«Never FLUSHDB here… This test uses
unique run/workspace ids and needs no global cleanup»—. Escribir con
identificadores únicos y afirmar por contención en vez de por igualdad hace que
el conjunto de claves solo crezca, que ninguna conclusión de un test pueda ser
retractada por otro, y que -p 1 deje de tener función. En el caso citado la
reescritura es de dos líneas: los registros que devuelve PeekOutboxDragonfly
llevan RunID y SessionID, así que basta filtrar por el propio y afirmar
sobre el filtrado.
Tres costes, porque no es gratis. El primero: el aislamiento por tenant llega
hasta donde llega el prefijo t:{tenant}: (dfly/keys.go:50-55), y hay media
docena larga de colecciones globales que no lo llevan por diseño
(dfly/keys.go:70-72) —approvals:expiry, bill:records, bill:budgets,
forge:sessions:by-created, mem:intake, http-runs—. Un test que barra una
de ellas seguirá siendo no monótono y seguirá necesitando o coordinación o una
clave particionada. El segundo: sin FlushDB la basura se acumula, y el
almacén no tiene hoy política de retención sobre run:* ni sobre sus trazas.
El tercero: la reescritura de las ~70 aserciones de longitud exacta del paquete
internal/app no es mecánica.
Aun así, la aritmética es favorable. Se cambia una regla que serializa la suite entera, para siempre, por un trabajo acotado de una vez.
Invariantes, enunciados de forma falsable
Cuatro afirmaciones que este ensayo sostiene y que se pueden refutar leyendo el código o midiendo:
Toda tool sin readOnlyHint serializa hoy tras la clave "workspace", y esa
clave es un sync.Mutex en memoria creado por invocación de Run: no excluye a
otro run del mismo proceso ni a otra réplica. Refutable exhibiendo un mecanismo
de exclusión entre runs sobre /workspace.
Ningún camino de producción de Economics escribe
wallet_state.platform_wu_milli; su único escritor
(wallet.record_platform_work) toma el cerrojo de tenant y solo lo llama un
test. Refutable exhibiendo un llamante en producción.
El invariante consumed + reserved ≤ granted no se mantiene: settle lo viola
por diseño y overage_wu_milli existe para reportarlo. Refutable exhibiendo la
comprobación en la ruta de liquidación.
Con FlushDB eliminado de los dos helpers y las aserciones filtradas por
identificador propio, la suite pasa en verde sin -p 1. Es la más falsable de
las cuatro: se comprueba ejecutándola.
Lo que está roto, sin terminar o sin evidencia
write_locks es un campo decorativo en la ruta de flows: declarado en
FlowStepIR (flows/types.go:93), anunciado al modelo en el esquema del spawn
(swarm_loop_v3.go:2281) y sin consumidor que lo lleve al swarmV3SpawnSpec.
El propio corpus de diseño lo tipifica como defecto de propagación de runtime.
destructiveHint se declara (mcp/protocol.go:33), se emite una vez
(mcp/builtin.go:363) y no se lee nunca. Toda la argumentación de la primera
sección depende de que ese campo empiece a leerse; hasta entonces es una
propuesta.
La coordinación de la cartera no tiene prueba que la ejercite. El test que se
llama test_concurrent_missions_cannot_spend_the_same_capacity_twice
(tests/test_wallet.py:135-153) hace tres reservas en serie, y el propio
conftest lo reconoce: «SELECT ... FOR UPDATE is a no-op on SQLite, so a
genuine two-connection race cannot be reproduced» (tests/conftest.py:6-8). Lo
que se verifica es la aritmética, no la exclusión. La afirmación de que el
cerrojo funciona descansa en un E2E de Docker que este ensayo no ha ejecutado.
No hay medición de contención en ninguno de los tres puntos. Ni tiempo de espera
en el mutex de swarm_loop_v3, ni tiempo de retención de la fila de
wallet_state, ni duración comparada de la suite con y sin -p 1. Las
conclusiones de este texto son sobre corrección —qué cerrojo es necesario— y no
sobre rendimiento; cuánto cuesta cada uno es una pregunta abierta que se
responde instrumentando, no razonando.
Y una limitación de método, la que las propias fuentes imponen: la I-confluencia solo es tan buena como el invariante que se le da. «If invariants are incorrectly or incompletely specified, an ℐ-confluent database system may violate application-level correctness.» En un motor agéntico el invariante más importante —qué significa que el trabajo esté bien hecho— no es una restricción de integridad y no se puede enunciar así. Ningún teorema decide por el diseñador si es aceptable que dos workers escriban el mismo informe en orden arbitrario. Lo que estos dos sí hacen es eliminar la excusa de no haber mirado.
Referencias
Hellerstein, J. M. y Alvaro, P. (2019). Keeping CALM: When Distributed Consistency is Easy. arXiv:1901.01930. https://arxiv.org/abs/1901.01930 — «The CALM Theorem shows that the programs that have consistent, coordination-free distributed implementations are exactly the programs that can be expressed in monotonic logic.» · «A program has a consistent, coordination-free distributed implementation if and only if it is monotonic.» · «A program P is monotonic if for any input sets S,T where S⊆T, P(S)⊆P(T).» · «In the relational algebra, we can allow each machine to employ selection, projection, intersection, join and transitive closure (the monotonic operators of relational algebra), but not set-difference (the sole non-monotonic operator).» · «The key insight in CALM is to focus on consistency from the viewpoint of program outcomes rather than the traditional histories of storage mutation.» · «CALM falls short of being a constructive result—it does not actually tell us how to write consistent, coordination-free distributed systems.» · «The CALM conjecture was presented in a keynote talk at PODS 2010 and written up shortly thereafter alongside a number of corollaries. In a subsequent series of papers, Ameloot and colleagues presented a formalization and proof of the CALM Theorem which remains the reference formalism at this time.» — Procedencia de cada frase, corregida: solo la primera está en el resumen de arXiv. Las seis restantes proceden del texto completo recuperado vía ar5iv (https://ar5iv.labs.arxiv.org/html/1901.01930), donde «A program has a consistent, coordination-free distributed implementation if and only if it is monotonic» es el enunciado del Teorema 1 y «A program P is monotonic…» el de la Definición 1. Una nota anterior atribuía las dos primeras frases al resumen; era inexacta para la segunda. La versión de CACM (2020) devolvió HTTP 403 y no se ha usado.
Bailis, P., Fekete, A., Franklin, M. J., Ghodsi, A., Hellerstein, J. M. y Stoica, I. (2015). Coordination Avoidance in Database Systems. PVLDB 8(3), 185-196. https://www.vldb.org/pvldb/vol8/p185-bailis.pdf — «Definition 6 (ℐ-confluence). A set of transactions T is ℐ-confluent with respect to invariant I if, for all I-T-reachable states Di, Dj with a common ancestor state, Di ⊔ Dj is I-valid.» · «Theorem 1. A globally I-valid system can execute a set of transactions T with coordination-freedom, transactional availability, convergence if and only if T is ℐ-confluent with respect to I.» · «coordination can only be avoided if all local commit decisions are globally valid.» · «a row-level "greater-than" (>) threshold invariant is ℐ-confluent for counter increment and assign (←) but not decrement (Claims 11, 13), while a row-level "less-than" (<) threshold invariant is ℐ-confluent for counter decrement and assign but not increment (Claims 12, 14).» · Tabla 2, filas «< | Increment [Counter] | No» y «Uniqueness | Choose specific value | No» frente a «Uniqueness | Choose some value | Yes». · «At one extreme, if a user's transactions do not modify database state, she can guarantee any satisfiable invariant. At the other extreme, with no invariants, a user can safely perform any operations she likes.» · «simply because these invariants are ℐ-confluent does not mean that all execution strategies will scale well: for example, using locking would not be coordination-free.» · «If invariants are incorrectly or incompletely specified, an ℐ-confluent database system may violate application-level correctness.» — Citas leídas directamente del PDF de VLDB, páginas 188-192.
Balegas, V., Serra, D., Duarte, S., Ferreira, C., Rodrigues, R., Preguiça, N., Shapiro, M. y Najafzadeh, M. (2015). Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants. SRDS 2015. arXiv:1503.09052. https://arxiv.org/abs/1503.09052 — «We present a new replicated data type, called bounded counter, which adds support for numeric invariants to eventually consistent geo-replicated databases.» · «Our approach adapts ideas from escrow transactions to devise a solution that is decentralized, fault-tolerant and fast.» — Recuperado el resumen; el texto completo del artículo no se ha leído, de modo que aquí solo se usa para nombrar la técnica, no para respaldar ninguna cifra.
Model Context Protocol (2025). Schema 2025-06-18, interfaz
ToolAnnotations.
https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-06-18/schema.ts
— «If true, the tool does not modify its environment. Default: false»
(readOnlyHint) · «If true, the tool may perform destructive updates to its
environment. If false, the tool performs only additive updates. (This property is
meaningful only when readOnlyHint == false) Default: true» (destructiveHint)
· «NOTE: all properties in ToolAnnotations are hints. They are not
guaranteed to provide a faithful description of tool behavior… Clients should
never make tool use decisions based on ToolAnnotations received from untrusted
servers.»
Model Context Protocol (2025). Specification 2025-06-18 — Server / Tools. https://modelcontextprotocol.io/specification/2025-06-18/server/tools — «For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers.» — Relevante porque la política de cerrojos de ALLAN descansa por completo en una anotación que la especificación declara no fiable.
The Go Authors. Command go — Compile packages and dependencies. https://pkg.go.dev/cmd/go — «-p n · the number of programs, such as build commands or test binaries, that can be run in parallel. The default is GOMAXPROCS, normally the number of CPUs available.»
The PostgreSQL Global Development Group. PostgreSQL 16 Documentation, 13.3.2. Row-Level Locks. https://www.postgresql.org/docs/16/explicit-locking.html — «FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as though for update. This prevents them from being locked, modified or deleted by other transactions until the current transaction ends.» · «Row-level locks are released at transaction end or during savepoint rollback, just like table-level locks.» · «Row-level locks do not affect data querying; they block only writers and lockers to the same row.»