Skip to content

Commit 13fd0ee

Browse files
committed
fix: Monkeypatch BigQuery library so KeyboardInterrupt cancels active query
1 parent 8135e40 commit 13fd0ee

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

deepnote_toolkit/sql/sql_execution.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import uuid
66
import warnings
7+
from typing import Any
78
from urllib.parse import quote
89

910
import google.oauth2.credentials
@@ -24,6 +25,7 @@
2425
get_project_auth_headers,
2526
)
2627
from deepnote_toolkit.ipython_utils import output_sql_metadata
28+
from deepnote_toolkit.logging import LoggerManager
2729
from deepnote_toolkit.ocelots.pandas.utils import deduplicate_columns
2830
from deepnote_toolkit.sql.duckdb_sql import execute_duckdb_sql
2931
from deepnote_toolkit.sql.jinjasql_utils import render_jinja_sql_template
@@ -33,6 +35,53 @@
3335
from deepnote_toolkit.sql.sql_utils import is_single_select_query
3436
from deepnote_toolkit.sql.url_utils import replace_user_pass_in_pg_url
3537

38+
logger = LoggerManager().get_logger()
39+
40+
41+
# TODO(BLU-5171): Temporary hack to allow cancelling BigQuery jobs on KeyboardInterrupt (e.g. when user cancels cell execution)
42+
# Can be removed once
43+
# 1. https://github.com/googleapis/python-bigquery/pull/2331 is merged and released
44+
# 2. Dependicies updated for the toolkit. We don't depend on google-cloud-bigquery directly, but it's transitive
45+
# dependency through sqlalchemy-bigquery
46+
def _monkeypatch_bigquery_wait_or_cancel():
47+
try:
48+
from typing import Optional, Union
49+
50+
import google.cloud.bigquery._job_helpers as _job_helpers
51+
from google.cloud.bigquery import job, table
52+
53+
def _wait_or_cancel(
54+
job_obj: job.QueryJob,
55+
api_timeout: Optional[float],
56+
wait_timeout: Optional[Union[object, float]],
57+
retry: Optional[Any],
58+
page_size: Optional[int],
59+
max_results: Optional[int],
60+
) -> table.RowIterator:
61+
try:
62+
return job_obj.result(
63+
page_size=page_size,
64+
max_results=max_results,
65+
retry=retry,
66+
timeout=wait_timeout,
67+
)
68+
except (KeyboardInterrupt, Exception):
69+
try:
70+
job_obj.cancel(retry=retry, timeout=api_timeout)
71+
except (KeyboardInterrupt, Exception):
72+
pass
73+
raise
74+
75+
_job_helpers._wait_or_cancel = _wait_or_cancel
76+
logger.debug("Successfully monkeypatched google.cloud.bigquery._job_helpers._wait_or_cancel")
77+
except ImportError:
78+
logger.debug("Could not monkeypatch BigQuery _wait_or_cancel: google.cloud.bigquery not available")
79+
except Exception as e:
80+
logger.warning("Failed to monkeypatch BigQuery _wait_or_cancel: %s", repr(e))
81+
82+
83+
_monkeypatch_bigquery_wait_or_cancel()
84+
3685

3786
def compile_sql_query(
3887
skip_jinja_template_render,

tests/unit/test_sql_execution_internal.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
from deepnote_toolkit.sql import sql_execution as se
1010

1111

12+
def test_bigquery_wait_or_cancel_handles_keyboard_interrupt():
13+
import google.cloud.bigquery._job_helpers as _job_helpers
14+
15+
mock_job = mock.Mock()
16+
mock_job.result.side_effect = KeyboardInterrupt("User interrupted")
17+
mock_job.cancel = mock.Mock()
18+
19+
with pytest.raises(KeyboardInterrupt):
20+
# _wait_or_cancel should be monkeypatched by `_monkeypatch_bigquery_wait_or_cancel`
21+
_job_helpers._wait_or_cancel(
22+
job_obj=mock_job,
23+
api_timeout=30.0,
24+
wait_timeout=60.0,
25+
retry=None,
26+
page_size=None,
27+
max_results=None,
28+
)
29+
30+
mock_job.cancel.assert_called_once_with(retry=None, timeout=30.0)
31+
32+
1233
def test_build_params_for_bigquery_oauth_ok():
1334
with mock.patch(
1435
"deepnote_toolkit.sql.sql_execution.bigquery.Client"

0 commit comments

Comments
 (0)