paint-brush
A las carreras: 3 estrategias efectivas para mitigar las condiciones de carrerapor@ahasoftware
307 lecturas
307 lecturas

A las carreras: 3 estrategias efectivas para mitigar las condiciones de carrera

por Aha!7m2023/09/22
Read on Terminal Reader

Demasiado Largo; Para Leer

Si bien no existe una panacea para solucionar las condiciones de carrera, muchas pueden solucionarse con las estrategias adecuadas. Obtenga ejemplos de dos categorías de condiciones de carrera y tres formas de resolverlas.
featured image - A las carreras: 3 estrategias efectivas para mitigar las condiciones de carrera
Aha! HackerNoon profile picture



¿Qué es una condición de carrera?

Busqué una buena definición de condición de carrera y esta es la mejor que encontré:


Una condición de carrera es un comportamiento imprevisto causado por múltiples procesos que interactúan con recursos compartidos en un orden diferente al esperado.


Esto es bastante complicado y todavía no está muy claro cómo se manifiestan las condiciones de carrera en Rails .


Al utilizar Rails, siempre trabajamos con múltiples procesos: cada solicitud o trabajo en segundo plano es un proceso individual que puede operar en su mayor parte de forma independiente de otros procesos.


También estamos siempre trabajando con recursos compartidos. ¿La aplicación utiliza una base de datos relacional? Ese es un recurso compartido. ¿La aplicación utiliza algún tipo de servidor de caché? Sí, ese es un recurso compartido. ¿Utiliza algún tipo de API externa? Lo has adivinado: ese es un recurso compartido.


Hay dos categorías de ejemplo de condiciones de carrera de las que me gustaría hablar y luego tocar cómo abordarlas.


Leer-modificar-escribir

La categoría lectura-modificación-escritura es un tipo de condición de carrera en la que un proceso leerá valores de un recurso compartido, modificará el valor dentro de la memoria y luego intentará volver a escribirlo en el recurso compartido. Esto parece muy sencillo cuando lo miramos a través del lente de un solo proceso. Pero cuando surge un segundo proceso, puede resultar en algún comportamiento inesperado.


Considere el código que se parece a este:

 class IdeasController < ActionController::Base def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! end end

Aquí estamos leyendo ( Idea.find(params[:id]) ), modificando ( @idea.votes += 1 ) y luego escribiendo ( @idea.save! ).


Podemos ver que esto incrementaría en uno el número de votos de una idea. Si hubiera una idea con cero votos, terminaría con un voto. Sin embargo, si llega una segunda solicitud y lee la idea de la base de datos mientras todavía tenía cero votos e incrementa ese valor en la memoria, podríamos tener una situación en la que lleguen dos votos simultáneamente, pero el resultado final es que el número de votos en la base de datos hay solo uno.


Esto también se conoce como condición de carrera de actualización perdida .


Verificar y luego actuar

La categoría verificar y luego actuar es un tipo de condición de carrera en la que los datos se cargan desde un recurso compartido y, según el valor presente, determinamos si es necesario realizar una acción.

Uno de los ejemplos clásicos de cómo se muestra esto es en validates_uniqueness_of validación en Rails, como este:

 class User < ActiveRecord::Base validates_uniqueness_of :email end


Considere el código que se parece a este:

 User.create(email: "[email protected]")


Con la validación implementada, Rails verificará si existe algún usuario con ese correo electrónico. Si no hay otra, actuará persistiendo al usuario en la base de datos. Sin embargo, ¿qué pasaría si una segunda solicitud estuviera ejecutando el mismo código al mismo tiempo? Podríamos terminar en una situación en la que ambas solicitudes verifiquen para determinar si hay datos duplicados (y no los hay); entonces ambas actuarán guardando los datos, lo que dará como resultado un usuario duplicado en la base de datos.


Abordar las condiciones de carrera

No existe una solución milagrosa para solucionar las condiciones de carrera, pero existen varias estrategias que pueden aprovecharse para cualquier problema en particular. Hay tres categorías principales para eliminar condiciones de carrera:

1. Retire la sección crítica

Si bien esto podría considerarse como una eliminación del código infractor, a veces puedes refactorizar el código para que no sea vulnerable a las condiciones de carrera. Otras veces, puedes investigar las operaciones atómicas.

Una operación atómica es aquella en la que ningún otro proceso puede interrumpir la operación, por lo que sabes que siempre se ejecutará como una sola unidad.


Para el ejemplo de lectura-modificación-escritura, en lugar de incrementar los votos de ideas en la memoria, se podrían incrementar en la base de datos:

 @ideas.increment!(:votes)


Eso ejecutará sql que se ve así:

 UPDATE "ideas" SET "votes" = COALESCE("votes", 0) + 1 WHERE "ideas"."id" = 123


Utilizar esto no estaría sujeto a las mismas condiciones de carrera.


Para el ejemplo de verificar y actuar, en lugar de permitir que Rails valide el modelo, podríamos insertar el registro directamente en la base de datos con un upsert:

 User.where(email: "[email protected]").upsert({}, unique_by: :email)


Eso insertará el registro en la base de datos. Si hay un conflicto en el correo electrónico (lo que requeriría un índice único en el correo electrónico), simplemente ignorará la inserción.

