Code reviewed and issue fixed.
This commit is contained in:
+27
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user