Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prisma client hangs when using Graphql Yoga server on lambda #2241

Closed
jonthewayne opened this issue Apr 20, 2020 · 2 comments
Closed

Prisma client hangs when using Graphql Yoga server on lambda #2241

jonthewayne opened this issue Apr 20, 2020 · 2 comments

Comments

@jonthewayne
Copy link

jonthewayne commented Apr 20, 2020

Bug description

We're using graphql yoga 1.18.3 and deploying via the most recent serverless framework to aws lambda:

serverless.yml:

functions:
  graphql:
    handler: src/functions/server.server
    events:
      - http:
          path: /
          method: post
          cors: true

server.ts:

import { GraphQLServerLambda } from 'graphql-yoga';
import { typeDefs } from '../schema';
import resolvers from '../resolvers';
import permissions from '../permissions';
import { createContext } from '../context';

const lambda = new GraphQLServerLambda({
  typeDefs,
  resolvers,
  middlewares: [permissions],
  context: createContext
});

exports.server = lambda.graphqlHandler;
exports.playground = lambda.playgroundHandler;

context.ts:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export interface Context {
  req?: any;
  prisma: PrismaClient;
}

export function createContext(req): Context {
  return { ...req, prisma };
}

Using this generator block in schema.prisma:

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

We're connecting to a large aurora mysql 5.7 db.

The client call to the db works, but the server then "hangs" after the resolver, consistently making the lambda last 6 seconds before returning a 502 internal server error.

How to reproduce

1.) Setup a quick sample graphql yoga server and deploy it to aws lambda behind an http endpoint.
2.) Create a simple mutation resolver that uses the prisma client.
3.) Access the graphql playground by putting the http endpoint into your browser.
4.) Try the mutation and note the lag and 502 response.

Expected behavior

The function should return quickly and without a 502 internal server error. The only workaround was to run prisma.disconnect() during or right after every resolver. I tried to create a middleware but the "hang" happened before that could be run.

We already had a function that wrapped around each resolver, so I was able to add the disconnect there. I have no idea if that is helpful, but it's working for now:

catch.ts:

export const catchResult = fn => {
    return function (...args) {
        // hook prisma instance here so we can use to disconnect below
        const prisma = args[2].prisma;
        return fn(...args)
            .then((result) => {
                // resolver was successful, disconnect from prisma and return result
                prisma.disconnect();
                console.log("Prisma client successfully disconnected.");
                return result;
            }).catch((err) => {
                // resolver threw an error, disconnect from prisma and throw on
                prisma.disconnect();
                console.log("Prisma client successfully disconnected.");
                throw err;
            });
    };
};

Then in the resolver, we import and wrap our function like this:

signupUser: catchResult(async (_, { firstName, lastName, email, password }, ctx) => {
    const validEmail = validateEmail(email);
    if (!validEmail) {
      throw new Error('Invalid email.');
    }

    const validPassword = validatePassword(password);
    if (!validPassword) {
      throw new Error('The password is too short.');
    }

    const duplicateEmail = await ctx.prisma.user.findMany({ where: { email } });
    if (duplicateEmail.length > 0) {
      throw new Error('There is already an account with this email address.');
    }

    const verificationToken = generateResetToken();

    const user = await ctx.prisma.user.create({
      data: {
        firstName,
        lastName,
        email,
        verificationToken,
        password: hashPassword(password),
        supportToken: uuid()
      }
    });

    await newRegistrationEmail(user, verificationToken);

    const token: Token = jwt.sign({ id: user.id, role: user.role }, process.env.JWT_SECRET, {
      expiresIn: '1y'
    });

    return { token, user };
  })

Prisma information

Schema has simple user model:

enum Role {
  MEMBER
  ADMIN
}

model User {
  id                String       @default(cuid()) @id
  createdAt         DateTime     @default(now())
  updatedAt         DateTime     @updatedAt
  // 
  status            Status       @default(ACTIVE)
  email             String       @unique
  emailVerified     Boolean      @default(false)
  emailUpdates      Boolean      @default(true)
  // 
  firstName         String?
  lastName          String?
  password          String?
  passwordSet       Boolean      @default(false)
  // 
  verificationToken String?      @unique
  // 
  role              Role         @default(MEMBER)
  memberships       Membership[]
  // 
  stripeCustomerId  String?
  supportToken      String?
}

First noticed the error when trying to check if a user with the given email was already in our db:

const duplicateEmail = await ctx.prisma.user.findMany({ where: { email } });

The "hang" happened whether the resolver threw an error or returned successfully.

Environment & setup

Lambda is set to use nodejs12.x in serverless.yml.

"@prisma/client": "^2.0.0-beta.2" in package.json.

@pantharshit00
Copy link
Contributor

Closing as a duplicate of #1754

Please read: #2121

Basically, you will need to set callBackWaitForEmptyEventLoop option to false because event loop can't be empty if we have a db connection from a previous lambda execution.

Export the server like so in your code:

exports.server = (event, context, cb) => {
  // Set to false to send the response right away when the callback executes, instead of waiting for the Node.js event loop to be empty.
  context.callbackWaitsForEmptyEventLoop = false;

  return lambda.graphqlHandler(event, context, cb);
};

@jonthewayne
Copy link
Author

I can confirm your fix worked! Thank you! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants