Meteor userId when running async jobs

I’ve got a doozy for y’all, about running jobs on a Meteor server using MeteorJobs.

We use Meteor.userId all over the place in our code, even in “low-level” methods such as augmenting the Collection.find method (where we might hard-wire certain query parameters based on Meteor.userId).

I’m adding a scheduling component to our app which will use MeteorJobs to periodically and asynchronously run functions. My understanding is that there is no “logged in user” when code is executing inside a job. The job is being run in a different context? environment? as the normal server code one writes for Meteor.

It’s easy enough to pass a userId in the payload of each of these jobs, so we do have an entry point to specify who is “logged in”. The problem is that if any code anywhere down the stack from any function we call in our job uses Meteor.userId, it won’t work. And to fix this, we’d have to go into every function that uses Meteor.userId, and add a userId argument and conditionally use that argument instead of Meteor.userId.

We tried overwriting the Meteor.userId function at the beginning of each job to just return the passed in userId, but the problem is that the jobs aren’t all run with their own instance of the Meteor object. So overwriting it in one job takes effect in another job where we wanted a separate userId.

Any ideas for how to set some global variable “per job”, i.e. that applies to each job individually?

Try this snippet:

DDP._CurrentInvocation.withValue(new DDPCommon.MethodInvocation({
  userId: '...',
}), () => {
  // here Meteor.userId() returns the value you want
});
6 Likes

That looks good! But where are DDP and DDPCommon coming from?

import DDP from 'ddp'; gives me DDP._CurrentInvocation as undefined so the snippet errors.

(And I’m also not sure about DDPCommon.)

EDIT: I googled around and found import { DDP } from 'meteor/ddp-client'; and import { DDPCommon } from 'meteor/ddp-common'; which appear to have the fields the snippet uses. Let’s see how they do, functionality-wise!

EDIT2: It appears to work, thank you so much!

2 Likes

I have been wondering the same thing myself, so thanks for asking and @apendua for answering!

However, in many cases it may not be the best idea to rely so much on the magic DDP “context”. In our app, we try to have a strict separation of application logic and the meteor entrypoints. So only “endpoints” such as publishes and methods may directly access Meteor stuff, and they pass on the userId and whatever else to the functions that actually do the work as parameters. You get a lot more clarity and peace of mind that way I have found. So in our jobs, we add the userId as payload and call the business logic using that as a parameter in all cases.

I agree it’s better not to rely to much on Meteor context. However, it’s usually impossible to ensure 3rd party packages don’t do that, e.g. collection hooks, so if you’re using any of those, ensuring the right context is usually the only reasonable choice.

1 Like

@harrison I also recommend looking at Meteor.EnvironmentVariable(), which gives much more flexibility and allows to put more than just userId into the context.

It is still better to do such context overrides within the scope of said 3rd party package instead of setting it globally on the DDP context which is far more intertwined with the rest of the stack.

For instance, the collection hooks packge does indeed offer alternative ways to setting user id on a server-only call that lacks a connection.

@serkandurusoy Connection is exactly the example I was thinking about :slight_smile:

Can you explain a bit more why do you think it’s “better”? Actually, if I set userId for one package but not for the other I end up in a situation of inconsistency. If I set it “globally”, I can be sure that every piece of business logic receives the right userId.

Connection is exactly the example I was thinking about

Yes and you are force telling your application that there’s a connection/user even when there’s not.

Actually, if I set userId for one package but not for the other I end up in a situation of inconsistency. If I set it “globally”, I can be sure that every piece of business logic receives the right userId.

This is only by consequence of and a solution to the problem actually created by the “not better” design.

What I mean by better is that the established best practices in software design around server methods (instance/connection/lifecycle handlers, domain models, business logic controllers, utility functions, data access etc) suggest keeping these methods as reliant as possible to their direct inputs/arguments.

There of course are patterns that either sidestep this because it is either more convenient for a specific part of the software or it makes more sense from another point of view like broader or cross-cutting concerns, testing etc. Patterns like singleton and dependency injection come to play at this point.

Environment is also another special piece of this design puzzle and again, best practices suggest keeping a single entry point in your app for such environment and directly passing in that entry point as a reference to your methods.

Now Meteor’s server context is also one of these patterns where the initial connection is grabbed, sanitized, formatted and passed on to the chain of execution. If you follow Meteor’s source code, you’ll see that even this globally available context is passed on to high level methods (publications, methods, etc) as instance methods, even though they still can be obtained from the global namespace. And then, officially endorsed packages like validated-methods take a step even further to convert those to function arguments because arguments make up a contract that you can validate against and they also serve as self documentation.

Now, when you pass these arguments explicitly (instead of assuming/shoehorning them to a global namespace) your program execution and testing will become more predictable, testable and maintainable. More debuggable even more so without having to attach arbitrary debugging watchers to seemingly unrelated parts of your app.

Now, coming back to the 3rd party packages example, a good package should provide you means to override the context by way of arguments or supplementary configuration because of all I’ve said above. Collection hooks does that by way of a configuration option where you can use it either globally (more towards your preference) or per instance/connection lifecycle.

By using more instance specific overrides on each distinct part of the app, rather than providing a global override, you won’t need to be aware of the global set up of your app and can isolate development, testing, debugging and maintenance to that single part of your app and it is easy for anyone reading your code to immediately see the outward bounds of that part of the app.

All this being said, best practices are not necessarily quantative things; if you and your team are comfortable with one way of doing things, that’s probably the most effective way of doing things for you for this time, until you yourself feel something else is fit to replace that. Perhaps this discussion can lead to that, or not. In either case, this topic should serve other readers as a succint reference to see alternative views to a given task :slight_smile:

2 Likes

I have created this helper function in my code:


// @ts-ignore
import { DDPCommon } from 'meteor/ddp-common';
import { DDP } from 'meteor/ddp';

/**
 * Call a function with a given userId. This makes `Meteor.userId()` return the given userId.
 * It also makes `Meteor.user()` return the user object for the given userId.
 *
 * @param userId
 * @param f a sync or async function to be called with the userId
 */
export function callWithUserId<T>(userId: string, f: () => T): T {
    // @ts-ignore
    return DDP._CurrentInvocation.withValue(
        new DDPCommon.MethodInvocation({
            userId,
        }),
        f
    );
}