Skip to content

pool.connect raises Client has an active query. and the connection never releases #660

Open
@mekto

Description

As in the example below even if we catch an error before returning from pool.connect it throws Client has an active query. and the connection never releases (pendingReleaseConnections increases every time such error occours).

await pool.connect(async (connection) => {
  try {
    const [timestamp, random] = await Promise.all([
      connection.oneFirst(sql.unsafe`SELECT CURRENT_TIMESTAMP`),

      (async function() {
        throw new Error('random error');
        return Math.random();
      })(),
    ]);

    console.log({ timestamp, random });
  } catch (err) {
    console.log('caugth an error');
  }
});

Stack trace:

108 |                     }
109 |                     if (currentState !== 'ACQUIRED') {
110 |                         throw new Error('Client is not acquired.');
111 |                     }
112 |                     if (activeQueryPromise) {
113 |                         throw new Error('Client has an active query.');
                                    ^
error: Client has an active query.
      at .../node_modules/@slonik/driver/dist/factories/createDriverFactory.js:113:31
      at internalRelease (.../node_modules/@slonik/driver/dist/factories/createDriverFactory.js:101:53)
      at release .../node_modules/@slonik/driver/dist/factories/createDriverFactory.js:140:38)
      at .../node_modules/slonik/dist/factories/createConnection.js:108:26

If we replace pool.connect with pool.transaction then it throws Expected `activeQueryPromise` to be set, which could be related to #648.

In this case the stack trace is:

183 |                         }
184 |                         try {
185 |                             activeQueryPromise = query(sql, values);
186 |                             const result = await activeQueryPromise;
187 |                             if (!activeQueryPromise) {
188 |                                 throw new Error('Expected `activeQueryPromise` to be set.');
                                                ^
error: Expected `activeQueryPromise` to be set.
      at .../node_modules/@slonik/driver/dist/factories/createDriverFactory.js:188:43
      at processTicksAndRejections (native:7:39)

Real life example

To provide more context this is real life example from an app build using Sveltekit. This is an server hook (aka middleware) and runs before each request. This is a good place to provide database connection to the app endpoints.

// hooks.server.js

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
  const pool = await reusePoolInstance();

  return pool.connect(async (db) => {
    event.locals.db = db;
    event.locals.user = await getSessionUser(event);

    return resolve(event);  // <-- returns Response object without exception
  });
}

The resolve function returns Promise<Response> and no exception is raised. But what the resolve function does is similar to Promise.all([...]) part from my example at the top. E.g. it tries to load parallel data from a page and its layout and both can load some data from database using provided connection in the hook. If any of them raises en exception (which are caught be the framework before returning from pool.connect) the pool.connect raises Client has an active query. and full page request resolves to 500.

What is alse common is to call throw redirect(302, '/login') in the endpoints. The framework handels it and returns proper Response object, but it causes the 'Client has an active query.' error.

We could wrap pool.connect in the try ... catch block like this:

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
  const pool = await reusePoolInstance();

  let response;
  
  try {
    return await pool.connect(async (db) => {
      event.locals.db = db;
      event.locals.user = await getSessionUser(event);
  
      response = await resolve(event);  // <-- returns Response object without exception
      return response;
    });
  } catch (err) {
    if (err === 'Client has an active query.') {
      return response;
    }
    throw err;
  }
}

and the app handles errors and redirects correctly without the 500 error. But, when pool.connect raises an error, pendingReleaseConnections increases every time and after 10 such cases it is unable to aquire new connection and app hangs and needs to be restarted.

So I consider it as a bug in slonik. Possible workaround could be to have any way to manually realese such failed connection.

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