¿Qué debo hacer cuando el bloqueo optimista no funciona?

11

Tengo este siguiente escenario:

  1. Un usuario realiza una solicitud GET/projects/1 y recibe un ETag .
  2. El usuario realiza una solicitud PUT/projects/1 con el ETag desde el paso 1.
  3. El usuario realiza otra solicitud PUT /projects/1con el ETag desde el paso 1.

Normalmente, la segunda solicitud PUT recibiría una respuesta 412, ya que el ETag ahora está obsoleto: la primera solicitud PUT modificó el recurso, por lo que el ETag ya no coincide.

Pero, ¿qué pasa si las dos solicitudes PUT se envían al mismo tiempo (o exactamente una después de la otra)? La primera solicitud PUT no tiene tiempo para procesar y actualizar el recurso antes de que llegue PUT # 2, lo que hace que PUT # 2 sobrescriba PUT # 1. El objetivo del bloqueo optimista es que eso no suceda ...

maximedupre
fuente
3
Atomice sus operaciones en transacciones de nivel empresarial, como explica Esben a continuación.
Robert Harvey
¿Qué pasaría si atomizara mis operaciones usando transacciones? ¿PUT # 2 no se procesará hasta que PUT # 1 esté completamente procesado?
maximedupre
77
Convertirse en un pesimista?
jpmc26
bueno, esto es para lo que sirve el bloqueo.
Fattie
Correcto, por supuesto, el Put # 2 no debe procesarse, se supone que son únicos.
Fattie

Respuestas:

21

El mecanismo ETag especifica solo el protocolo de comunicación para el bloqueo optimista. Es responsabilidad del servicio de aplicación implementar el mecanismo para detectar actualizaciones concurrentes para aplicar el bloqueo optimista.

En una aplicación típica que usa una base de datos, generalmente lo haría abriendo una transacción al procesar una solicitud PUT. Normalmente leería el estado existente de la base de datos dentro de esa transacción (para obtener un bloqueo de lectura), verificaría la validez de su Etag y sobrescribirá los datos (de una manera que provocará un conflicto de escritura cuando haya una transacción concurrente incompatible), entonces comprometerse. Si configura la transacción correctamente, uno de los commits debería fallar porque ambos intentarán actualizar los mismos datos simultáneamente. Luego podrá utilizar este error de transacción para devolver 412 o volver a intentar la solicitud, si tiene sentido para la aplicación.

Lie Ryan
fuente
La forma en que el servidor implementa actualmente el mecanismo para detectar actualizaciones concurrentes es mediante la comparación de hashes del recurso. El servidor también usa transacciones para todas las operaciones, pero no estoy adquiriendo ningún bloqueo, lo que podría ser la causa del problema. Sin embargo, en su ejemplo, ¿cómo puede haber un error en una de las confirmaciones si las transacciones están usando bloqueos? La segunda transacción debe estar pendiente al leer el estado, hasta que se resuelva la primera transacción.
maximedupre
1
@maximedupre: si está utilizando una transacción, tiene algún tipo de bloqueos, aunque pueden ser bloqueos implícitos (los bloqueos se adquieren automáticamente cuando lee / actualiza campos en lugar de solicitarlos explícitamente). El mecanismo que describí anteriormente puede implementarse utilizando solo esos bloqueos implícitos. Como su otra pregunta, depende de la base de datos que esté utilizando, pero muchas bases de datos modernas usan MVCC (control de concurrencia de múltiples versiones) para permitir que múltiples lectores y escritores trabajen en los mismos campos sin bloquearse innecesariamente entre sí.
Lie Ryan
1
Advertencia: en muchos DBMS (PostgreSQL, Oracle, SQL Server, etc.), el nivel de aislamiento de transacción predeterminado es "lectura confirmada", donde su enfoque no es suficiente para evitar la condición de carrera del OP. En tales DMBS, puede solucionarlo al incluirlo AND ETag = ...en UPDATEla WHEREcláusula de su estado de cuenta y verificar el recuento actualizado de filas después. (O mediante el uso de un nivel de aislamiento de transacción más estricto, pero realmente no lo recomiendo.)
ruakh
1
@ruakh: depende de cómo escriba su consulta, sí, el nivel de aislamiento predeterminado no proporciona ese comportamiento automáticamente para todas las consultas, pero a menudo es posible estructurar su transacción de una manera que sea suficiente para implementar un bloqueo optimista. En la mayoría de los casos, si la consistencia de la transacción es importante en la aplicación, recomendaría la lectura repetible como nivel de aislamiento predeterminado de todos modos; En las bases de datos que usan MVCC, la sobrecarga de lectura repetible es bastante mínima y simplifica significativamente la aplicación.
Lie Ryan
1
@ruakh: el principal inconveniente de la lectura repetible es que tendrá que estar preparado para volver a intentarlo o fallar si hay una transacción concurrente. Esto normalmente es un problema, pero las aplicaciones que proporcionan bloqueo optimista como estrategia de concurrencia ya requerirán este manejo de todos modos, por lo que las fallas de lectura repetibles se asignan naturalmente a fallas de bloqueo optimistas y esto en realidad no agregará nuevos inconvenientes.
Lie Ryan
13

