-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix BEGIN leaking into SQL panel when using DatabaseCache store #2397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,21 @@ | |
| # additional queries. | ||
| allow_sql = contextvars.ContextVar("debug-toolbar-allow-sql", default=True) | ||
|
|
||
| # Whether SQL queries should be recorded by the toolbar. Set to False | ||
| # while the toolbar persists its own data (e.g. the DatabaseCache store), | ||
| # so internal queries like BEGIN/COMMIT don't leak into the SQL panel. | ||
| sql_recording = contextvars.ContextVar("debug-toolbar-sql-recording", default=True) | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def no_sql_recording(): | ||
| """Context manager to temporarily disable SQL recording.""" | ||
| token = sql_recording.set(False) | ||
| try: | ||
| yield | ||
| finally: | ||
| sql_recording.reset(token) | ||
|
|
||
|
|
||
| DDT_MODELS = { | ||
| m._meta.db_table for m in apps.get_app_config("debug_toolbar").get_models() | ||
|
|
@@ -203,74 +218,88 @@ def _record(self, method, sql, params): | |
| stop_time = perf_counter() | ||
| duration = (stop_time - start_time) * 1000 | ||
| _params = None | ||
| with contextlib.suppress(TypeError): | ||
| # Decode params - binary data will be handled by DebugToolbarJSONEncoder | ||
| # in store.py when the panel data is serialized | ||
| _params = self._decode(params) | ||
| template_info = get_template_info() | ||
|
|
||
| # Sql might be an object (such as psycopg Composed). | ||
| # For logging purposes, make sure it's str. | ||
| if vendor == "postgresql" and not isinstance(sql, str): | ||
| if isinstance(sql, bytes): | ||
| sql = sql.decode("utf-8") | ||
| else: | ||
| sql = sql.as_string(conn) | ||
|
|
||
| # Skip tracking for toolbar models by default. | ||
| # This can be overridden by setting SKIP_TOOLBAR_QUERIES = False | ||
| skip_toolbar_queries = dt_settings.get_config()["SKIP_TOOLBAR_QUERIES"] | ||
| if sql_recording.get(): | ||
| should_record = not skip_toolbar_queries or not any( | ||
| table in sql for table in DDT_MODELS | ||
| ) | ||
| else: | ||
| sql = str(sql) | ||
|
|
||
| kwargs = { | ||
| "vendor": vendor, | ||
| "alias": alias, | ||
| "sql": self._last_executed_query(sql, params), | ||
| "duration": duration, | ||
| "raw_sql": sql, | ||
| "params": _params, | ||
| "stacktrace": get_stack_trace(skip=2), | ||
| "template_info": template_info, | ||
| } | ||
| # Inside tracking.no_sql_recording(): only let through queries | ||
| # that mention a known toolbar table, so SKIP_TOOLBAR_QUERIES | ||
| # keeps deciding those. Table-less commands (e.g. the BEGIN/ | ||
| # COMMIT issued by the toolbar's own persistence calls) stay | ||
| # hidden either way (issue #2338). | ||
| should_record = not skip_toolbar_queries and any( | ||
| table in sql for table in DDT_MODELS | ||
| ) | ||
|
Comment on lines
+235
to
+237
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe we want to hide the table-less commands here. |
||
|
|
||
| if vendor == "postgresql": | ||
| # If an erroneous query was ran on the connection, it might | ||
| # be in a state where checking isolation_level raises an | ||
| # exception. | ||
| try: | ||
| iso_level = conn.isolation_level | ||
| except conn.InternalError: | ||
| iso_level = "unknown" | ||
| # PostgreSQL does not expose any sort of transaction ID, so it is | ||
| # necessary to generate synthetic transaction IDs here. If the | ||
| # connection was not in a transaction when the query started, and was | ||
| # after the query finished, a new transaction definitely started, so get | ||
| # a new transaction ID from logger.new_transaction_id(). If the query | ||
| # was in a transaction both before and after executing, make the | ||
| # assumption that it is the same transaction and get the current | ||
| # transaction ID from logger.current_transaction_id(). There is an edge | ||
| # case where Django can start a transaction before the first query | ||
| # executes, so in that case logger.current_transaction_id() will | ||
| # generate a new transaction ID since one does not already exist. | ||
| final_conn_status = conn.info.transaction_status | ||
| if final_conn_status == STATUS_IN_TRANSACTION: | ||
| if initial_conn_status == STATUS_IN_TRANSACTION: | ||
| trans_id = self.logger.current_transaction_id(alias) | ||
| if should_record: | ||
| with contextlib.suppress(TypeError): | ||
| # Decode params - binary data will be handled by DebugToolbarJSONEncoder | ||
| # in store.py when the panel data is serialized | ||
| _params = self._decode(params) | ||
| template_info = get_template_info() | ||
|
|
||
| # Sql might be an object (such as psycopg Composed). | ||
| # For logging purposes, make sure it's str. | ||
| if vendor == "postgresql" and not isinstance(sql, str): | ||
| if isinstance(sql, bytes): | ||
| sql = sql.decode("utf-8") | ||
| else: | ||
| trans_id = self.logger.new_transaction_id(alias) | ||
| sql = sql.as_string(conn) | ||
| else: | ||
| trans_id = None | ||
|
|
||
| kwargs.update( | ||
| { | ||
| "trans_id": trans_id, | ||
| "trans_status": conn.info.transaction_status, | ||
| "iso_level": iso_level, | ||
| } | ||
| ) | ||
| sql = str(sql) | ||
|
|
||
| kwargs = { | ||
| "vendor": vendor, | ||
| "alias": alias, | ||
| "sql": self._last_executed_query(sql, params), | ||
| "duration": duration, | ||
| "raw_sql": sql, | ||
| "params": _params, | ||
| "stacktrace": get_stack_trace(skip=2), | ||
| "template_info": template_info, | ||
| } | ||
|
|
||
| if vendor == "postgresql": | ||
| # If an erroneous query was ran on the connection, it might | ||
| # be in a state where checking isolation_level raises an | ||
| # exception. | ||
| try: | ||
| iso_level = conn.isolation_level | ||
| except conn.InternalError: | ||
| iso_level = "unknown" | ||
| # PostgreSQL does not expose any sort of transaction ID, so it is | ||
| # necessary to generate synthetic transaction IDs here. If the | ||
| # connection was not in a transaction when the query started, and was | ||
| # after the query finished, a new transaction definitely started, so get | ||
| # a new transaction ID from logger.new_transaction_id(). If the query | ||
| # was in a transaction both before and after executing, make the | ||
| # assumption that it is the same transaction and get the current | ||
| # transaction ID from logger.current_transaction_id(). There is an edge | ||
| # case where Django can start a transaction before the first query | ||
| # executes, so in that case logger.current_transaction_id() will | ||
| # generate a new transaction ID since one does not already exist. | ||
| final_conn_status = conn.info.transaction_status | ||
| if final_conn_status == STATUS_IN_TRANSACTION: | ||
| if initial_conn_status == STATUS_IN_TRANSACTION: | ||
| trans_id = self.logger.current_transaction_id(alias) | ||
| else: | ||
| trans_id = self.logger.new_transaction_id(alias) | ||
| else: | ||
| trans_id = None | ||
|
|
||
| kwargs.update( | ||
| { | ||
| "trans_id": trans_id, | ||
| "trans_status": conn.info.transaction_status, | ||
| "iso_level": iso_level, | ||
| } | ||
| ) | ||
|
|
||
| # Skip tracking for toolbar models by default. | ||
| # This can be overridden by setting SKIP_TOOLBAR_QUERIES = False | ||
| if not dt_settings.get_config()["SKIP_TOOLBAR_QUERIES"] or not any( | ||
| table in sql for table in DDT_MODELS | ||
| ): | ||
| # We keep `sql` to maintain backwards compatibility | ||
| self.logger.record(**kwargs) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,7 +206,9 @@ def exists(cls, request_id: str) -> bool: | |
| @classmethod | ||
| def set(cls, request_id: str): | ||
| """Set a request_id in the store and clean up old entries""" | ||
| with transaction.atomic(): | ||
| from debug_toolbar.panels.sql.tracking import no_sql_recording | ||
|
|
||
| with no_sql_recording(), transaction.atomic(): | ||
| # Create the entry if it doesn't exist (ignore otherwise) | ||
| _, created = HistoryEntry.objects.get_or_create(request_id=request_id) | ||
|
|
||
|
|
@@ -227,7 +229,9 @@ def delete(cls, request_id: str): | |
| @classmethod | ||
| def save_panel(cls, request_id: str, panel_id: str, data: Any = None): | ||
| """Save the panel data for the given request_id""" | ||
| with transaction.atomic(): | ||
| from debug_toolbar.panels.sql.tracking import no_sql_recording | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this creates a circular dependency. It may be a good idea to move to a different place.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. I used this aproach to avoid the circular import of tracking module. Some suggestion from where I can move the function?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tim-schilling is it fine to move the sql_recording and the suppress_sql_recording (old no_sql_recording) to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think creating a new module would be fine, though perhaps we should rename it slightly so it's not tailored to the sql panel. |
||
|
|
||
| with no_sql_recording(), transaction.atomic(): | ||
| obj, _ = HistoryEntry.objects.get_or_create(request_id=request_id) | ||
| store_data = obj.data | ||
| store_data[panel_id] = serialize(data) | ||
|
|
@@ -276,10 +280,13 @@ def __getattr__(self, name): | |
|
|
||
| @functools.wraps(attr) | ||
| def untracked(*args, **kwargs): | ||
| from debug_toolbar.panels.sql.tracking import no_sql_recording | ||
|
|
||
| panel = getattr(self._cache, "_djdt_panel", None) | ||
| self._cache._djdt_panel = None | ||
| try: | ||
| return attr(*args, **kwargs) | ||
| with no_sql_recording(): | ||
| return attr(*args, **kwargs) | ||
| finally: | ||
| self._cache._djdt_panel = panel | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the name here may be better as
suppress_sql_recordingas it's a bit clearer on its purpose.