Skip to content

Commit dacef6b

Browse files
committed
Move postgresql connection logic to NormalCursorWrapper._record()
Previously, the logic for determining when to switch transaction IDs was contained in SQLPanel.get_transaction_id(). However, this was not sufficient, as it only looked at the transaction status after the query was finished. If two queries were issued consecutively but in separate transactions, such as in the following: with transaction.atomic(): list(User.objects.all()) with transaction.atomic(): list(Group.objects.all()) both queries would be given the same transaction ID since they would both have the same transaction status after being executed (TRANSACTION_STATUS_INTRANS). Instead, move the logic to NormalCursorWrapper._record() and compare the transaction status before the query to the transaction status after the query to determine if a new transaction was started. Also, use the conn.status attribute for determining transaction status instead of conn.get_transaction_status(), since the former is a local connection variable, while the latter requires a call to the server.
1 parent 6cd5f8e commit dacef6b

File tree

2 files changed

+52
-33
lines changed

2 files changed

+52
-33
lines changed

debug_toolbar/panels/sql/panel.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,36 +61,29 @@ def __init__(self, *args, **kwargs):
6161
self._num_queries = 0
6262
self._queries = []
6363
self._databases = {}
64-
self._transaction_status = {}
64+
# synthetic transaction IDs, keyed by DB alias
6565
self._transaction_ids = {}
6666

67-
def get_transaction_id(self, alias):
68-
if alias not in connections:
69-
return
70-
connection = connections[alias]
71-
conn = connection.connection
72-
if not conn:
73-
return
74-
75-
if connection.vendor == "postgresql":
76-
cur_status = conn.get_transaction_status()
77-
else:
78-
raise ValueError(connection.vendor)
79-
80-
last_status = self._transaction_status.get(alias)
81-
self._transaction_status[alias] = cur_status
82-
83-
if not cur_status:
84-
# No available state
85-
return None
86-
87-
if cur_status != last_status:
88-
if cur_status:
89-
self._transaction_ids[alias] = uuid.uuid4().hex
90-
else:
91-
self._transaction_ids[alias] = None
92-
93-
return self._transaction_ids[alias]
67+
def new_transaction_id(self, alias):
68+
"""
69+
Generate and return a new synthetic transaction ID for the specified DB alias.
70+
"""
71+
trans_id = uuid.uuid4().hex
72+
self._transaction_ids[alias] = trans_id
73+
return trans_id
74+
75+
def current_transaction_id(self, alias):
76+
"""
77+
Return the current synthetic transaction ID for the specified DB alias.
78+
"""
79+
trans_id = self._transaction_ids.get(alias)
80+
# Sometimes it is not possible to detect the beginning of the first transaction,
81+
# so current_transaction_id() will be called before new_transaction_id(). In
82+
# that case there won't yet be a transaction ID. so it is necessary to generate
83+
# one using new_transaction_id().
84+
if trans_id is None:
85+
trans_id = self.new_transaction_id(alias)
86+
return trans_id
9487

9588
def record(self, alias, **kwargs):
9689
self._queries.append((alias, kwargs))

debug_toolbar/panels/sql/tracking.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010

1111
try:
1212
from psycopg2._json import Json as PostgresJson
13+
from psycopg2.extensions import STATUS_IN_TRANSACTION
1314
except ImportError:
1415
PostgresJson = None
16+
STATUS_IN_TRANSACTION = None
1517

1618
# Prevents SQL queries from being sent to the DB. It's used
1719
# by the TemplatePanel to prevent the toolbar from issuing
@@ -139,6 +141,14 @@ def _decode(self, param):
139141
return "(encoded string)"
140142

141143
def _record(self, method, sql, params):
144+
alias = self.db.alias
145+
vendor = self.db.vendor
146+
147+
if vendor == "postgresql":
148+
# The underlying DB connection (as opposed to Django's wrapper)
149+
conn = self.db.connection
150+
initial_conn_status = conn.status
151+
142152
start_time = time()
143153
try:
144154
return method(sql, params)
@@ -156,10 +166,6 @@ def _record(self, method, sql, params):
156166
pass # object not JSON serializable
157167
template_info = get_template_info()
158168

159-
alias = self.db.alias
160-
conn = self.db.connection
161-
vendor = self.db.vendor
162-
163169
# Sql might be an object (such as psycopg Composed).
164170
# For logging purposes, make sure it's str.
165171
sql = str(sql)
@@ -190,9 +196,29 @@ def _record(self, method, sql, params):
190196
iso_level = conn.isolation_level
191197
except conn.InternalError:
192198
iso_level = "unknown"
199+
# PostgreSQL does not expose any sort of transaction ID, so it is
200+
# necessary to generate synthetic transaction IDs here. If the
201+
# connection was not in a transaction when the query started, and was
202+
# after the query finished, a new transaction definitely started, so get
203+
# a new transaction ID from logger.new_transaction_id(). If the query
204+
# was in a transaction both before and after executing, make the
205+
# assumption that it is the same transaction and get the current
206+
# transaction ID from logger.current_transaction_id(). There is an edge
207+
# case where Django can start a transaction before the first query
208+
# executes, so in that case logger.current_transaction_id() will
209+
# generate a new transaction ID since one does not already exist.
210+
final_conn_status = conn.status
211+
if final_conn_status == STATUS_IN_TRANSACTION:
212+
if initial_conn_status == STATUS_IN_TRANSACTION:
213+
trans_id = self.logger.current_transaction_id(alias)
214+
else:
215+
trans_id = self.logger.new_transaction_id(alias)
216+
else:
217+
trans_id = None
218+
193219
params.update(
194220
{
195-
"trans_id": self.logger.get_transaction_id(alias),
221+
"trans_id": trans_id,
196222
"trans_status": conn.get_transaction_status(),
197223
"iso_level": iso_level,
198224
"encoding": conn.encoding,

0 commit comments

Comments
 (0)