feat(container): update, restart y stats - #41
Conversation
`DockerSwarm::Container` no implementaba las tres, y el `method_missing` de
`Base` hacia que la ausencia NO se notara: cualquier metodo desconocido que no
termine en `?` define un accessor y devuelve nil (base.rb:158-160). Peor,
`respond_to?` tambien devuelve true, asi que un consumidor defensivo que
chequee antes de llamar recibe la misma mentira. Aguas abajo el BCM serializa
ese nil y responde 200 OK con cuerpo nulo.
Tres decisiones, ninguna copiada del hermano:
1. NO se incluye Concerns::Updatable. Ese concern es para services de Swarm:
manda ?version= para concurrencia optimista (updatable.rb:26). El update de
un container no tiene version — otro endpoint, otra semantica — asi que
incluirlo mandaria un query param que el Engine ignora en silencio.
2. `update` absorbe kwargs a proposito. Creatable#save llama
`update(registry_auth:)` cuando el objeto esta persistido; con la firma
simple ese kwarg se vuelve Hash posicional en Ruby 3 (medido) y terminaria
posteado como atributo: {"registry_auth": null} hacia el Engine, sin error.
Los dos de registry se descartan: son cosa de Service.
Y devuelve el cuerpo, no un booleano: Docker responde {"Warnings": [...]} y
ahi avisa cuando un limite no se pudo aplicar. Colapsarlo a true se comeria
la senal.
3. `restart` NO copia a Service#restart. El hermano simula con ForceUpdate
PORQUE los services no tienen endpoint de restart; los containers si.
4. `stats` fuerza stream: false, y por eso el metodo vuelve. El endpoint
streamea por default. Medido contra Engine 29.7.2: sin el parametro, 6
objetos en 6s y la conexion NO cierra (exit 28 de curl); con stream=false,
un objeto y cierra en 1s. En RPC el default cuelga, y el modo de falla no es
un error sino una espera.
El parametro se MERGEA en vez de reemplazarse — a diferencia de
Loggable#logs, donde un caller que pasa su hash solo cambia que streams lee;
aca lo dejaria colgado.
Verificado contra un Engine real, con efecto semantico y no solo el 200:
stats 200 · query {stream: false} · Hash con memory_stats.usage en 1.0s
restart 204 · query {t: 1} · true · StartedAt se movio
update 200 · {"Warnings" => nil} · Memory 67108864 -> 134217728
Sin tests todavia: van despues de la revision de forma.
Closes #39
Part of #30
Los 4 CONFIRMED por la verificacion adversarial, y los 4 son de este PR: al
hacer que `update` tolerara el kwarg de `Creatable#save` cree un agujero nuevo.
F1 (3/5, high) — `save` sobre un container persistido posteaba {} y perdia los
cambios locales EN SILENCIO. Creatable#save:32 llama `update(registry_auth:)`
sin atributos; tras el except el payload quedaba vacio y el Engine responde 200
sin aplicar nada — medido: body {} -> {"Warnings":null}, Memory sin cambiar.
Antes devolvia nil por method_missing, asi que no es regresion; pero tuve la
oportunidad de arreglarlo y en cambio lo hice PARECER que funcionaba, que es
peor. Ahora un payload vacio levanta ArgumentError con un mensaje que explica y
redirige a `update("Memory" => ...)`.
El save generico NO se soporta a proposito: POST /containers/{id}/update no es
"guardar el objeto", es un endpoint angosto de limites de recursos. Derivar el
payload de los atributos locales exigiria una whitelist de campos que driftea
contra la API. Mejor decirlo que fingirlo.
F2 (3/5) — `save` devolvia el Hash del Engine en vez de un booleano, rompiendo
su contrato. Se resuelve por F1: ese camino ahora levanta.
F3 (2/5) — `update` dejaba el estado local stale. Se agrega `reload` tras el
200, y NO `assign_attributes` como hace Concerns::Updatable: el payload es
plano (Memory) y el objeto lo tiene anidado (HostConfig.Memory), asi que
asignarlo crearia un atributo fantasma en vez de actualizar el real. `reload`
trae la forma correcta y es el mismo cierre que usa `save` al crear.
F4 (1/5, security) — el except filtraba solo `opts`, asi que una credencial
pasada POSICIONALMENTE ({registry_auth: "...", Memory: ...}) viajaba en el
payload y salia en el log (body=...). Ahora el except va sobre el MERGE, y
cubre las claves como Symbol y como String. Es la misma fuga que #24 arreglo
por el otro lado.
Verificado contra Engine 29.7.2:
save sobre persistido -> ArgumentError (antes: 200 OK silencioso)
credencial posicional -> filtrada, no viajo
update camino feliz -> HostConfig.Memory 67108864 -> 134217728 sin reload manual
rubocop -> no offenses
Part of #39
Review multi-modelo — 5 revisaron, 4 findings, los 4 CONFIRMED y los 4 aplicados
Los cuatro hallazgos son de este PR, y el grave muestra algo incómodo: al hacer que
F1 — el que más duele, porque lo introduje al arreglar otra cosa
Así que El (Verificado antes de elegir: nadie llama F3 — donde me aparté de lo que proponían los revisoresSugerían Evidencia de los cuatro, contra Engine 29.7.2Lo que quedó abierto y declarado
|
| "Container#update necesita al menos un atributo. Un payload vacío recibe 200 OK " \ | ||
| "del Engine y NO aplica nada. Si venís de `save`: los containers no soportan el " \ | ||
| "save genérico — usá `update(\"Memory\" => …)` con los límites explícitos." |
There was a problem hiding this comment.
por standard esto? no deberia de estar en ingles?
Review de @gedera en el PR 41. Medida la convencion del repo antes de cambiar: de los 5 mensajes de error de la gema, CUATRO estan en ingles "assign_attributes expects a Hash, got #{new_attributes.class}" "Docker socket error: #{actual_error.message}" "HTTP #{status}: #{error_msg}" "image pull failed: #{detail}" y uno en espanol ("registry_auth y registry_auth_from son mutuamente excluyentes", 0.8.0). Copie al outlier. La distincion que queda clara: prosa explicativa en espanol —los comentarios de container.rb, updatable.rb y el CHANGELOG lo son— pero SUPERFICIE PUBLICA en ingles, y un mensaje de excepcion es superficie publica: lo lee quien consume la gema, no quien la mantiene. No se toca el outlier de 0.8.0: es preexistente y de otro cambio.
|
Medí la convención antes de cambiar, porque no la tenía clara: de los 5 mensajes de error de la gema, cuatro están en inglés ( Tenés razón: corregido a inglés en La distinción que me queda, y la aplico de acá en adelante: prosa explicativa en español —los comentarios de Verificado cómo sale: No toqué el outlier de 0.8.0 — es preexistente y de otro cambio. |
…stats Los unitarios pinean lo que MANDAMOS; los de integracion, lo que el Engine HACE. Hay una cosa que un unitario no puede cubrir por definicion: que `stats` VUELVA. Mockeando `Api.request` se verifica que pasamos stream: false, no que eso evite el cuelgue. Denegacion probada en DOS capas: contra main (sin metodos ni rutas) 27 examples, 13 failures contra 7f56c96 (metodos SIN los 4 findings) 27 examples, 4 failures :214 reload del estado local -> F3 :223 payload vacio levanta -> F1 :229 save sobre persistido levanta -> F1/F2 :248 registry_auth como clave String -> F4 O sea que cada hallazgo del MVF tiene un test que lo deniega, uno a uno: si alguien revierte cualquiera de los cuatro, se pone rojo. Los de integracion destaparon el issue #38 en la practica: el `after` hacia `destroy(force: true)` y `Deletable#destroy` NO ACEPTA ARGUMENTOS, asi que el ArgumentError se lo comia el rescue, el container quedaba vivo y el ejemplo siguiente moria con 409 Conflict — con el error real tapado por el conflicto de nombre. Se cierra con `stop` + `destroy` y con nombre unico POR EJEMPLO, no por corrida, para que una fuga no encadene. 217 examples, 0 failures (unitarios) 8 examples, 0 failures (integracion, contra Engine 29.7.2) rubocop: 56 files, no offenses Part of #39
Cuatro artefactos quedaron MINTIENDO despues del cambio — no uno: interface decia "Creatable, Deletable, Loggable; #start, #stop" consumed listaba 6 endpoints de containers, ahora son 9 glossary "la gema expone create/start/stop/destroy/logs" skill (x2) el indice de capacidades y la tabla de superficie test la cobertura nueva, unit + integration En interface y skill no alcanza con listar los metodos: van las tres decisiones que un consumidor necesita saber y no puede deducir de la firma —que #update NO usa Concerns::Updatable y por que, que devuelve el cuerpo y no un booleano, y que #stats fuerza stream: false porque con el default la llamada no vuelve— mas la que rompe expectativas: `save` sobre un container persistido LEVANTA, no se soporta el save generico. En test se declara lo que solo la integracion puede cubrir: que `stats` VUELVA. Mockeando Api.request se verifica que mandamos stream: false, no que eso evite el cuelgue; de ahi el Timeout.timeout(15) del spec de integracion. NO se re-ancla ningun artefacto. Los cuatro apuntan a tags de version (v0.10.0, v0.9.0) y ya venian stale contra 0.11.0 — pero anclar a v0.12.0 seria declarar una release que todavia no paso. El re-anclaje va con #40, que es la release, y queda dicho en cada refresh. arch-lint: 14 -> 11 warns, exit 0. 217 examples, 0 failures. Part of #39
… de consumed
INCR-001 behavior — faltaba la capa, y el argumento de critias es bueno: la
cadencia la declara el propio §4 ("solo se diagrama un flujo cuando un PR lo
toca o agrega") y el precedente in-repo pone la vara BAJA — el flujo 3.7 es
start/stop, dos llamadas de un paso. `Container#update` es mas complejo que
eso: merge -> except -> chequeo de vacio -> raise -> POST -> reload -> devolver
cuerpo, con la bifurcacion save-sobre-persistido que pasa de 200 no-op
silencioso a ArgumentError. Ese "silencioso -> ruidoso" es exactamente lo que
la capa ya diagrama en 3.4 y 3.11.
Se agrega el flujo 3.13 con las dos ramas, y `stats` en las notas (misma forma
del problema, otra cara: no falla, CUELGA).
El conteo 12 -> 13 se movio en los CINCO lugares donde vive: behavior §2
(indice), §2 (titulo "Documentados"), la meta, §4 (el derivado "= 12"),
README.md y AGENTS.md. Si se mueve uno solo, STRUCT-003 rompe en el proximo
barrido.
INCR-001 errors — NO va una fila en §a, y eso ya estaba normado por el propio
artefacto: los ArgumentError de stdlib son "contrato publico de esas firmas, no
parte de la jerarquia DockerSwarm::Error". Lo que faltaba es la otra mitad: esa
misma clausula ENUMERA los sitios y ahora hay un tercero. Una linea.
INCR-003 consumed §c — la frase del ?version= se escribio para Service#update y
quedo CONTRADICIENDO a la fila que este mismo PR agrego a §b ("sin ?version=,
eso es de services"). Un agente que leyera §c concluiria que un replay de
Container#update da 409, cuando re-aplica el mismo limite. Se califica el
sujeto.
Observacion B — §b decia `?stream=false` OBLIGATORIO y no lo es: el codigo
mergea y hay un unitario que asserta que se puede pisar. Pasa a "por default
(pisable)". interface y SKILL ya decian "fuerza", que si es correcto.
Observacion C — §e de test tenia la linea vieja "container (start/stop)" tres
bullets arriba del bullet nuevo: el lector encontraba primero la lista
incompleta.
Y un arreglo propio: el comentario `%%` que puse INLINE en el mermaid va en su
propia linea o se renderiza como texto. Pasa a Note, que es lo que usa el resto
del archivo (0 usos de %% en las otras 12 secuencias).
arch-lint: 11 -> 10 warns, exit 0. 221 examples, 0 failures.
Part of #39
Causa raíz
DockerSwarm::Containerno implementaupdate,restartnistats, y la ausencia no se nota —lib/docker_swarm/base.rb:158-160:Cualquier método desconocido que no termine en
?define un accessor y devuelvenil, sinNoMethodError. Medido:respond_to?también miente, así que un consumidor defensivo que chequee antes de llamar recibe lo mismo. Aguas abajo, elcontainers_controllerdel BCM serializa esenily responde200 OKcon cuerpo nulo (sequre/box_cluster_manager#43).Cuatro decisiones, y ninguna es copiar al hermano
① No se incluye
Concerns::Updatable. Está escrito para services de Swarm: mandaquery_params: { version: current_version }(updatable.rb:26) para el control de concurrencia optimista. ElPOST /containers/{id}/updateno tieneversion— es otro endpoint, con otra semántica (límites de recursos). Incluirlo mandaría un query param que el Engine ignora en silencio.②
updateabsorbe kwargs a propósito, y esto es lo menos obvio del PR.Concerns::Creatable#save:32hace:Con la firma natural
update(new_attributes = {}), ese kwarg se convierte en Hash posicional en Ruby 3 — medido:O sea que
savesobre un container persistido postearía{"registry_auth": null}al Engine, sin error. Basura silenciosa: el mismo tipo de bug que este PR viene a cerrar, introducido al cerrarlo. Los dos de registry se descartan porque son cosa deService.Y
updatedevuelve el cuerpo, no un booleano. Docker responde{"Warnings": [...]}y ahí avisa cuando un límite no se pudo aplicar; colapsarlo atruese comería justamente esa señal.③
restartno copia aService#restart. El hermano simula el restart incrementandoForceUpdate(service.rb:40-43) porque los services de Swarm no tienen endpoint de restart. Los containers sí. Copiarlo sería arrastrar un workaround que acá no hace falta.④
statsfuerzastream: false, y por eso el método vuelve. Medido contra Engine 29.7.2:En una llamada RPC el default cuelga — y el modo de falla no es un error, es una espera. Mismo patrón que ADR-027: el default de Docker no es el que sirve.
El parámetro se mergea en vez de reemplazarse, a diferencia de
Loggable#logs. Ahí un caller que pasa su propio hash sólo cambia qué streams lee; acá lo dejaría colgado. Se puede pisar a propósito (stats(stream: true)), pero no por accidente.Evidencia — contra un Engine real, con efecto semántico
No alcanza con el
200: se verificó que hicieran lo que prometen.stats{stream: false}memory_stats.usage, en 1.0srestart{t: 1}trueStartedAt13:36:06.844 → 13:36:08.255update{}{"Warnings" => nil}Memory67108864 → 134217728Qué NO prueba
arm64-darwin). El comportamiento destatssinstreampodría diferir en otras versiones; lo que sí es estable es que el default del spec esstream=true.updatese probó conMemory/MemorySwap. No se ejercitaron los demás campos (NanoCpus,RestartPolicy, …) ni el caso donde el Engine sí devuelve warnings.Alcance
lib/docker_swarm/api.rb— 3 rutas nuevas encontainers:.lib/docker_swarm/models/container.rb— los 3 métodos.Qué NO hace: no toca
Base, ni elmethod_missingque es la causa de fondo del silencio (eso es superficie compartida por los 11 modelos y merece su propia decisión); no trae tests ni incremento de doc todavía; y no incluye el release ni los bumps, que son #40.Closes #39
Part of #30