Как я могу предотвратить взаимные блокировки postgres при запуске шутовых тестов на CircleCI?
Когда я запускаю свои тесты на CircleCI, он регистрирует следующее сообщение много раз, и в конечном итоге тесты не проходят, потому что ни один из методов базы данных не может получить данные из-за взаимоблокировок:
{
"message": "Error running raw sql query in pool.",
"stack": "error: deadlock detected\n at Connection.Object.<anonymous>.Connection.parseE (/home/circleci/backend/node_modules/pg/lib/connection.js:567:11)\n at Connection.Object.<anonymous>.Connection.parseMessage (/home/circleci/-backend/node_modules/pg/lib/connection.js:391:17)\n at Socket.<anonymous> (/home/circleci/backend/node_modules/pg/lib/connection.js:129:22)\n at emitOne (events.js:116:13)\n at Socket.emit (events.js:211:7)\n at addChunk (_stream_readable.js:263:12)\n at readableAddChunk (_stream_readable.js:250:11)\n at Socket.Readable.push (_stream_readable.js:208:10)\n at TCP.onread (net.js:597:20)",
"name": "error",
"length": 316,
"severity": "ERROR",
"code": "40P01",
"detail": "Process 1000 waits for AccessExclusiveLock on relation 17925 of database 16384; blocked by process 986.\nProcess 986 waits for RowShareLock on relation 17870 of database 16384; blocked by process 1000.",
"hint": "See server log for query details.",
"file": "deadlock.c",
"line": "1140",
"routine": "DeadLockReport",
"level": "error",
"timestamp": "2018-10-15T20:54:29.221Z"
}
Это тестовая команда, которую я запускаю: jest --logHeapUsage --forceExit --runInBand
- Я также попробовал это:
jest --logHeapUsage --forceExit --maxWorkers=2
Практически все тесты запускают какую-то функцию базы данных. Эта проблема только начала возникать, когда мы добавили больше тестов. У кого-нибудь еще была такая же проблема?
1 ответ
Судя по сообщению об ошибке, мы получили Deadlock из-за RowShareLock;
Это означает, что две транзакции (давайте назовем их transactionOne и transactionTwo) имеют заблокированный ресурс, который требуется другой транзакции.
Пример:
transactionOne locks record in UserTable with userId = 1
transactionTwo locks record in UserTable with userId = 2
transactionOne attempts to update in UserTable for userId = 2, but since it is locked by another transaction - it waits for the lock to be released
transactionTwo attempts to update in UserTable for userId = 1, but since it is locked by another transaction - it waits for the lock to be released
Now the SQL engine detects that there is a deadlock and randomly picks one of the transactions and terminates it.
Lets say the SQL engine picks transactionOne and terminates it. This will result in the exception that is posted in the question.
transactionTwo is now allowed to perform an update in UserTable for user with userId = 1.
transactionTwo completes with success
Механизмы SQL довольно быстро обнаруживают взаимоблокировки, и исключение будет мгновенным.
Это причина зависаний. Взаимоблокировки могут иметь разные первопричины.
Я вижу, вы используете плагин pg. Убедитесь, что вы правильно используете его с транзакциями: транзакции pg node-postgres
Я бы подозревал несколько разных основных причин и их решения:
Причина 1. Несколько тестов выполняются для одного и того же экземпляра базы данных.
Это могут быть разные конвейеры ci, выполняющие один и тот же тест для одного и того же экземпляра Postgres.
Решение:
Это наименее вероятная ситуация, но конвейер CI должен предоставлять свой отдельный экземпляр Postgres при каждом запуске.
Причина 2: Транзакции не обрабатываются с помощью соответствующего catch("ROLLBACK")
Это означает, что некоторые транзакции могут оставаться в силе и блокировать другие.
Решение: Все транзакции должны иметь соответствующую обработку ошибок.
const client = await pool.connect()
try {
await client.query('BEGIN')
//do what you have to do
await client.query('COMMIT')
} catch (e) {
await client.query('ROLLBACK')
throw e
} finally {
client.release()
}
Причина 3: параллелизм. Например: тесты выполняются параллельно и вызывают взаимоблокировки.
Мы пишем масштабируемые приложения. Это означает, что тупиковые ситуации неизбежны. Мы должны быть к ним готовы и вести себя соответствующим образом.
Решение: Используйте стратегию «Попробуем еще раз» . Когда мы обнаруживаем в нашем коде, что есть исключение взаимоблокировки, мы просто повторяем конечное количество раз. Этот подход был проверен со всеми моими производственными приложениями более десяти лет.
Решение с вспомогательной функцией:
//Sample deadlock wrapper
const handleDeadLocks = async (action, currentAttepmt = 1 , maxAttepmts = 3) {
try {
return await action();
} catch (e) {
//detect it is a deadlock. Not 100% sure whether this is deterministic enough
const isDeadlock = e.stack?.includes("deadlock detected");
const nextAttempt = currentAttepmt + 1;
if (isDeadlock && nextAttempt <= maxAttepmts) {
//try again
return await handleDeadLocks(action, nextAttempt, maxAttepmts);
} else {
throw e;
}
}
}
//our db access functions
const updateUserProfile = async (input) => {
return handleDeadLocks(async () => {
//do our db calls
});
};
Если код становится сложным/вложенным. Мы можем попытаться сделать это с другим решением, используя функцию высокого порядка.
const handleDeadLocksHOF = (funcRef, maxAttepmts = 3) {
return async (...args) {
const currentAttepmt = 1;
while (currentAttepmt <= maxAttepmts) {
try {
await funcRef(...args);
} catch (e) {
const isDeadlock = e.stack?.includes("deadlock detected");
if (isDeadlock && currentAttepmt + 1 < maxAttepmts) {
//try again
currentAttepmt += 1;
} else {
throw e;
}
}
}
}
}
// instead of exporting the updateUserProfile we should export the decorated func, we can control how many retries we want or keep the default
// old code:
export const updateUserProfile = (input) => {
//out legacy already implemented data access code
}
// new code
const updateUserProfileLegacy = (input) => {
//out legacy already implemented data access code
}
export const updateUserProfile = handleDeadLocksHOF(updateUserProfile)