Skip to content

Interrupt/force close db connection when HTTP request finishes  #394

Open
@davidmartos96

Description

@davidmartos96

Is there any way to force close the database connection in a pool, even if there are queries running? The use case is to force close a connection obtained during an HTTP request in case of a timeout managed from the routes/middlewares layer in the HTTP server.
We've recently encountered with an issue on our server which made the database fill up the available connections because HTTP was timing out, but the database was still processing queries, even after the HTTP socket was closed.

We were able to achieve what we want with some tweaks in the postgres package, but I would like to hear your opinion in case you feel an alternative API would be better. Ideally without requiring extending a Pool implementation

Similar previous work:
grafana/grafana#22123

Postgres Dart fork with changes: https://github.com/davidmartos96/postgresql-dart/tree/feat/abort_conn

Custom pool, but it extends the PoolImplementation

/// A pool that aborts the connection if receives an abort signal.
/// This is useful to abort the db query if the request is aborted.
class AbortSignalAwarePool<T> extends pool_impl.PoolImplementation<T> {
  AbortSignalAwarePool(super.selector, super.settings);

  factory AbortSignalAwarePool.withEndpoints(
    List<pg.Endpoint> endpoints, {
    pg.PoolSettings? settings,
  }) =>
      AbortSignalAwarePool(pool_impl.roundRobinSelector(endpoints), settings);

  /// This method is the only one that is overridden to add the abort signal handling.
  /// and let the postgres library know about the error outside the connection callback
  /// so that it releases the connection.
  @override
  Future<R> withConnection<R>(
    Future<R> Function(pg.Connection connection) fn, {
    pg.ConnectionSettings? settings,
    locality,
  }) {
    // AbortSignal internally it's a Completer that can only be completed with an error
    // There can be an AbortSignal in the current Dart zone if we pass it through
    // in an appropriate HTTP middleware
    final AbortSignal? abortSignal = getAbortSignalInZone();

    final Future<R> Function(pg.Connection connection) effectiveFn;
    if (abortSignal == null) {
      effectiveFn = fn;
    } else {
      effectiveFn = (connection) async {
        try {
          // Internally it's a Future.any([abortSignal, fn(connection)])
          // so that the connection callbacks throws if the abort signal is triggered
          return await abortSignal.waitFuture(fn(connection));
        } catch (e) {
          if (abortSignal.wasAborted) {
            // Interrupt the pg connection if the http request has finished
            // to avoid dangling connections
            unawaited(connection.close(interruptRunning: true));
          }
          rethrow;
        }
      };
    }

    return super.withConnection(effectiveFn, settings: settings, locality: locality);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions