-
-
Notifications
You must be signed in to change notification settings - Fork 753
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
Use AsyncLocalStorage to create a session #2074
Comments
Chimin‘ in... |
Related: #1881 |
Just throwing this in here as a potential way to handle this pre v5. I typed this out in response to another issue asking about setting up NewRelic and passing around that transaction, but then deleted it from there because it was more relevant here. The current way hooks are handled, they are not in the same "execution context" for this to work. But we could create a mixin, or extend classes to make this work I believe. The following code is not tested and just an idea. const { AsyncLocalStorage } = require('async_hooks');
const session = new AsyncLocalStorage();
app.mixins.push((service, path) => {
const oldFind = service.find;
service.find = function (params) {
if (session.getStore()) {
return oldFind.call(this, { session, ...params });
}
return new Promise((resolve) => {
session.run(new Map(), () => {
// session.getStore().set('time', new Date().getTime());
return oldFind.call(this, { session, ...params ).then(resolve);
});
});
};
// do the same for the rest of the methods
}); |
We do this in our application as well though we have to set up the initial storage outside of feathers (hooked into the transports for REST and socket.io) to maintain the same context throughout a single request. We used the Would love to have something like this in feathers -- there are some things that really should have per-request visibility across calls without having to explicitly pass in |
I... don't particularly understand this async storage stuff. I manually added a "trace uuid" in a global hook / middleware, and then have been making sure on each internal service invocation to pass that trace ID along, so that I can correlate all the queries afterwards. It's a bit verbose, but I got it to work on the server. This definitely has a bunch of boilerplate that I'm afraid of getting wrong, so having something that handles this automatically would be very cool. (I didn't decide or yet what would be a single transaction from the client side.) |
I have been thinking about this for a few years now and have come up with some other vanilla feathers solutions as well. For example. // Extend classes or add mixin with a function that returns a service-like object that picks off
// params you want to pass along and automatically pass them
service.withSession = context => {
return {
find(params) {
return service.find({ session: context.params.session, ...params })
}
...other methods
}
}
// can be used like this in a hook
app.service("albums").withSession(context).find({ ... }) And another option is something like this const sessionHook = context => {
context.params.session = { ... }
context.sessionService = (serviceName) => {
find(params) {
return context.app.service(serviceName).find({ session: context.params.session, ...params })
}
...other methods
}
return context;
}
// and can be used in a hook like this
context.sessionService('albums').find({ ... }) |
A quick update on my latest iteration of this and also including a // app.hooks.js
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const { ContextLoader } = require('./loaders');
const initializeSession = async (context, next) => {
if (context.params.session) {
// console.log('session manually passed', context.path);
return next();
}
const currentSession = asyncLocalStorage.getStore();
if (currentSession) {
// console.log('continuing session', context.path);
context.params.session = currentSession;
return next();
}
return asyncLocalStorage.run(new Map(), async () => {
// console.log('starting session', context.path);
context.params.session = asyncLocalStorage.getStore();
return next();
});
};
const initializeLoader = (context, next) => {
if (context.params.loader) {
context.loader = context.params.loader;
return next();
}
const currentLoader = context.params.session.get('loader');
if (currentLoader) {
context.params.loader = currentLoader;
context.loader = currentLoader;
return next();
}
const loader = new ContextLoader(context);
context.params.session.set('loader', loader);
context.loader = loader;
return next();
};
module.exports = [initializeSession, initializeLoader]; This seems to be working well so far. I tried using a POJO instead of a // some hook later
context.params.session = { ...context.params.session, active: true };
// This is bad because we have reassigned (not mutated) params.session so it is
// a totally different object from asyncLocalStorage.getStore() I think a better solution is to use a // some hook later
context.params.session.set('active': true);
// All good...no reassignment and asyncLocalStorage.getStore() is updated. You may notice in the // I don't love this because its on `params`. It feels less important than app.service
const result = await context.app.service('users').get(1);
const result2 = await context.params.loader.service('users').load(1);
// `context.loader.service` seems more naturally compared with `context.app.service`
const result = await context.app.service('users').get(1);
const result2 = await context.loader.service('users').load(1); |
A solution could be to use Object.defineProperty and setting writable to false to prevent reassignment: |
This snippet is a basic example using
AsyncLocalStorage
to create asession
object that is available to any "nested" services. This could be useful in passing things like user, authentication, transactions, etc to these service calls nested N levels deep instead of having to "params drilling" (aka explicitly passing params from service to service).You can see a full working example here: https://github.com/DaddyWarbucks/test-feathers-cls
And you can read more about
AsyncLocalStorage
here: https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorageNote that there are performance penalties for using
AsynLocalStorage
. A quick Google search will turn up some benchmarks, although none are very detailed.It is also important to note that
AsyncLocalStorage
was released in Node v13 (with backports for later v12 versions). So this is a rather new feature. But, I also found https://github.com/kibertoad/asynchronous-local-storage which would be a good example for how to fallback tocls-hooked
if we wanted to support back to Node v8.The text was updated successfully, but these errors were encountered: