Code reviewed and issue fixed.

This commit is contained in:
2026-03-27 14:01:11 -04:00
parent d8828d487a
commit 359c801027
10 changed files with 788 additions and 113 deletions
+27 -4
View File
@@ -124,11 +124,34 @@ class Ticket(db.Model):
history = db.relationship('TicketHistory', backref='ticket', lazy='dynamic', cascade='all, delete-orphan')
def generate_ticket_number(self):
"""Generate unique ticket number like TKT-20240101-0001"""
"""Generate a unique ticket number like TKT-20240101-0001.
Concurrency safety
------------------
The naive approach — query the last ticket, increment its sequence,
then insert — has a TOCTOU race: two concurrent requests can both read
the same "last" ticket and generate the same next number, causing an
IntegrityError at commit time.
We eliminate the race by appending FOR UPDATE to the SELECT. This
acquires a row-level write lock on the last matching ticket for the
duration of the current transaction, serialising concurrent callers
through the database rather than through application code. The lock is
released automatically when the transaction commits or rolls back.
MySQL / MariaDB: with_for_update() emits SELECT … FOR UPDATE.
SQLite (dev/test): SELECT … FOR UPDATE is silently ignored, which is
acceptable because SQLite's connection-level locking already prevents
concurrent writes in practice.
"""
date_str = datetime.utcnow().strftime('%Y%m%d')
last = Ticket.query.filter(
Ticket.ticket_number.like(f'TKT-{date_str}-%')
).order_by(Ticket.id.desc()).first()
last = (
Ticket.query
.filter(Ticket.ticket_number.like(f'TKT-{date_str}-%'))
.order_by(Ticket.id.desc())
.with_for_update() # acquires a write lock; serialises concurrent callers
.first()
)
if last:
seq = int(last.ticket_number.split('-')[-1]) + 1
else: