-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Mongo Package Async API #11605
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
Mongo Package Async API #11605
Conversation
The new proposed changes look pretty simple to adopt 😀 As a suggestion it might be nice to show the new async code example along side the current fibers based code example. // api/LinksCollectionAsync.js
// import { Mongo } from "meteor/mongo"; // current fibers
import { createAsyncCollection } from "meteor/mongo";
// export const LinksCollection = new Mongo.Collection("links"); // current fibers
export const LinksCollectionAsync = createAsyncCollection("links"); Any format that gives an easy side by side comparison makes it a lot easier to see the required format changes during a quick inspection 👍. We'd probably all be able to give a bit faster & better feedback that way! For example from your example above, inside Meteor.call('insertAsync' {
title: "Do the Tutorial",
url: "https://www.meteor.com/tutorials/react/creating-an-app"
});
await insertLink({
title: "Do the Tutorial",
url: "https://www.meteor.com/tutorials/react/creating-an-app"
}); From inspecting the code, it looks like this would be no problem, but it would help to see what is a random example vs a proposed change. |
I'm really not a fan of having |
For sure shorter is better but the problem here is that we want to keep the old methods available yet, maybe forever in the client. It would be impossible for the developers to know when they are using each type of collections and the code would be very hard to understand. And we can't just rely on type definitions to make sure people are not confused as not every project is going to use Typescript. In the future we could migrate back to shorter names after all the Fibers dependencies are removed. Another thing to discuss here, MongoDB Node.js driver already change many names and methods, like But again, open to ideas here. |
Yes, you definitely don't need this intermediate function.
Good feedback, thanks! |
I'm surprised that const old = X.find(query).fetch();
const new = await (await X.findAsync(query)).fetchAsync(); I wanted to check, what'd be needed to change it.
Doing all of that would mean that the entire API relies on a single Meteor.startup(async () => {
await Mongo.waitForConnection();
}); To make the transition easier, this could be a part of one of the official packages, or even a separate package. On a side note, I'd vote against |
I know you get the difference between a Meteor Collection and the usage of the driver directly but to be clear to all readers, in a regular usage of the driver usually there is a connect step first and a Meteor Collection does this internally that is why a second await is necessary. In my second try I was not creating a But again, I didn't go forward in this path in the second try and we could try it, maybe it could work but we need to be careful with side effects.
These constructors should not be used externally so we could change to anything later, personally I would avoid classes if I was rewriting this from scratch, as the idea here is to keep compatibility with the current version I tried to change as few code as possible.
My main point with this function was to be able to lazy initialize the collections properly in the top level so we could still create our Collection instances in the top level of files and export them, but they are definitely other options. As I said I did many iterations (you can see in my commits) and then in the end maybe we can indeed improve somethings because the flows are easier to visualize now 😉 . I'll double check your specific proposed changes later and reply with more details and if they are possible or not and what would be the consequences, thank you for your detailed feedback. This is exactly what I'm looking for exposing these code changes here, thank you 😄 . |
Great references and points in most of the comments. As a personal taste, I would try to stick with the naming convention proposed by the nodejs driver. I know insert is simpler than having to write insertOne or insertMany, but it's not such a big deal, and I think it would help newcomers to understand "easily" how to use the mongo driver functions in meteor. |
ae6095f
to
bc1837a
Compare
Hi @radekmie, I'm back to this code and analyzing your ideas. In this comment I'll focus on
We would need to connect to MongoDB in advance in order to create Cursors without requiring The code below is not going to work in the current version of Meteor as startup callbacks are called in parallel.
This is a problem because we can use If we change this code to await each hook it would be a solution: var callStartupHooks = Profile("Call Meteor.startup hooks", function () {
// run the user startup hooks. other calls to startup() during this can still
// add hooks to the end.
while (__meteor_bootstrap__.startupHooks.length) {
var hook = __meteor_bootstrap__.startupHooks.shift();
Profile.time(hook.stack || "(unknown)", () => Promise.await(hook()));
}
// Setting this to null tells Meteor.startup to call hooks immediately.
__meteor_bootstrap__.startupHooks = null;
}); The only problem left is to make sure mongo package is always the first to add the callback, the declaration order in Am I missing something or this change is really necessary for your approach to work? If I'm correct we could also create a different startup phase and run then one by one so we don't change the behavior in the old Something probably like |
Hi @hexsprite, in this comment I'm going to focus on Do you see any other alternatives after my comment here? One goal here is to keep existent apps working in the same way and provide an easy way to migrate collections gradually. I still don't see a better way than creating new methods to avoid confusion. |
Hi @radekmie, in this comment I'll focus on
I was treating these constructors as internal APIs (as they are) so using What are the benefits that you see in using The only one that I can think is to centralize the import in the class but do we want to keep this external API as a class? What is the point of using a class to use just a static method of it? The whole point of classes is to keep the state and using static methods is exactly the opposite, so I'm not sure if this is the best option here.
|
Update on I was able to use a sync find but using Promise.await (aka Fiber) in the top-level to initialize the connection during the code evaluation. Even using beforeStartupQueue was not enough as the collection is created in the top-level. Not sure yet if this trade-off is worth it. It would require top-level await support in order to remove Fibers completely but maybe this is going to be necessary anyway. Comments? I pushed the changes in the commit 9509c3a so we can see if all the tests are going to pass with this set up as well. Of course, if we decide to move this change forward we could potentially simplify other parts of this PR as well. |
Hi @filipenevola, thanks for the update!
Right, I forgot that
This would introduce the same problems down the road, as more and more code would rely on it.
I agree that it is an internal API but it's been there for a long time and we're able to make it backward-compatible. And yes, no one should use it, but as I checked, I've used
I'd say it goes like this:
If we could rely on standard constructors, that'd be ideal. First of all, due to the objective matter of the entire MongoDB API. If we'd say that everything but one call is a method, I see it as an inconsistency. But sure, factory functions are technically the same, but not bound to an object - I'm fine with that. However, class X { async constructor() {} }
// Uncaught SyntaxError: Class constructor may not be an async method And because of that, TypeScript won't allow it either. (There's also this StackOverflow answer, but I link it only as a more comprehensive writeup rather than anything else.)
I'd say that this is the main, not whole point of them. As we're in a (weirdly) typed language (I assume that this code will be in TypeScript sooner or later), making it a static method makes it possible for subclasses to extend. And again, I agree that composition is better than inheritance in general, but as the MongoDB API is already strongly object-oriented, I'd stick to it.
Oh, believe me - me neither...
I'd say it's a big step, as the entire MongoDB integration relies on a single |
Hi @radekmie, just to clarify a few things here:
Right now I'm starting the connection when mongo package is evaluated so you wouldn't need any call in your app code. If you want to use an external the internal API would still accept an external driver. The problem here is that I'm relying in a top-level Promise.await what will require top-level await support to migrate away from Fibers, but I think this is going to be necessary sooner or later. Another point, we are depending on one await for the connection, but we still need one await for each MongoDB operation. Something like this: import { Meteor } from "meteor/meteor";
import { LinksCollectionAsync } from "../imports/api/LinksCollectionAsync";
Meteor.startup(async () => {
if (await LinksCollectionAsync.find().countAsync()) {
console.log(`No need to insert the first link`);
console.log(`Links: `, await linksCursor.fetchAsync());
return;
}
await LinksCollectionAsync.insertAsync({
title: "Do the Tutorial",
url: "https://www.meteor.com/tutorials/react/creating-an-app",
createdAt: new Date(),
});
}); Two test groups are breaking after this change, I'm going to check why soon. We probably need to open a discussion about top-level await implementation in Meteor if we are going to proceed with this change. |
Ah, right, we've slightly misunderstood @filipenevola 😅
That's what I meant and that's great.
Yep, that's right. Plus, I'd add
Agree, but I think it's not that urgent if nobody raised it already (I think?). |
The errors we are getting here (MongoServerSelectionError connect ECONNREFUSED) are somehow related to this top level connection, but I was not able yet to understand why they are happening. I was also studying the top-level await specification to make sure our solution here is compatible with top-level await specification and it is compatible. The examples even mention this case, where you want to connect to a resource in a top-level code to make sure all the consumer modules are ready to use the resource. So in terms of specifications we are safe to proceed but it's important to remember that Meteor doesn't implement top-level await yet. The implementation here is using So I believe we can continue investing in this approach in order to avoid |
EDIT: OH, right... The client. |
b797f6d
to
5a10c24
Compare
Agree that it would be ideal to avoid Could there be a Meteor setting that you opt in to when starting up with |
Apologies if I'm misunderstanding this suggestion, but... To me, the benefit to having a suffixed version of Mongo methods is that it allows us to complete the migration over time by having both the Fiber and Fiber-free versions existing side-by-side in the same codebase. If this behavior were controlled by a global setting that can only be either on or off for the whole application at any given time, then we'd be forced to migrate from Fibers -> async/await in one fell swoop, which can be much more challenging for a large, existing codebase. As someone maintaining a large, existing Meteor codebase, I very much appreciate the effort to make a gradual migration possible! I agree that it does make for some awkward naming in the meantime, but it seems like the lesser evil -- and as Filipe wrote above, perhaps the names could be swapped in a later version after the migration is complete. |
Indeed, a gradual migration path is going to be important for my project as well. What I would do:
|
Ah, I hadn't considered that some might want to piecemeal migrate to their codebases. Still might be nice to have an option for those that want to migrate wholesale without having My concern is that |
Except its not just your code that will make these calls. Any meteor library you use will be expecting While an Async suffix isnt the prettiest, it is the best way to not break the ecosystem or force lots of work upon users or library maintainers all at once, but instead let it be done over a couple of meteor releases |
I would encourage everyone here to approach this huge change by assuming only one thing really: It is the end of line of meteor in its first incarnation and all your code along with all packages will need to be substantially modified. Every single call your application makes into meteor will need to be modified. And every single method in the call chain that has any of those calls needs to be changed too. Yes, that’s right. There will be no free lunches and no automatic upgrade to async/await instead of fiber-synchronous calls. It will just be a lot of work and headaches. You will save time by moving your code to Typescript and/or using some other tooling (e.g. SonarCloud) to detect where in your call chain you have now forgotten to convert the calling method to be async and add an await on invocations. The Meteor team can facilitate the move by introducing runtime warnings for all fiber-based non-async methods along with the new *Async methods so that you can prepare for the big switch and that will require coordination. |
I'm against changing to For our production system it would require a lot of changes as we're heavily relying on MongoDb transactions that we're accessing via raw collection. |
Together with @fredmaiaarantes we've decided that I'll continue @filipenevola's work here. As of right now, I want to set up a project to see where the current implementation is and then add tests for the new async API. I'm fully aware of the problems coming from using the async API on the client too (#11505 (comment)) and will put some thought into it as well. 🤞 |
A small update after a couple of hours. Open topics:
If possible, I'd love to hear your opinion on these, @filipenevola. Of course, everyone else is welcome to comment as well. What I've done so far:
To do in the first place:
You can find my code here: https://github.com/radekmie/meteor/tree/mongo-no-fiber-option |
@fredmaiaarantes, @filipenevola, @anyone-interested-here If possible, please do comment on the topics above. In the meantime (this week) I'll work on more tests. |
I think that having a
I haven't checked the code, so I can only make assumptions for now. Personally, I am happy with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing to start work on here is a guide article and changelog.
); | ||
} | ||
|
||
if (options && options.methods) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removal of these should be documented.
'MongoInternals.RemoteCollectionDriver then you need to use ' + | ||
'Promise.await() or await on it. Since it is async in recent ' + | ||
'versions of Meteor. ' + | ||
'Read more https://docs.meteor.com/changelog.html.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should point to a specific docs article. Even better an upgrade guide in Guide.
var observeHandle = cursor.observeChanges( | ||
{ | ||
added: function(id, fields) { | ||
{added: function (id, fields) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting.
useTransform: true | ||
}); | ||
} | ||
_.each([...CURSOR_METHODS, Symbol.iterator, Symbol.asyncIterator], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do without underscore?
The point is, it'll never do anything async; at least as long as the connection will already be there. I've already suggested that it can be done in #11605 (comment).
It will affect both client and server. It is another topic that introducing async functions on the client is harder than on the backend... @StorytellerCZ thanks for the input. Just keep in mind that it's not my branch - I haven't opened a PR yet. I'll do that later this week. |
I see some mentions of relying on top-level awaits to be able to keep the existing pattern of creating collection instances at top level, but top-level awaits itself comes with a requirement to move from commonjs to ecmascript modules. https://www.stefanjudis.com/today-i-learned/top-level-await-is-available-in-node-js-modules/ Without having done any work with ES modules I suspect that making that move will both break compatibility with older browsers and probably a lot of other breaking changes as well (hello reify). Can someone that knows more elaborate so we don’t just assume that it IS possible to move to ES modules and top-level await? |
I will reread everything here today and share my thoughts. Thanks for pushing it! |
We are planning to implement top-level await within our existing module system instead of switching the bundler's output to ES modules. Other bundlers have successfully done this, such as esbuild, so it is possible. |
I just messaged @filipenevola to see if he can clarify these questions @radekmie. |
Hi @radekmie sorry for the delay, my 2 cents in the open topics:
Yes, I've changed to that after our discussions here.
I agree. It should be restrictive on mixing the two approaches in both sides (in the ideal World) but the idea was to make room for people to migrate slowly to the new async methods instead of migrating everything in a single batch in a single collection, I think this could be beneficial as the main challenge here is to provide a nice way for people to migrate their code. But we are also allowing creating two collection instances for the same collection in MongoDB using different approaches so maybe we could prevent the mixed use case inside the same instance. It would be fine probably.
We don't NEED anything else to make sure the old methods are still working. This work is done. What we SHOULD is add new tests to validate Async execution paths, but as they are going to be 100% new I'm not too afraid as we can get errors and problems in the beta phase and then add these tests to cover specific cases and avoid regressions, we did this in the MongoDB 5 oplog changes and in the end, the result was great.
I explained in my commit message. TL;DR: it was testing a case that doesn't exist.
Also explained in the commit. TL;DR: adding an option to test only one package tests on test-in-console/run.sh I'm open to helping with other topics as well, feel free to send me an email ([email protected]) if I don't get back soon enough, I'm not tracking my GitHub notifications anymore. |
As I opened #12028 just now, this one can be closed. |
Why
As you are probably aware we are going to replace the internals of Meteor to avoid Fibers in async executions.
The first phase was to make Meteor up-to-date with Node.js 14 (LTS until April 2021) and now we are starting the second phase:
Make Fibers usage in Meteor server application runtime optional
.In order to allow developers to start writing code that is not going to change when we remove Fibers from internals we are already providing methods in the
mongo
package API returningPromises
so in the future when we replace all the usages of Fibers internally your app is already ready and no changes are necessary. If you continue to write your code as you do today everything will still work as expected but you are not going to be prepared for when we remove Fibers from internals.Meteor
mongo
package is by far the most affected package in terms of APIs that are going to be async when Fibers are removed as mongodb Node.js driver uses async calls to access MongoDB.It's important to understand that we are not moving away from Fibers because we want to, it's a necessary change because of the direction that the Javascript ecosystem is going and we need to follow the standards to take advantage of nice new futures that are going to come up in the next years in the Node.js ecosystem.
If you are completely new to this topic please read this discussion and if you have questions around Fibers and async please ask your questions in the discussion. This PR should be used to discuss changes to the
mongo
package API only.What
We are providing in this PR async APIs for the two main resources of
mongo
package:How
We are not breaking any previous standard usage of Collection and Cursor. Also this change is not required but it's recommended for new code that you are writing today. Your previous code should work fine with this change and that is why is so important that you provide feedback here.
We are creating alternative methods with the same name but appending
Async
to it, so instead of callingfind
when you are using an async collection you should callfindAsync
. The latter will return a promise.Also in order to create the collection properly we are not going to use directly a class because when Meteor creates the collection it already connects to the MongoDB server so this class instantiation would need to be converted to a
Promise
but a class constructor should return its instance so we are wrapping this creation in a function calledcreateAsyncCollection
. This is still up for discussion because as you can see in the code in the end this function is also not returning aPromise
but this was the result of multiple iterations so maybe we could iterate one more time and keep using the constructor but I personally would prefer to avoid the use of classes explicitly as it's more limited than regular functions.Internal Changes
We have to change some internals in order to achieve our goals but maybe you are using the internals so it's good to document here anyway:
await
or you can still usePromise.await
but remember thatPromise.await
is going to be removed also because it relies on a Fiber to work._driver
option you are going to get an error explaining the same as above.null
._suppressSameNameError
is true.ignoreInstanceReuse
that we are going to experiment during the betas. Maybe it's going to be removed if there isn't a compelling use case. Set it astrue
if you really know what you are doing. Default isundefined
.How to use
When the changes in this PR are tested, ready and published we will recommend that you start to use this API in new code. The process of removing Fiber from the core is going to take months so you don't need to migrate everything now but it's good to start writing new code using this new API.
Fibers don't exist in the client so you don't need to use the new API in the client but if you want it's also supported there. We support these async operations in the client as well because it's important that you can still write methods to run in the client and in the server.
Examples
I'm still going to update this description with more examples later:
Old Deprecations
We found some very old code during these changes and we have removed a few cases:
connection
option inside the options asmanager
, this is not supported anymore.Collaboration
This PR is not final, we still can discuss and make changes to it, your collaboration is important with feedback.
It's also very important that you test your project with these changes. We are still deciding which approach to recommend here but one quick way is to clone Meteor and run your project with your local copy of Meteor checked out in this branch.
We've tested this PR in a few cases but we are still going to do a lot of more testing ourselves as well.