Tienes que ejecutar el siguiente par atómicamente:

  • comprobación de la validez de la etiqueta (es decir, está actualizada)
  • actualizar el recurso (que incluye actualizar su etiqueta)

Otros lo llaman una transacción, pero fundamentalmente, la ejecución atómica de estas dos operaciones es lo que impide que una sobrescriba a la otra por accidente; sin esto tienes una condición de carrera, como lo estás notando.

Esto aún se considera un bloqueo optimista, si observa el panorama general: que el recurso en sí no está bloqueado por la lectura inicial (GET) por ningún usuario o cualquier usuario que esté mirando los datos, ya sea con la intención de actualizar o no.

Es necesario algún comportamiento atómico, pero esto ocurre dentro de una sola solicitud (el PUT) en lugar de intentar mantener un bloqueo en múltiples interacciones de red; esto es un bloqueo optimista: el objeto no está bloqueado por GET pero PUT todavía puede actualizarlo de forma segura.

También hay muchas formas de lograr la ejecución atómica de estas dos operaciones: bloquear el recurso no es la única opción; por ejemplo, un hilo ligero o bloqueo de objeto puede ser suficiente y depende de la arquitectura y el contexto de ejecución de su aplicación.

Erik Eidt
fuente
44
+1 por notar que lo importante es ser atómico. Dependiendo del recurso subyacente que se actualice, esto se puede lograr sin transacciones ni bloqueos. Por ejemplo, la comparación atómica y el intercambio de un recurso en memoria, o el abastecimiento de eventos de datos persistentes.
Aaron M. Eshbach
@ AaronM.Eshbach, de acuerdo, y gracias por llamarlos.
Erik Eidt
1

Está en el desarrollador de la aplicación verificar la etiqueta electrónica y proporcionar esa lógica. No es mágico que el servidor web lo haga por ti porque solo sabe cómo calcular E-Tagencabezados para contenido estático. Así que tomemos su escenario anterior y analicemos cómo debería ocurrir la interacción.

GET /projects/1

El servidor recibe la solicitud, determina la etiqueta electrónica para esta versión del registro y la devuelve con el contenido real.

200 - OK
E-Tag: "412"
Content-Type: application/json
{modified: false}

Como el cliente ahora tiene el valor E-Tag, puede incluir eso con la PUTsolicitud:

PUT /projects/1
If-Match: "412"
Content-Type: application/json
{modified: true}

En este punto, su aplicación debe hacer lo siguiente:

  • Verifique que la etiqueta electrónica sigue siendo correcta: "412" == "412"?
  • Si es así, realice la actualización y calcule una nueva etiqueta electrónica

Envía la respuesta de éxito.

204 No Content
E-Tag: "543"

Si llega otra solicitud e intenta realizar una PUTsimilar a la solicitud anterior, la segunda vez que su código de servidor la evalúa, usted es responsable de proporcionar el mensaje de error.

  • Verifique que la etiqueta electrónica siga siendo correcta: "412"! = "543"

En caso de falla, envíe la respuesta a la falla.

412 Precondition Failed

Este es el código que realmente tienes que escribir. De hecho, la etiqueta E puede ser cualquier texto (dentro de los límites definidos en la especificación HTTP). No tiene que ser un número. También puede ser un valor hash.

Berin Loritsch
fuente
Esta no es una notación HTTP estándar que está usando aquí. En HTTP estándar compatible, solo usa ETag en un encabezado de respuesta. Nunca envía ETag en un encabezado de solicitud, sino que utiliza el valor ETag adquirido previamente en un encabezado If-Match o If-None-Match en los encabezados de solicitud.
Lie Ryan
-2

Como complemento a las otras respuestas, publicaré una de las mejores citas en la documentación de ZeroMQ que describe fielmente el problema subyacente:

Para hacer programas MT completamente perfectos (y lo digo literalmente), no necesitamos mutexes, bloqueos ni ninguna otra forma de comunicación entre subprocesos, excepto los mensajes enviados a través de sockets ZeroMQ.

Por "programas MT perfectos", me refiero a un código que es fácil de escribir y comprender, que funciona con el mismo enfoque de diseño en cualquier lenguaje de programación y en cualquier sistema operativo, y que se escala en cualquier número de CPU con cero estados de espera y sin punto de rendimientos decrecientes.

Si ha pasado años aprendiendo trucos para hacer que su código MT funcione, y mucho menos rápidamente, con bloqueos, semáforos y secciones críticas, se disgustará cuando se dé cuenta de que todo fue para nada. Si hay una lección que hemos aprendido de más de 30 años de programación concurrente, es: simplemente no comparta el estado. Es como dos borrachos tratando de compartir una cerveza. No importa si son buenos amigos. Tarde o temprano, se van a pelear. Y mientras más borrachos agregas a la mesa, más se pelean por la cerveza. La trágica mayoría de las aplicaciones de MT parecen peleas de bares borrachos.

acechador
fuente