new_customer
Una aplicación web llama a mi función varias veces por segundo (pero solo una vez por sesión). Lo primero que hace es bloquear la customer
tabla (hacer un 'insertar si no existe', una variante simple de un upsert
).
Entiendo que los documentos son que otras llamadas new_customer
simplemente deberían hacer cola hasta que todas las llamadas anteriores hayan terminado:
LOCK TABLE obtiene un bloqueo a nivel de tabla, esperando si es necesario para liberar cualquier bloqueo en conflicto.
¿Por qué a veces es un punto muerto?
definición:
create function new_customer(secret bytea) returns integer language sql
security definer set search_path = postgres,pg_temp as $$
lock customer in exclusive mode;
--
with w as ( insert into customer(customer_secret,customer_read_secret)
select secret,decode(md5(encode(secret, 'hex')),'hex')
where not exists(select * from customer where customer_secret=secret)
returning customer_id )
insert into collection(customer_id) select customer_id from w;
--
select customer_id from customer where customer_secret=secret;
$$;
error del registro:
2015-07-28 08:02:58 BST DETALLE: El proceso 12380 espera a ExclusiveLock en la relación 16438 de la base de datos 12141; bloqueado por el proceso 12379. El proceso 12379 espera a ExclusiveLock en la relación 16438 de la base de datos 12141; bloqueado por el proceso 12380. Proceso 12380: seleccione new_customer (decode ($ 1 :: text, 'hex')) Proceso 12379: seleccione new_customer (decode ($ 1 :: text, 'hex')) 2015-07-28 08:02:58 BST SUGERENCIA: Consulte el registro del servidor para obtener detalles de la consulta. 2015-07-28 08:02:58 BST CONTEXTO: función SQL "nuevo_cliente" declaración 1 2015-07-28 08:02:58 BST STATEMENT: select new_customer (decode ($ 1 :: text, 'hex'))
relación:
postgres=# select relname from pg_class where oid=16438;
┌──────────┐
│ relname │
├──────────┤
│ customer │
└──────────┘
editar:
Me las arreglé para obtener un caso de prueba reproducible simple. Para mí, esto parece un error debido a algún tipo de condición de carrera.
esquema:
create table test( id serial primary key, val text );
create function f_test(v text) returns integer language sql security definer set search_path = postgres,pg_temp as $$
lock test in exclusive mode;
insert into test(val) select v where not exists(select * from test where val=v);
select id from test where val=v;
$$;
El script bash se ejecuta simultáneamente en dos sesiones bash:
for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done
registro de errores (generalmente un puñado de puntos muertos en las 1000 llamadas):
2015-07-28 16:46:19 BST ERROR: deadlock detected
2015-07-28 16:46:19 BST DETAIL: Process 9394 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9393.
Process 9393 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9394.
Process 9394: select f_test('blah')
Process 9393: select f_test('blah')
2015-07-28 16:46:19 BST HINT: See server log for query details.
2015-07-28 16:46:19 BST CONTEXT: SQL function "f_test" statement 1
2015-07-28 16:46:19 BST STATEMENT: select f_test('blah')
editar 2:
@ypercube sugirió una variante con el lock table
exterior de la función:
for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done
Curiosamente, esto elimina los puntos muertos.
fuente
customer
usa de una manera que agarre un bloqueo más débil? Entonces podría ser un problema de actualización de bloqueo.Respuestas:
Publiqué esto en pgsql-bugs y la respuesta de Tom Lane indica que se trata de un problema de escalada de bloqueo, disfrazado por la mecánica de la forma en que se procesan las funciones del lenguaje SQL. Esencialmente, el bloqueo generado por el
insert
se obtiene antes del bloqueo exclusivo en la tabla :Esto también explica por qué bloquear la tabla fuera de la función en un bloque envolvente plpgsql (como lo sugiere @ypercube) evita los puntos muertos.
fuente
Suponiendo que ejecuta otras instrucciones antes de llamar a new_customer, y adquieren un bloqueo que entra en conflicto
EXCLUSIVE
(básicamente, cualquier modificación de datos en la tabla del cliente), la explicación es muy simple.Uno puede reproducir el problema con un ejemplo simple (ni siquiera incluye una función):
1ra sesión:
2da sesión
1ra sesión
Cuando la primera sesión realiza la inserción, adquiere el
ROW EXCLUSIVE
bloqueo en una tabla. Mientras tanto, la sesión 2 intenta también obtener elROW EXCLUSIVE
bloqueo e intenta adquirir unEXCLUSIVE
bloqueo. En ese momento tiene que esperar a la primera sesión, ya que elEXCLUSIVE
bloqueo entra en conflicto conROW EXCLUSIVE
. Por fin, la primera sesión salta a los tiburones e intenta obtener unEXCLUSIVE
bloqueo, pero dado que los bloqueos se adquieren en orden, se pone en cola después de la segunda sesión. Esto, a su vez, espera al primero, produciendo un punto muerto:La solución a este problema es adquirir bloqueos lo antes posible, generalmente como una primera cosa en una transacción. Por otro lado, la carga de trabajo de PostgreSQL solo necesita bloqueos en algunos casos muy raros, por lo que te sugiero que reconsideres la forma en que lo haces (mira este artículo http://www.depesz.com/2012/06/10 / why-is-upsert-so-complicado / ).
fuente
Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))
Mientras Jack acaba de recibir:Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))
- indicando que la llamada a la función es el primer comando en ambas transacciones (a menos que me falte algo).