Bu makale şu anda yalnızca İngilizce olarak mevcuttur. Çeviri yakında eklenecektir.
Two Odoo users confirm pickings at almost the same moment. One succeeds. The other gets:
psycopg2.errors.DeadlockDetected:
deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 12346.
Process 12346 waits for ShareLock on transaction 67891; blocked by process 12345.
HINT: See server log for query details.
The retry sometimes succeeds, sometimes fails again. This is the most operationally painful Odoo concurrency issue — it shows up in inventory, accounting, and any model with shared parent rows. Applies to Odoo 17.0/18.0/19.0.
Quick Fix
Enable Odoo's automatic deadlock retry by ensuring RegistryManager.lock_request_retries is set in the controller. For inventory specifically, the retry is usually built-in but may need:
# odoo.conf
db_template = template1
log_level = warn # so deadlock messages show up
For your custom code, wrap risky operations in a retry loop:
from odoo import api
from psycopg2.errors import DeadlockDetected
import time
import random
def _process_with_retry(self, max_attempts=3):
for attempt in range(max_attempts):
try:
return self._actual_process()
except DeadlockDetected:
if attempt == max_attempts - 1:
raise
self.env.cr.rollback()
time.sleep(0.1 * (2 ** attempt) + random.random() * 0.05)
Exponential backoff with jitter is the standard pattern for deadlock retry.
Why This Happens
PostgreSQL deadlocks happen when two transactions hold locks the other needs. In Odoo stock moves, the locking pattern is:
- Transaction A locks
stock.moverow 123. - Transaction A goes to lock
stock.quantrow 456 (the source quant). - Transaction B (concurrent) locked
stock.quantrow 456 first. - Transaction B wants
stock.moverow 123 next. - Both wait for each other → deadlock.
The five common scenarios in Odoo:
- Concurrent reservations on the same product. Two users reserving stock for the same product locking the same
stock.quantrows in different orders. - Picking confirmation simultaneously. Two users confirming different pickings that share a source location.
- Crons running alongside user actions. A reservation cron and an interactive picking colliding.
- Custom code that locks rows in non-canonical order. Always-lock-children-before-parents is the convention; reversing it across transactions creates deadlocks.
account.move.lineposting — accounting moves that touch shared partner balances or analytic accounts in interleaved order.
Step-by-Step Diagnosis
1. Enable detailed lock logging:
# postgresql.conf
log_lock_waits = on
deadlock_timeout = 1s
log_min_duration_statement = 100
Reload PostgreSQL. Reproduce the deadlock. Look at the full lock-wait detail in the log.
2. Identify the two queries that deadlocked. PostgreSQL logs both queries when reporting a deadlock:
ERROR: deadlock detected
DETAIL: Process 12345 waits ... blocked by process 12346
... statement: UPDATE stock_quant SET reserved_quantity = ...
... statement: UPDATE stock_move SET state = 'done' ...
Look at the queries — one is locking quants then moves, the other moves then quants.
3. Trace back to Odoo source. For each query, find the Python code that issued it. Stock module functions like _action_assign, _action_done, _quant_tasks are common origins.
4. Check pg_locks during a stress test:
SELECT pid, locktype, relation::regclass, mode, granted
FROM pg_locks
ORDER BY pid;
Run this in a loop while reproducing. You will see the lock acquisition order each transaction takes.
5. Look for custom code in stock.move overrides. git grep "_action_done\|_action_assign\|_apply_putaway_strategy" custom/ finds custom inventory logic. Each is a candidate for non-canonical lock order.
Permanent Fix
Lock in canonical order. The Odoo convention is: parents before children, lower id before higher id, primary models before derived. For stock:
# RIGHT — always lock the move first, then the quant
move = self.env['stock.move'].browse(move_id)
self.env.cr.execute("SELECT * FROM stock_move WHERE id = %s FOR UPDATE", (move_id,))
quant = self.env['stock.quant'].browse(quant_id)
self.env.cr.execute("SELECT * FROM stock_quant WHERE id = %s FOR UPDATE", (quant_id,))
Sort lock acquisition by id when locking many rows:
def _reserve_quants(self, quants):
sorted_quants = quants.sorted(key=lambda q: q.id)
for quant in sorted_quants:
self.env.cr.execute("SELECT * FROM stock_quant WHERE id = %s FOR UPDATE", (quant.id,))
# ... reserve logic
Add deadlock retry to risky code paths:
from psycopg2.errors import DeadlockDetected, SerializationFailure
def _process_picking(self, picking):
for attempt in range(3):
try:
with self.env.cr.savepoint():
picking._action_done()
return
except (DeadlockDetected, SerializationFailure):
if attempt == 2:
raise
self.env.cr.rollback()
time.sleep(0.1 + random.random() * 0.1)
Three retries with jittered backoff handles 99 percent of transient deadlocks.
Use shorter transactions. Long transactions hold locks longer, increasing deadlock probability. Commit between logical units:
for picking in pickings:
with self.env.cr.savepoint():
picking._action_done()
self.env.cr.commit() # release locks before processing the next
Use SELECT FOR UPDATE SKIP LOCKED for queue-like patterns. When multiple workers process the same queue:
SELECT id FROM stock_move
WHERE state = 'confirmed' AND product_id = %s
FOR UPDATE SKIP LOCKED
LIMIT 10;
SKIP LOCKED makes each worker grab a different subset rather than waiting and deadlocking.
For chronic deadlocks on accounting moves, install OCA's queue_job and process journal entries through the queue. Single-threaded queue processing eliminates concurrency entirely; throughput drops, but consistency is bulletproof.
How to Prevent It
- Always retry on deadlock. Make this a code-review rule. Any code path that touches
stock.quant,account.move.line, or any shared inventory/financial row gets a retry decorator. - Lock in canonical order. A team rule like "always sort by id before locking" prevents cross-transaction order disagreements.
- Short transactions. Commit between logical units of work. Long transactions are deadlock-prone.
- Test under concurrency. Run two simulated users doing the same picking confirmation in parallel. If it deadlocks consistently, fix the lock order. If it deadlocks intermittently, add retry.
SKIP LOCKEDfor queues. Whenever multiple workers compete for a queue of work,SKIP LOCKEDis the right idiom.- Monitor deadlock rate. PostgreSQL's
pg_stat_database.deadlockscounter shows lifetime deadlocks. Spikes indicate a regression.
Related Errors
- Database locked during import — adjacent locking issue.
- Slow list view on > 1M rows — what happens when concurrency is fine but throughput is not.
- Cron job stuck running — what crons can become if they hit deadlocks without retry.
- Too many PostgreSQL connections — when retries pile up and saturate connections.
Frequently Asked Questions
Why does PostgreSQL not just queue the second transaction?
Because that would be a deadlock, just slower. PostgreSQL detects the cycle (two transactions waiting on each other) and aborts one — the alternative is hanging forever. The aborted transaction can retry; the surviving one proceeds.
Should I use SERIALIZABLE isolation level?
It would convert deadlocks into serialization failures (which are also retryable), but at a steep performance cost on Odoo's workload. The default READ COMMITTED with explicit FOR UPDATE and retry is the right choice.
Can I disable deadlock detection?
No, and you would not want to. Disabling deadlock detection means transactions hang forever. The fix is to design code so deadlocks are rare and to retry the rare ones gracefully.
Why do my deadlocks only happen in production?
Production has more concurrent users. A code path that locks in non-canonical order may never deadlock with one user but always deadlock with 50. Test under concurrency in staging before declaring a fix complete.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Yazan
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP ile İşinizi Dönüştürün
Operasyonlarınızı kolaylaştırmak için uzman Odoo uygulaması, özelleştirme ve destek.
İlgili Makaleler
Odoo Form Görünümüne Özel Düğme Nasıl Eklenir (2026)
Odoo 19 form görünümlerine özel eylem düğmeleri ekleyin: Python eylem yöntemi, görünüm devralma, koşullu görünürlük, onay diyalogları. Üretimde test edilmiştir.
Odoo'da Studio Olmadan Özel Alan Nasıl Eklenir (2026)
Odoo 19'daki özel modül aracılığıyla özel alanlar ekleyin: model mirası, görünüm uzantısı, hesaplanan alanlar, mağaza/depo dışı kararlar. Kod öncelikli, sürüm kontrollü.
Odoo'da Harici Düzeni Kullanarak Özel Rapor Nasıl Eklenir?
Web.external_layout'u kullanarak Odoo 19'da markalı bir PDF raporu oluşturun: QWeb şablonu, paperformat, action bağlama. Baskı logosu + altbilgi geçersiz kılmalarıyla.