2. Detectar y recuperar

A veces no se puede eliminar la sección crítica. Es posible que haya una acción atómica, pero no funciona de la forma que requiere el código. En esas situaciones, puede probar un enfoque de detección y recuperación. Con este enfoque, se configuran salvaguardas que le informarán si ocurre una condición de carrera. Puede cancelar o volver a intentar la operación sin problemas.


Para el ejemplo de lectura-modificación-escritura, esto podría hacerse con un bloqueo optimista . El bloqueo optimista está integrado en Rails y puede permitir la detección de cuándo múltiples procesos están operando en el mismo registro al mismo tiempo. Para habilitar el bloqueo optimista, solo necesita agregar una columna lock_version a su tabla y Rails la habilitará automáticamente.

 change_table :ideas do |t| t.integer :lock_version, default: 0 end


Luego, cuando intente actualizar un registro, Rails solo lo actualizará si lock_version es la misma versión que estaba en la memoria. Si no es así, generará una excepción ActiveRecord::StaleObjectError , que se puede rescatar para manejarla. Manejarlo podría ser un retry o podría ser simplemente un mensaje de error informado al usuario.

 def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! rescue ActiveRecord::StaleObjectError retry end


Para el ejemplo de verificar y luego actuar, esto se podría hacer con un índice único en la columna y luego rescatar la excepción al persistir los datos.

 add_index :users, [:email], unique: true


Con un índice único implementado, si ya existen datos en la base de datos con ese email , Rails generará un error ActiveRecord::RecordNotUnique y eso se puede rescatar y manejar adecuadamente.

 begin user = User.create(email: "[email protected]") rescue ActiveRecord::RecordNotUnique user = User.find_by(email: "[email protected]") end

Idempotencia

Para volver a intentar acciones, es importante que toda la operación sea idempotente. Esto significa que si una operación se realiza varias veces, el resultado es el mismo que si solo se aplicara una vez.


Por ejemplo, imagine si un trabajo se enviara por correo electrónico y se realizara cada vez que se cambiaran los votos de una idea. Sería realmente malo si se enviara un correo electrónico por cada reintento. Para que la operación sea idempotente, puede posponer el envío del correo electrónico hasta que se complete toda la operación de votación. Alternativamente, puede actualizar la implementación del proceso que envía el correo electrónico para enviarlo solo si los votos cambiaron desde la última vez que se envió. Si se produce una condición de carrera y necesita volver a intentarlo, el primer intento de enviar un correo electrónico puede resultar en un fracaso y es seguro activarlo nuevamente.


Es posible que muchas operaciones no sean idempotentes, como poner en cola un trabajo en segundo plano, enviar un correo electrónico o llamar a una API de terceros.

3. Protege el código

Si no puede detectar y recuperar, puede intentar proteger el código. El objetivo aquí es crear un contrato en el que solo un proceso pueda acceder al recurso compartido a la vez. Efectivamente, está eliminando la concurrencia: dado que solo un proceso puede tener acceso a un recurso compartido, podemos evitar la mayoría de las condiciones de carrera. Sin embargo, la desventaja es que cuanto más se elimine la concurrencia, más lenta puede ser la aplicación, ya que otros procesos esperarán hasta que se les permita el acceso.


Esto podría solucionarse mediante el bloqueo pesimista integrado en Rails. Para usar el bloqueo pesimista , puede agregar lock a las consultas que se están creando y Rails le indicará a la base de datos que mantenga un bloqueo de fila en esos registros. La base de datos evitará que cualquier otro proceso obtenga el bloqueo hasta que finalice. Asegúrese de incluir el código en una transaction para que la base de datos sepa cuándo liberar el bloqueo.

 Idea.transaction do @idea = Idea.lock.find(params[:id]) @idea.votes += 1 @idea.save! end


Si el bloqueo a nivel de fila no es posible, existen otras herramientas como Redlock o with_advisory_lock que podrían usarse. Estos permitirán bloquear un bloque de código arbitrario. Usar esto podría ser tan simple como algo como esto:

 email = "[email protected]" User.with_advisory_lock("user_uniqueness_#{email}"} do User.find_or_create_by(email: email) end


Estas estrategias harán que los procesos esperen hasta que se obtenga un bloqueo. Por lo tanto, también querrán tener algún tipo de tiempo de espera para evitar que un proceso espere eternamente, así como algún control sobre qué hacer en caso de un tiempo de espera.


Si bien no existe una panacea para arreglar las condiciones de carrera, muchas condiciones de carrera se pueden arreglar mediante estas estrategias. Sin embargo, cada problema es un poco diferente, por lo que los detalles de las soluciones pueden variar. Puedes echar un vistazo a mi charla de RailsConf 2023 , que detalla más sobre las condiciones de carrera.



Sobre el Autor

Kyle d’Oliveira

Kyle d'Oliveira


A Kyle le apasiona convertir ideas abstractas en piezas de software funcionales. Es ingeniero de software principal en Aha! — el software de desarrollo de productos número uno del mundo . Cuando no está en desarrollo, Kyle disfruta de comida increíble y de cervecerías artesanales cerca de su casa en Vancouver, Canadá.






También publicado aquí.