prisma-client-extensions icon indicating copy to clipboard operation
prisma-client-extensions copied to clipboard

Callback Free Interactive Transactions -- exhausting connection pools in Prisma 5

Open zachequi opened this issue 1 year ago • 2 comments

We've been using (a variant of) the callback-free-itx extension with Prisma 4 in production for a while now, it's been rock solid. We're exploring the upgrade to Prisma 5 and finding that periodically we're getting "unable to start a transaction in the given time" P2028 errors. We've narrowed it down to the callback free interactive transaction code. If we don't use transactions at all then we're stable.

I swapped over our implementation to be the exact one in this repository and we're seeing the same behavior. I don't have a small reproduction case yet, I'm opening this ticket for visibility and in the hope that somebody else has seen this on Prisma 5 and perhaps can point me in the right direction.

zachequi avatar Sep 12 '23 22:09 zachequi

Incase this is helpful for anyone finding this from a search, I'm seeing good progress by not using a proxy object and instead returning a brand new object in $begin. For us that looks like:

return {    
    $commit: async () => {
      commit();
      await prismaTranactionResult;
    },
    $rollback: async () => {
      rollback();
      await prismaTranactionResult.catch((err) => {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        if (err.message !== PRISMA_ROLLBACK_MSG) {
          console.log(`Rollback txn, cause: ${err}`);
        }
      });
    },
    ...capturedPrismaTxClient,
    $executeRaw: capturedPrismaTxClient.$executeRaw,
    $executeRawUnsafe: capturedPrismaTxClient.$executeRawUnsafe,
    // @ts-expect-error we're copying an internal method
    $executeRawInternal: capturedPrismaTxClient.$executeRawInternal,
    // @ts-expect-error we're copying an internal method
    _createPrismaPromise: capturedPrismaTxClient._createPrismaPromise,
    // @ts-expect-error we're copying an internal method
    _request: capturedPrismaTxClient._request,
    // @ts-expect-error we're copying an internal method
    _executeRequest: capturedPrismaTxClient._executeRequest,
  };

zachequi avatar Sep 12 '23 22:09 zachequi

we got similar issue can you please share your implementation or point me what needs to be fixed here?

{
  code: 'P2028',
  clientVersion: '5.4.2',
  meta: {
    error: "Transaction not found. Transaction ID is invalid, refers to an old closed transaction Prisma doesn't have information about anymore, or was obtained before disconnecting."
  }
}

this is our implementation

  public readonly prisma = new PrismaClient({ log: ['query'] }).$extends({
    client: {
      async $begin() {
        const prisma = Prisma.getExtensionContext(this);
        let setTxClient: (txClient: Prisma.TransactionClient) => void;
        let commit: () => void;
        let rollback: () => void;

        // a promise for getting the tx inner client
        const txClient = new Promise<Prisma.TransactionClient>((res) => {
          setTxClient = (txClient) => res(txClient);
        });

        // a promise for controlling the transaction
        const txPromise = new Promise((_res, _rej) => {
          commit = () => _res(undefined);
          rollback = () => _rej(this.ROLLBACK);
        });

        // opening a transaction to control externally
        if (
          '$transaction' in prisma &&
          typeof prisma.$transaction === 'function'
        ) {
          const tx = prisma.$transaction((txClient) => {
            setTxClient(txClient as unknown as Prisma.TransactionClient);

            return txPromise.catch((e) => {
              if (e === this.ROLLBACK) return;
              throw e;
            });
          });

          // return a proxy TransactionClient with `$commit` and `$rollback` methods
          return new Proxy(await txClient, {
            get(target, prop) {
              if (prop === '$commit') {
                return () => {
                  commit();
                  return tx;
                };
              }
              if (prop === '$rollback') {
                return () => {
                  rollback();
                  return tx;
                };
              }
              return target[prop as keyof typeof target];
            },
          }) as FlatTransactionClient;
        }

        throw new Error('Transactions are not supported by this client');
      },
    },
  });

saboorajat avatar Nov 08 '23 13:11 saboorajat