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

Introduce Async Hooks into the SDK #7691

Closed
9 tasks done
AbhiPrasad opened this issue Mar 31, 2023 · 5 comments
Closed
9 tasks done

Introduce Async Hooks into the SDK #7691

AbhiPrasad opened this issue Mar 31, 2023 · 5 comments

Comments

@AbhiPrasad
Copy link
Member

AbhiPrasad commented Mar 31, 2023

ref: #3660

Let's work toward adding async hooks into the SDK. What we probably want to use is AsyncLocalStorage, which was made stable in v16.4.0, but added in v13.10.0, v12.17.0

Important to note that for Node 8, 10, 12 we should still use domains, and then for Node 14 and above we can use AsyncLocalStorage.

Requirements:

  1. There must be only one hub management strategy active at a time (no exceptions)
  2. No breaking changes

Additional Details:

We can get this done with the following steps.

// @sentry/node
import { setAsyncContextStrategy } from '@sentry/core';

interface AsyncContextStrategy {
  getCurrentHub(): Hub | undefined;
  runWithAsyncContext<T>(callback: (hub: Hub) => T): T;
}

init() {
  setHubStrategy(strategy)
}
  1. Update getCurrentHub to reference some global strategy for grabbing the current hub. When you initialize the SDK in Node 8-12, it uses a domain strategy. For Node 14+ it'll use async local storage strategy.
  2. Instead of using domains directly in our code, we should define a runWithHub method that creates a new domain/asynclocalstorage/whatever for that code. Then we can go in and replace domain usage with runWithHub instead.

The global strategy can just just be registered onto the global object. This means there can only be one global strategy at a time.

We have to add this concept of a strategy to @sentry/core, so it can also be used by non-node SDKs (think vercel edge, cloudflare workers etc.).

@AbhiPrasad
Copy link
Member Author

AbhiPrasad commented Apr 11, 2023

Did a little load testing with our changes with artillery (good tech). This was with production express, that had been warmed up, single route:

const express = require("express");
const app = express();
const Sentry = require("@sentry/node");

Sentry.init({
  dsn: "https://5382661235ae4a2da41a88dd2a4cc141@o19635.ingest.sentry.io/5338414",
  tracesSampleRate: 1.0,
  integrations: [
    // enable HTTP calls tracing
    new Sentry.Integrations.Http({ tracing: true }),
    // enable Express.js middleware tracing
    new Sentry.Integrations.Express({
      // to trace all requests to the default router
      app,
      // alternatively, you can specify the routes you want to trace:
      // router: someRouter,
    }),
  ],
});

// RequestHandler creates a separate execution context using domains, so that every
// transaction/span/breadcrumb is attached to its own Hub instance
app.use(Sentry.Handlers.requestHandler());
// TracingHandler creates a trace for every incoming request
app.use(Sentry.Handlers.tracingHandler());

app.get("/hello/:name", function (req, res) {
  res.send("hello " + req.params.name);
});

app.use(Sentry.Handlers.errorHandler());

app.listen(3000);
console.log("Listening on port 3000");

Domains: 2500 req/s

"http.response_time": {
  "min": 0,
  "max": 106,
  "count": 100000,
  "p50": 7,
  "median": 7,
  "p75": 8.9,
  "p90": 10.9,
  "p95": 13.1,
  "p99": 19.1,
  "p999": 49.9
},

Async Local Storage: 3300 req/s

"http.response_time": {
  "min": 0,
  "max": 85,
  "count": 100000,
  "p50": 5,
  "median": 5,
  "p75": 7,
  "p90": 8.9,
  "p95": 10.1,
  "p99": 13.1,
  "p999": 21.1
},

This represents a 30% improvement in throughput 🚀

Looking at the Sentry data:
image

first peak is domains - second is async local storage

@timfish
Copy link
Collaborator

timfish commented Apr 11, 2023

Thanks for doing the load testing Abhi!

Out of interest, what throughput do you get if you don't set a strategy (ie. it just does the default which calls the callback directly)?

@AbhiPrasad
Copy link
Member Author

No strategy enabled is 3600 rps

"http.response_time": {
  "min": 0,
  "max": 91,
  "count": 100000,
  "p50": 5,
  "median": 5,
  "p75": 6,
  "p90": 7.9,
  "p95": 8.9,
  "p99": 12.1,
  "p999": 16
},

Around a 8% increase when using ALS, and 60% increase in response time when using domain in the p99 case (oooof)

@AbhiPrasad
Copy link
Member Author

Considering we have just the docs left - going to close this issue. Will go back and close the related issues once the release has happened!

@joshperry
Copy link

I'm working an update from 6.16 to latest for our app and just want to say I appreciate you guys shipping this. Domains were mentioned in the comments of our code and I was a bit shocked and ended up learning a ton about Sentry today. Also thank you for having this code opensource, very helpful and looks awesome!

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

No branches or pull requests

3 participants