Does KnexJS + SQL Server support processing multiple transactions at the same time?

61 views Asked by At

I'd like to know if I can process multiple separate database transactions at once, using knexjs and SQL Server (SQL Server 2022 Docker image).

As for the specifics regarding transactions, I need to transfer money from account A to account B, both accounts are stored in the same table. Here is the DB schema for the table:

CREATE TABLE dbo.BankAccount (
  AccountGUID     char(16) NOT NULL, 
  Amount          decimal(7, 2) NULL,
  CONSTRAINT PK_TestContext PRIMARY KEY (TestContextGUID)
);

The process involves also some backend business logic that's why I decided to make use of a read-modify-write strategy with a strong transaction isolation level (SERIALIZABLE) - I use knexjs object-based transactions to apply the transaction isolation level. The whole process looks like this:

  1. A transaction object is acquired:

const txc = await knex.transaction({ isolationLevel: "SERIALIZABLE"})

  1. Read for account A:

const A = await knex.table("dbo.BankAccount").select('*').where('AccountGUID', idA).first().transacting(txc),

  1. Read for account B:

const B = await knex.table("dbo.BankAccount").select('*').where('AccountGUID', idB).first().transacting(txc),

  1. Business logic is applied.

  2. Update for account A:

await knex.update(A).where('AccountGUID',idA).transacting(txc)

  1. Update for account B:

await knex.update(B).where('AccountGUID',idB).transacting(txc)

  1. Transaction is committed:

await txc.commit()

I applied the following connection config for knexjs:

{ 
 client: 'mssql',
 connection: {
   host: db,
   port: 1433,
   user: 'username',
   password: 'password',
   database: 'db',
   options: {
     encrypt: true,
     useUTC: true,
     appName: 'AppName',
     trustServerCertificate: true,
   },
   pool: {
     min: 2,
     max: 5,
     idleTimeoutMillis: 5000,
     softIdleTimeoutMillis: 2000,
     evictionRunIntervalMillis: 500,
   },
}

Based on the configuration, I assumed the number of connections in the connection pool should be in the range of 2-5 and scale up/down based on the number of queries and transactions awaiting. However, when I was testing race condition and integrity of the final state, I noticed that knex doesn't scale up the number of connections in the pool even if multiple transactions are awaiting.

My test scenario is based on executing multiple transfer money transactions between the same accounts at once (in the range 10-20) - the first transaction is always processed correctly, but the rest fails with the error Requests can only be made in the LoggedIn state, not the SentClientRequest state during acquiring the transaction object (const txc = await knex transaction({...}) step), because they try to use the same connection the first transaction is using. I believe the connection pool management should be knexjs responsibility (correct me here if I'm wrong). I tried manually retrying the intialization of the transaction with the following loop:

let retries = 0;
 while (retries < 20) {
  try {
   await new Promise((res) => setTimeout(res, 1000));
   return await this.knex.transaction({ isolationLevel: 'serializable' });
  } catch (err) {
     retries += 1;
  }
}

and the error changed, I started getting timeout errors on reading the account from the database, as if there were still some locks even though the previous transactions were committed (or rollbacked in case of error): select top (@p0) * from [dbo.BankAccount] where [AccountGUID] = @p1 - Timeout: Request failed to complete in 15000ms. Could you provide me with the solution for this problem or some hints on where to find the answers?

0

There are 0 answers