Adding a company answers 500 "database is locked" when the form is sent twice, though the company was saved #206
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#206
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happened
On ragnar (
f99fb3138,0.2.1-dev.f99fb31), adding France Travail as a company on Companies → New showed an error 500 page at 2026-09-15 16:23:16 (+0200).The access log shows two
POST /jobs/companies/new/from the same browser in the same second:The first saved the company and redirected. The second failed about 100 ms later:
The company exists once (
pk 15,France Travail, kindemployment_service, created14:23:16.291 UTC), so no data was lost or duplicated. But the browser shows the response to the last request, so the person sees a 500 for something that worked. That invites a retry, and a retry creates a duplicate.Why
Three things together:
static/js/app.jshas no submit guard, and the company form has none of its own. A double click, or Enter and then a click, sends two POSTs.docker/Dockerfile:--workers 3), so the two POSTs really do run at the same time.DATABASESinconfig/settings/base.pysets noOPTIONS. On ragnar:journal_mode=delete,busy_timeout=5000,ATOMIC_REQUESTS=True, Django 6.1.1. Every request is aDEFERREDtransaction. The second request starts it with a read (the form's validation), then tries to upgrade to a write while the first holds the write lock. SQLite refuses that upgrade withSQLITE_BUSYstraight away and does not wait out the busy timeout, since waiting could deadlock. That is why it failed in 100 ms and not after 5 s.Whatever takes the write lock early can hit this: a double submit, two tabs, or the browser extension capturing while someone edits. Companies are just where it showed first.
LogoFormMixinalso runslogos.from_urlinside the request transaction, so a slow logo fetch holds the write lock for the whole network round trip and makes the window wider.What would fix it
base.pywhenever the engine is sqlite3. Django 5.1+ supports all of these:"transaction_mode": "IMMEDIATE": the write lock is taken when the transaction begins, so a second writer waits (the busy timeout then applies) and is not refused."init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;": readers stop blocking the writer and the writer stops blocking readers."timeout"(for example 20 s) rather than relying on the 5 s default.manage.py backupstill produces a consistent copy under WAL (the-wal/-shmfiles), and that the image's data volume allows them.app.jsfor everyform[method=post]: disable the submitter after the first submit, and re-enable it onpageshowso the back button does not leave a dead form. It must still work with the script blocked, which today's forms already do.transaction.on_commit).Tests
DATABASES["default"]["OPTIONS"]getstransaction_mode=IMMEDIATEand WAL for a file database and leaves:memory:alone.