Best practice for DgraphClientStub, DgraphClient lifetime

I’ve not been able to find any guidance on a best practice for a request lifetime with respect to DgraphClientStub, DgraphClient within the context of a server process. Given a node server with a module that implements request handlers with something like express or koa what are the best practices for the lifetime of instances of these classes.

Here is what I’m currently doing which will run for the lifetime of the node server instance. This works great but I’m wondering if it is necessary to create the DgraphClientStub on each request and further I have the same question for the DgraphClient. I’m assuming the overhead for creating these instances on each request has some overhead which could be avoided in a high performance environment.

Lastly I have seen some community client apps that create both on startup and keep them live for the duration of the app - although not my case it made me question this same thing on the server side.

import { Context } from 'koa';
import Router from '@koa/router';
import { DgraphClientStub, DgraphClient } from 'dgraph-js';
import grpc from 'grpc';

// Dgraph GraphQL router for handling GraphQL operations
export const dgqlRouter = new Router();

const endpoints = (process.env.DGRAPH_ENDPOINTS || 'localhost:9080').split(',');

function createClientStubs() {
  return endpoints.map((e) => new DgraphClientStub(e, grpc.credentials.createInsecure()));
}

function releaseStubs(stubs: DgraphClientStub[]) {
  for (const stub of stubs) {
    stub.close();
  }
}

/**
 * Passthrough GraphQL route for running
 * pure Dgraph GraphQL against the db
 */
dgqlRouter.post('/dgql', async (ctx: Context) => {
  // TODO: Add queryId functionality
  const { query, vars } = ctx.request.body;
  if (!query) {
    ctx.status = 400;
    return;
  }

  // NOTE: I'm not sure if this should be created for each request or once
  //       in this module - how would one clean it up on shutdown?
  // See: https://github.com/dgraph-io/dgraph-js#cleanup-resources
  const stubs = createClientStubs();
  try {
    const client = new DgraphClient(...stubs);
    if (vars) {
      const res = await client.newTxn().queryWithVars(query, vars);
      ctx.body = { error: false, result: res.getJson() };
    } else {
      const res = await client.newTxn().query(query);
      ctx.body = { error: false, result: res.getJson() };
    }
  } catch (e) {
    ctx.body = { error: true, message: e.toString() };
  } finally {
    releaseStubs(stubs);
  }
});

Hi @clintwood,
The standard practice to use DgraphClient is inspired by the GRPC spec itself. In short, there is no need for creating new connections every time you try to connect to a GRPC server. There are a few reasons behind it:

  1. This approach is error-prone. If connections are not closed properly, it could lead to memory issues as more and more connections are getting created in the application. The eventual state would be OOM’s.
  2. In a highly available and concurrent system, this could lead to too many connections over the GRPC server which could lead to increased network latency and maybe downtime if maximum connection limits reached.

Just to set up a context, GRPC’s leverages HTTP2 under the hood. Besides introducing binary framing and compression in HTTP2, one of the key enhancement was multiplexing (none before Http1.1, limited in Http1.1). This allows concurrent systems to connect GRPC server over one single connection. Of course, you can manage your connections through call options like idleTimeout or keepAliveWithoutCalls. Available call options are listed here.

I request you to please go through this and this to understand GRPC better. You can find official documentation with core concepts here.

These were my assumptions too, hence the question - thank you for the reply!!

Great @clintwood ! Keep your questions coming. I will request internally to add more documentation in terms of managing clients and their lifecycle for more clarity.

Apart from this, One of the best practices which is suggested by Dgraph is to connect to multiple Alphas to distribute the workload evenly. You can find more about that here.

Yes - I see that - I think my example actually uses multiple stubs which is what is used when creating the client! Love the scalability although I’ve not yet pushed the limits at all - will get there in time!

I do have a few more questions (off this topic) which I’ll get to in another topic post if I can’t find a clear answer on it!

Thanks again for the reply!