My perspective on Fibers-free Meteor

Feel free to share any of the 99% use cases with a before and after buddy

Great article, @radekmie. Reading more about your thought process and the roadmap is always enlightening, and it’s reassuring for all of us to see you taking ownership of the vision for a Fiber-less future. Really really appreciated your openness to discussion on that first PR as you were trying to get it finalized and merged. Excited to see what’s next :slightly_smiling_face:

1 Like

Hi @truedon – this is just a tiny snippet, but I gave a small example in the comment here:

Paraphrasing the relevant part:

At the end of the day, we want to turn Fiber-based code like this:

// returns number (synchronously)
const count = myCollection.find({}).count(); 

…into asynchronous code like this:

// returns number (asynchronously)
const count = await myCollection.find({}).count();

Really the only difference between those two end states is the presence of await.

In the interim, as we transition, we may need to use asynchronous methods with different names (the *Async suffix) to allow both to exist side-by-side:

// returns number (asynchronously)
const count = await myCollection.find({}).countAsync();

Although this change is tiny at a line-by-line level, it does mean that a lot of server code will be impacted, since you can only use await within an async method. So, server code that isn’t already asynchronous must be updated to become asynchronous. For example, that might start with making Meteor methods asynchronous, then making methods called by Meteor methods asynchronous, and so on… it has a large ripple effect throughout the codebase.

4 Likes

Thanks for taking the time to lay it out like this. I’ve encountered the async issue already several times - wanting to use async on methods and it makes sense to put it on the mongodb calls also. Once I can use this with tracker it’ll be great.

Is it not possible to just wrap the function so internally it’s async and therefore you don’t need to change any old code and it’ll run async if available?

Hm. I can’t think of a simple way to do that, but I know there are many things I don’t know (in life and in programming), so I’d hesitate to say it can’t be done. I will just say, I’m not sure how. If you can can get some proof-of-concept version of that working, definitely please share!

Since native asynchronous logic is something where you either have to use promise syntax (.then() and friends) or the syntactic sugar of async / await, I don’t think there’s a clean, easy way to do this migration without using fibers or something functionally equivalent.

When migrating old code to the new async syntax, it can be easy or hard depending on deep the call stack is. When you’re making changes in outer-level code, it’s actually pretty easy. For example…

Old version:

Meteor.methods({
  updateUserName(name) {
    db.users.update(this.userId, { $set: { name } });
  }
});

New version:

Meteor.methods({
  async updateUserName(name) {
    await db.users.updateAsync(this.userId { $set: { name } });
  }
});

Fairly easy change, because you can make a Meteor method async without really hurting anything else. (The only implication is that the method becomes unblocking, as if you’d called this.unblock() within the method – worth paying attention to, but hopefully your client code is not too reliant on Meteor method sequencing anyway. I have a hunch many Meteor developers don’t know that Meteor methods are strictly sequenced and hopefully aren’t depending on it, but I don’t know if that’s true.)

But, what if you call helper methods? For example, maybe updateUserName calls a method like validateUserPermissions that fetches the current user document from Mongo, but validateUserPermissions is used in hundreds of places in your codebase. Then, unfortunately, it can be pretty tricky to update all consumers of validateUserPermissions to also use the await syntax. And, if any of those methods are synchronous, they also need to be made async, and their consumers need to be made async, and their consumers will need to be made async… and so on. That is usually where it gets painful, and it’s why migrating away from fibers will take some time and effort in larger codebases.

Afaik you can return a promise without declaring the function as async and you can do the then inside - this would mean old code would still work and it wouldn’t be a breaking change. Just adding async all over the pubs and uses of mongo does sound fairly small although if it would be avoidable it would be optimal surely

async function - JavaScript | MDN

For example:

async function foo() {
   await 1
}

…is equivalent to:

function foo() {
   return Promise.resolve(1).then(() => undefined)
}

Sure, you can return a promise without declaring the function as async. The problem is just that consumers that were relying on foo to return something synchronously will now no longer work, because they’ll be getting a promise instead of a synchronous value.

A fun, dangerous example – here’s the old sync version:

function doesUserExist(userId) {
  return !!db.users.findOne(userId);
}

function someConsumerFunction() {
  if (doesUserExist(someUserId)) {
    // do something
  }
}

Let’s say we update doesUserExist to return a promise.

// returns Promise<boolean>
function doesUserExist(userId) {
  return db.users.findOneAsync(userId).then(maybeUser => !!maybeUser);
}

Then, consider our consumer function. Does it still work? Sadly, no, because if (doesUserExist(someUserId)) is now broken. Both Promise<true> and Promise<false> are truthy and will pass that conditional check.

To get the proper behavior, we have to rewrite the consumer too so that it properly resolves the promise to get the final boolean value. We can use the nice async/await syntax:

async function someConsumerFunction() {
  if (await doesUserExist(someUserId)) {
    // do something
  }
}

Or, we can use raw promise syntax:

