query-exporter 5.2.2__tar.gz → 5.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {query_exporter-5.2.2/query_exporter.egg-info → query_exporter-5.2.3}/PKG-INFO +1 -4
  2. {query_exporter-5.2.2 → query_exporter-5.2.3}/README.rst +0 -3
  3. {query_exporter-5.2.2 → query_exporter-5.2.3}/pyproject.toml +1 -3
  4. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/__init__.py +1 -1
  5. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/db.py +75 -49
  6. {query_exporter-5.2.2 → query_exporter-5.2.3/query_exporter.egg-info}/PKG-INFO +1 -4
  7. {query_exporter-5.2.2 → query_exporter-5.2.3}/LICENSE.txt +0 -0
  8. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/builtin_metrics.py +0 -0
  9. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/config.py +0 -0
  10. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/executor.py +0 -0
  11. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/main.py +0 -0
  12. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/py.typed +0 -0
  13. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/schema.py +0 -0
  14. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter/yaml.py +0 -0
  15. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter.egg-info/SOURCES.txt +0 -0
  16. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter.egg-info/dependency_links.txt +0 -0
  17. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter.egg-info/entry_points.txt +0 -0
  18. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter.egg-info/requires.txt +0 -0
  19. {query_exporter-5.2.2 → query_exporter-5.2.3}/query_exporter.egg-info/top_level.txt +0 -0
  20. {query_exporter-5.2.2 → query_exporter-5.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: query-exporter
3
- Version: 5.2.2
3
+ Version: 5.2.3
4
4
  Summary: Export Prometheus metrics generated from SQL queries
5
5
  Author-email: Alberto Donato <oss@albertodonato.net>
6
6
  Maintainer-email: Alberto Donato <oss@albertodonato.net>
@@ -251,8 +251,6 @@ The image has support for connecting the following databases:
251
251
  - ClickHouse (``clickhouse+native://``)
252
252
  - Teradata (``teradatasql://``)
253
253
 
254
- A `Helm chart`_ to run the container in Kubernetes is also available.
255
-
256
254
  Automated builds from the ``main`` branch are available on the `GitHub container registry`_ via::
257
255
 
258
256
  docker pull ghcr.io/albertodonato/query-exporter:main
@@ -295,7 +293,6 @@ For general purpose questions, you can use `Discussions`_ on GitHub.
295
293
  .. _`Docker Hub`: https://hub.docker.com/r/adonato/query-exporter
296
294
  .. _`configuration file format`: docs/configuration.rst
297
295
  .. _`contribution guide`: docs/contributing.rst
298
- .. _`Helm chart`: https://github.com/makezbs/helm-charts/tree/main/charts/query-exporter
299
296
  .. _`GitHub container registry`: https://github.com/albertodonato/query-exporter/pkgs/container/query-exporter
300
297
  .. _`Discussions`: https://github.com/albertodonato/query-exporter/discussions
301
298
 
@@ -212,8 +212,6 @@ The image has support for connecting the following databases:
212
212
  - ClickHouse (``clickhouse+native://``)
213
213
  - Teradata (``teradatasql://``)
214
214
 
215
- A `Helm chart`_ to run the container in Kubernetes is also available.
216
-
217
215
  Automated builds from the ``main`` branch are available on the `GitHub container registry`_ via::
218
216
 
219
217
  docker pull ghcr.io/albertodonato/query-exporter:main
@@ -256,7 +254,6 @@ For general purpose questions, you can use `Discussions`_ on GitHub.
256
254
  .. _`Docker Hub`: https://hub.docker.com/r/adonato/query-exporter
257
255
  .. _`configuration file format`: docs/configuration.rst
258
256
  .. _`contribution guide`: docs/contributing.rst
259
- .. _`Helm chart`: https://github.com/makezbs/helm-charts/tree/main/charts/query-exporter
260
257
  .. _`GitHub container registry`: https://github.com/albertodonato/query-exporter/pkgs/container/query-exporter
261
258
  .. _`Discussions`: https://github.com/albertodonato/query-exporter/discussions
262
259
 
@@ -96,6 +96,7 @@ tests = [
96
96
  "time-machine",
97
97
  ]
98
98
  types = [
99
+ "pytest-docker",
99
100
  "types-python-dateutil",
100
101
  "types-pyyaml",
101
102
  "types-requests",
@@ -118,9 +119,6 @@ ignore-words-list = "timeschedule"
118
119
  [tool.pyproject-fmt]
119
120
  expand_tables = [ "tool.tox" ]
120
121
 
121
- [tool.ty]
122
- rules.unresolved-import = "ignore"
123
-
124
122
  [tool.pytest]
125
123
  ini_options.testpaths = [
126
124
  "tests",
@@ -1,3 +1,3 @@
1
1
  """Export Prometheus metrics generated from SQL queries."""
2
2
 
3
- __version__ = "5.2.2"
3
+ __version__ = "5.2.3"
@@ -268,38 +268,54 @@ class QueryExecution:
268
268
  )
269
269
 
270
270
 
271
- class ConnectionTracker:
272
- """Track the connection running a query so it can be dropped on timeout."""
271
+ class StatementCanceller:
272
+ """Cancel the statement running on a connection from another thread.
273
+
274
+ The connection itself is never closed or invalidated from another thread,
275
+ as neither SQLAlchemy nor DBAPI connections are thread-safe. Instead, the
276
+ driver's `cancel()` (e.g. oracledb, psycopg) or `interrupt()` (sqlite3)
277
+ method is used if available, since drivers providing them support calling
278
+ them while another thread is executing a statement. The statement then
279
+ fails on the thread running it.
280
+ """
281
+
282
+ CANCEL_METHODS = ("cancel", "interrupt")
273
283
 
274
284
  def __init__(self) -> None:
275
285
  self._lock = Lock()
276
- self._conn: Connection | None = None
286
+ self._dbapi_conn: DBAPIConnection | None = None
287
+ self.cancelled = False
277
288
 
278
289
  @contextmanager
279
290
  def track(self, conn: Connection) -> Generator[None]:
280
291
  """Track `conn` for the duration of the statement.
281
292
 
282
293
  The connection is untracked before it's returned to the pool, so that a
283
- late invalidation can't drop a connection another query has since
284
- checked out.
294
+ late cancel can't hit a statement from another query that has since
295
+ checked out the connection.
285
296
  """
286
297
  with self._lock:
287
- self._conn = conn
298
+ self._dbapi_conn = conn.connection.dbapi_connection
288
299
  try:
289
300
  yield
290
301
  finally:
291
302
  with self._lock:
292
- self._conn = None
303
+ self._dbapi_conn = None
293
304
 
294
- def invalidate(self) -> None:
295
- """Drop the connection running the statement, if any.
305
+ def cancel(self) -> bool:
306
+ """Cancel the running statement, if any.
296
307
 
297
- This performs I/O and, depending on the driver, can block until the
298
- statement completes on its own, so it must never run on the event loop.
308
+ Return whether the driver supports cancelling statements.
299
309
  """
300
310
  with self._lock:
301
- if self._conn is not None:
302
- self._conn.invalidate()
311
+ if self._dbapi_conn is None:
312
+ return True
313
+ for name in self.CANCEL_METHODS:
314
+ if cancel := getattr(self._dbapi_conn, name, None):
315
+ self.cancelled = True
316
+ cancel()
317
+ return True
318
+ return False
303
319
 
304
320
 
305
321
  class Database:
@@ -326,17 +342,17 @@ class Database:
326
342
  max_workers=max_workers,
327
343
  thread_name_prefix=f"[Database-{self.name}]",
328
344
  )
329
- # Timed out queries are dropped from a separate pool: the query
330
- # executor is by definition saturated by the queries being dropped. At
331
- # most max_workers queries can be in flight, so the same size is enough.
332
- self._timeout_executor = ThreadPoolExecutor(
345
+ # Cancelling a statement can perform I/O, so it's done off the event
346
+ # loop, in a separate pool since the query one is by definition
347
+ # saturated by the statements being cancelled.
348
+ self._cancel_executor = ThreadPoolExecutor(
333
349
  max_workers=max_workers,
334
- thread_name_prefix=f"[Database-{self.name}-timeout]",
350
+ thread_name_prefix=f"[Database-{self.name}-cancel]",
335
351
  )
336
352
 
337
353
  def close(self) -> None:
338
354
  self._executor.shutdown(wait=True, cancel_futures=True)
339
- self._timeout_executor.shutdown(wait=False, cancel_futures=True)
355
+ self._cancel_executor.shutdown(wait=False, cancel_futures=True)
340
356
  self._engine.dispose()
341
357
 
342
358
  async def execute(self, query_execution: QueryExecution) -> MetricResults:
@@ -373,47 +389,38 @@ class Database:
373
389
  parameters = {}
374
390
 
375
391
  loop = asyncio.get_event_loop()
376
- tracker = ConnectionTracker()
392
+ canceller = StatementCanceller()
377
393
  future = loop.run_in_executor(
378
- self._executor, self._execute_sync, statement, parameters, tracker
394
+ self._executor,
395
+ self._execute_sync,
396
+ statement,
397
+ parameters,
398
+ canceller,
379
399
  )
380
400
  try:
381
401
  return await asyncio.wait_for(
382
402
  asyncio.shield(future), timeout=timeout
383
403
  )
384
404
  except TimeoutError:
385
- self._on_timeout(future, tracker)
405
+ # a queued statement won't run at all, the outcome of a running one
406
+ # is discarded
407
+ future.cancel()
408
+ self._cancel_executor.submit(self._cancel_sync, canceller)
386
409
  raise
387
410
 
388
- def _on_timeout(
389
- self,
390
- future: asyncio.Future[QueryResults],
391
- tracker: ConnectionTracker,
392
- ) -> None:
393
- """Drop the connection of a query that timed out.
394
-
395
- The result is deliberately not awaited: dropping the connection can
396
- block for as long as the query itself, and the timeout must be reported
397
- right away.
398
- """
399
- self._timeout_executor.submit(self._invalidate_sync, tracker)
400
- # the shielded future outlives the timeout, consume its outcome so that
401
- # asyncio doesn't report it as never retrieved
402
- future.add_done_callback(self._discard_outcome)
403
-
404
- def _invalidate_sync(self, tracker: ConnectionTracker) -> None:
411
+ def _cancel_sync(self, canceller: StatementCanceller) -> None:
405
412
  try:
406
- tracker.invalidate()
413
+ if not canceller.cancel():
414
+ self.logger.debug(
415
+ "driver doesn't support cancelling queries, "
416
+ "waiting for timed out query to complete"
417
+ )
407
418
  except Exception as error:
408
419
  self.logger.warning(
409
- "failed dropping connection for timed out query",
420
+ "failed cancelling timed out query",
410
421
  error=self._error_message(error),
411
422
  )
412
423
 
413
- def _discard_outcome(self, future: asyncio.Future[QueryResults]) -> None:
414
- if not future.cancelled():
415
- future.exception()
416
-
417
424
  def _setup_engine(self) -> Engine:
418
425
  engine = create_db_engine(self.config)
419
426
 
@@ -446,21 +453,40 @@ class Database:
446
453
  self,
447
454
  statement: TextClause,
448
455
  parameters: dict[str, Any],
449
- tracker: ConnectionTracker,
456
+ canceller: StatementCanceller,
450
457
  ) -> QueryResults:
451
458
  try:
452
- with self._engine.begin() as conn, tracker.track(conn):
459
+ with self._engine.begin() as conn, canceller.track(conn):
453
460
  try:
454
461
  start_time = perf_counter()
455
462
  result = conn.execute(statement, parameters)
456
463
  latency = perf_counter() - start_time
457
464
  return QueryResults.from_result(result, latency=latency)
458
465
  except Exception as error:
459
- raise self._db_error(error, exc_class=DatabaseQueryError)
466
+ raise self._execute_error(
467
+ error, DatabaseQueryError, canceller
468
+ )
460
469
  except DatabaseQueryError:
461
470
  raise
462
471
  except Exception as error:
463
- raise self._db_error(error, exc_class=DatabaseConnectError)
472
+ raise self._execute_error(error, DatabaseConnectError, canceller)
473
+
474
+ def _execute_error(
475
+ self,
476
+ error: Exception,
477
+ exc_class: type[DatabaseError],
478
+ canceller: StatementCanceller,
479
+ ) -> DatabaseError:
480
+ """Create a DatabaseError for a failed statement.
481
+
482
+ Errors from cancelled statements are expected, and the timeout has
483
+ already been reported, so they're not logged as errors.
484
+ """
485
+ if not canceller.cancelled:
486
+ return self._db_error(error, exc_class=exc_class)
487
+ message = self._error_message(error)
488
+ self.logger.debug("timed out query cancelled", error=message)
489
+ return exc_class(message)
464
490
 
465
491
  def _query_db_error(
466
492
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: query-exporter
3
- Version: 5.2.2
3
+ Version: 5.2.3
4
4
  Summary: Export Prometheus metrics generated from SQL queries
5
5
  Author-email: Alberto Donato <oss@albertodonato.net>
6
6
  Maintainer-email: Alberto Donato <oss@albertodonato.net>
@@ -251,8 +251,6 @@ The image has support for connecting the following databases:
251
251
  - ClickHouse (``clickhouse+native://``)
252
252
  - Teradata (``teradatasql://``)
253
253
 
254
- A `Helm chart`_ to run the container in Kubernetes is also available.
255
-
256
254
  Automated builds from the ``main`` branch are available on the `GitHub container registry`_ via::
257
255
 
258
256
  docker pull ghcr.io/albertodonato/query-exporter:main
@@ -295,7 +293,6 @@ For general purpose questions, you can use `Discussions`_ on GitHub.
295
293
  .. _`Docker Hub`: https://hub.docker.com/r/adonato/query-exporter
296
294
  .. _`configuration file format`: docs/configuration.rst
297
295
  .. _`contribution guide`: docs/contributing.rst
298
- .. _`Helm chart`: https://github.com/makezbs/helm-charts/tree/main/charts/query-exporter
299
296
  .. _`GitHub container registry`: https://github.com/albertodonato/query-exporter/pkgs/container/query-exporter
300
297
  .. _`Discussions`: https://github.com/albertodonato/query-exporter/discussions
301
298
 
File without changes