serverless icon indicating copy to clipboard operation
serverless copied to clipboard

Prisma calls hanging on Cloudflare Workers

Open jmif opened this issue 1 year ago • 5 comments

Not sure if this is a Prisma issue or a Neon issue, so opening both (Prisma issue here)

When running with wrangler dev using Prisma + NeonDB, the promises from basic database calls do not resolve, but the results are reflected in the database. I've followed the instructions here.

Steps to reproduce

I've created a repro here.

  1. Clone repo and add DATABASE_URL=xxx to .dev.vars in the root of the repo.
  2. Run npm run dev
  3. Performa GET request to the root route (port will be specified in npm run dev output)
  4. The request should hang and you should see the first console.log statement but not the second. You should also see the data from the first call inserted into the database, but not from the second.

Expected result

Request does not hang and inserts two records.

Actual result

Request hangs, promise of initial query does not resolve, but initial query is successful in that it's result is reflected in database.

Environment

OS: macOS Database: Neon Postgres 16 Node.js version: 20.11.1

jmif avatar Dec 05 '24 23:12 jmif

FYI I think this is an issue with this package, or at least an issue in with the docs. I removed Prisma completely and attempted to query my Neon database directly using a Pool and the same issue occurred.

When I set this, queries start working again:

neonConfig.poolQueryViaFetch = true;

According to the docs here this shouldn't be required to work in Cloudflare workers, so I think it's a bug. I also tried to set the ws constructor as I'm using Node compatibility mode but requests continued to hang.

jmif avatar Dec 22 '24 22:12 jmif

@jmif Did you test this with Prisma too? I have the same issue, even with set neonConfig.poolQueryViaFetch = true;

My prisma.ts

import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import { PrismaNeon } from '@prisma/adapter-neon';
import { Pool, neonConfig } from '@neondatabase/serverless';

import ws from 'ws';
neonConfig.webSocketConstructor = ws;

// To work in edge environments (Cloudflare Workers, Vercel Edge, etc.), enable querying over fetch
neonConfig.poolQueryViaFetch = true

// Type definitions
declare global {
   var prisma: PrismaClient | undefined
}

const connectionString = `${process.env.DATABASE_URL}`;

const pool = new Pool({ connectionString });
const adapter = new PrismaNeon(pool);
const prisma = global.prisma || new PrismaClient({ adapter });

if (process.env.NODE_ENV === 'development') global.prisma = prisma;

export default prisma;

nvti avatar Dec 23 '24 02:12 nvti

Yeah - adding the poolQueryViaFetch makes Prisma work for me

jmif avatar Dec 23 '24 03:12 jmif

Maybe the difference is I use NextJS (next-on-pages). My current workaround is to recreate prisma instance on every request. It makes my API too slow.

nvti avatar Dec 23 '24 03:12 nvti

Specifying maxUses: 1 on the Pool constructor fixed the calls hanging for me:

new Pool({ connectionString, maxUses: 1 })

This fixes the hanging while keeping Neon over websockets, so supporting interactive transactions (which is prevented by the alternative mentioned above of enabling poolQueryViaFetch).

I tracked this down after quite a bit of time debugging the internals of pg-pool, identifying that specifically the issue is when a Client is reused from the Pool. (For me it was a bit nondeterministic due to timing of awaiting several transactions in parallel, causing Clients to be reused sometimes but not yet released for reuse during other executions of the same request, avoiding the hang.)

As I understand it, this maxUses workaround effectively stops the Pool operating as a pool - i.e. a new Client is created each time one is requested from the pool, with no reuse of Clients that have been released back to the pool after being used once (by Prisma calling client.release(), which it does at the end of a transaction).

While in a non-serverless environment the disabling of a Pool that reuses Clients would be very bad, creating a new Neon Client for each transaction has a limited impact under serverless e.g. Cloudflare Workers, where clients are created and destroyed per-request in any case, not shared across requests.

It would still be good to fix the root cause of the hanging when a Client is reused from the pool, so the overhead of creating additional websocket connections for each new Client can be avoided.

I didn't get far enough to understand what causes the hang, so after spending a bunch of time tracking down when it occurs I've stuck with this workaround for now.

andyjy avatar Feb 10 '25 09:02 andyjy