function someConsumerFunction() {
  doesUserExist(someUserId).then(() => { /* do something */ });
}

…but either way, we do have to update the consumer. Sadly, there isn’t a practical difference between using the raw promise syntax vs async/await; it’s just syntactic sugar for the same thing.

1 Like

I get you man, it will be a big change when this comes out for sure

That was actually fibers :joy:

3 Likes

:joy: Ok I see, I think I get it better now. Need to see it in the wild really

Thanks for the informative post @menewman !

There are two options available when changing a public sync method into an async method, and both come with downsides.

The first option is to simply start returning a Promise<T> instead of T.

This has some advantages such as not needing to introduce new methods, monkeypatching methods still works with the old names etc but the big downside is that it will almost unnoticeably break all consumers of the method as in the example seen above.
Even more dangerous are methods that return Promise<void> where the caller doesn’t even check the return type. If everyone had been using Typescript, this would still be doable since the compiler would catch the change in signature, but that will never happen.

The second option is to introduce new versions of all framework methods that are directly or indirectly asynchronous

This is the path chosen by Meteor. It leads to having Collection.findOne next to Collection.findOneAsync and then as a next step, issue runtime (and compile-time for Typescripters) warnings about usages of the fibers-backed old deprecated sync method. After that step is complete and all internal and external packages have been updated to not invoke the sync methods you can disable fibers and ensure that the sync versions will crash and burn if invoked (e.g. code in seldom executed branches).

As a step far into the future it would be nice to clean up the code and reintroduce the old names without the Async suffix but still being async but that would make any old code people find on the Internet dangerously incompatible so I don’t even think it’s a good idea. I can live with XXXAsync.

In addition, one important enabler of this transition is the introduction of top-level awaits. Today you can have top level objects that internally carry out async operations during construction, e.g. const MyCollection = new Collection("XXX") which performs db operations in the constructor.

With top-level await this can instead be written something like
const MyCollection = await Colllection.createInstance("XXX") as async constructors are not supported in JS

The alternative would be to have the async code executed lazily whenever the first async method is invoked, e.g.
const found = await MyCollection.findOneAsync({}) // all init code is now executed before findOne can be run and init errors like a missing db connection will not happen until you actually invoke an async method

Top-level await is possible to implement server-side in Meteor because Meteor has its own custom module compilation&runtime system called “reify” and @zodern will be working on introducing this feature into reify soon.

1 Like

Thank you @menewman for getting involved here! There’s just one await missing in the last example (the one with countAsync). And yes, this “ripple effect” on the codebase is significant, especially if your code is not properly structured. (I have a blog post on that as well, if you’re interested: “On Multisynchronicity”; Fibers are also mentioned there…)

As @rjdavid pointed out, that’s exactly what Fibers were for. However, we may want to work on some codemods for automating this process, at least partially (e.g, adding all of the Async suffixes and making functions calling them async).

Also, thank you @permb for describing the process once again!

Ah, good catch. Fixed.

I think that it would be a smoother transition to invert the proposal, making everything return a promise and having an old legacy API exposed such as fetchSync and countSync. Therefore we can eliminate an entire step from the global migration to promises :slight_smile:

And we can save developer time by only sprinkling awaits. Isn’t that what meteor is all about, saving time?

And lastly, at no point in the migration will projects have gross temporary *Async that nobody wants anyways and will just be deleted in the end.

2 Likes

That can be done for a small project and not for huge projects wherein migration path can only be done in phases/stages and not in one update

3 Likes

In addition to what @rjdavid said above, I think the most challenging part of doing it this way would be managing dependencies – suddenly, all Meteor packages and libraries would break and have to be updated, which could be challenging for those who are using Atmosphere packages that are not all necessarily well-maintained anymore. Granted, the lack of maintenance is a long-term problem for any dependency anyway. But this would be a painful breaking change since you’d be at the mercy of the maintainers to update every dependency before your app would work again.

2 Likes

Oh yeah, that would be devastating…ha…ha… Ok well here is a refinement that builds on the idea that we should not have 2 migrations and not clutter the API with gross *Async and does not destroy meteors entire reputation in the process:

During the transition you could add a second set of your Mongo collections underneath your OLD collections, so you can have the old sync API and the new async API in use at the same time:

// Create Mongo collections
const Posts1 = new Mongo.Collection('Posts'); // sync api
const Posts2 = new Mongo.CollectionAsync('Posts'); // async api

// Query data, no "fetchAsync"!
Posts1.find().fetch() // old sync api
await Posts2.find().fetch() // new async api

The advantage to this is, that when the developer has finished their migration and they no longer need Collection (because they deleted them all), they are done. Done, done; No second migration. Seems easier to me.

I just went back and started to refactor some fiber code to promises and…I love fibers. I don’t want to see them go. fibers is meteor

I don’t think it’ll be as severe – these migrations will have more than a few months in between.

That’s exactly what we wanted to do and decided not to. Go read the text :stuck_out_tongue:

1 Like

meme