-
-
Notifications
You must be signed in to change notification settings - Fork 752
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
Asynchronous initialization of Feathers services #509
Comments
I've been thinking about that a couple of times as well. The problem was that it will be a breaking change but we can probably get in with #258. |
@daffl #258 is huge. I've got my share of Hapi before, and it is totally different beast under the hood, the work on unification looks really tough. And Passport+Hapi initiative was abandoned years ago, so it is a fresh start. As for |
Well, after suggesting that service creation should return a function the other day, so as to signal to user that it's a dynamic thing, the next thought was to return a Promise. So no matter the order of service creation when you use .then() you can be sure the service is already initialized. However, you do the Promise scheme it's gonna be better than how it is at present :) A very minor nitpick, but has big consequence for DX imo.... Circular references between services are common place. |
@idibidiart I'm not sure if I understood correctly, does the code above fully cover the scenario you've described? Probably service |
@bisubus if I understand your proposal correctly (and I am knew to the Feathers and Express way of things), my concern is that A) it is a breaking change since the app.service function doesn't return a promise and B) slow startup time as you have to wait on all services to launch as opposed to starting the app and then yielding/awaiting until promise for a given service resolves before proceeding with service invocations.... if that makes any sense. |
@idibidiart In implementation above it was done for This proposal in its current form doesn't slow or break anything. It just provides a promise of initialization, it can be either chained to make sure that everything is ready or ignored. In my case I chained |
i see what you mean! Thanks for the clarification. I think it makes sense to do it the way you've described including returning a promise from setup. What remains vague to me is where you say that app.service returns an instance of the service. I've gotten undefined when invoking it while the service is not ready. You're suggesting to wait on the setup promise to resolve prior to invoking app.service? |
@idibidiart Do you have a real-world example where I didn't mean that it will wait for |
Yes I have an example I can describe and point you to. I did not mean to wait for setup promise in app.service but to 'await' it from an async function (or use generator equivalent) before invoking the service methods. Won't require app.service to return a promise. The example is where graphql service is configured before the services it depends on so when invoking app.service when graphql service is launched those services don't exist yet. |
the thing is the dependency graph for services may have cycles (service A depends on service B and service B depends on service A) so we can't guarantee a linear dependency order, which is why I was thinking of waiting on setup promise resolving before proceeding with app.service invocation (again, if that makes any sense!) |
@idibidiart By 'graphql service is launched' you mean From my understanding of how Feathers service registration works, services will become available in the order in which they are defined, so the solution is to have Yes, the thing you're describing is usually addressed with DI container. I like DI a lot, but here it looks like overkill to me. With Node modules, the proper order of service definition can be relatively easily maintained. But you've touched unon interesting subject. There may be scenarios where it may be beneficial to have a promise of some particular service (returned from service |
The problem was in this project: https://github.com/advanwinkel/feathers-apollo-SQLite Note how graphql is configured before the services that it uses in the resolvers. Moving app.service calls to the dynamic parts of the resolver fixes it. Correcting the configuration order fixes it too. |
DI is too much agreed. |
So if you look at src/services/resolvers.js here is what I mean: `
// let Posts = await app.service('posts')
So elsewhere we'd have Resolvers().then(...) Not sure what setup() does. In this case, the graphql "service" (aka graphqlExpress) does not have the Feathers service interface. And what I meant by invocation-time circular dependency is like when Post resolver uses Users service and User resolver uses the Posts service. It wouldn't be a problem in this case because we're awaiting on Users service to resolve before calling it from Post resolver and awaiting on Posts service to resolve before calling it from User resolver. So I guess it's not a circular dependency between the Posts and Users service but a reciprocal relationship between the resolvers that depend on those services. Sorry to confuse. Updated comments accordingly I hope some of this makes some sense generally speaking despite my not fully grasping the way things work in Express and Feathers. |
@idibidiart Yes, I've understood you correctly then. This is solved by maintaining proper users-posts-graphql In the example you've shown graphql is not really Feathers The problem with promises of services that haven't been defined yet is that if Graphql depends on User, and User depends on Graphql (possibly unintentionally, and there may be more tiers), we will have classic circular dependency. This will result in pending promise with no error (while DI would throw CD error). Looks like antipattern to me. This is why I think that this is a job for DI, not for promises alone. It would be nice to have DI container as an optional module, not necessarily a core part. If it could leverage existing initialization promises to perform async DI (don't remember if I've seen a thing like this somewhere else, but it is doable), this would be even better. |
I believe I managed to confuse you in part. In the Feathers-GraphQL approach, Users service will never depend on GraphQL, as GraphQL is the only API end point and is the main entry. What I was trying to solve for in the example code I shared is to ignore the configuration order because that is the anti-pattern: you cannot depend on order being linear, i.e. that their may be circular dependencies (DI is obvious solution there) but not in this case. But that is the reason I did not wish to resort to a fix that depends on configuration order. By having a service return a promise I'm not solving the circular dependency case but I am solving another problem: wrong configuration order can lead to service being undefined and that presents a mystery to new comers who are not experienced enough with the way things work i Express/Feathers, so having to wait for the promise then proceeding with the remainder of the Resolver function (non-blocking thanks to async stack) we can assure that the Users or whatever service graphQL depends on is not undefined and that requests that may come to GraphQL before those services are defined will probably cause the server to throw an error because Resolvers.then(...) will not have setup the GraphQL service by then. But at least I can then easily tell what the issue is w/o having to understand the thing about configuration order. It was a thought experiment. The learning outcome from it and this discussion (which forced me to think more about it) was that the simplest and easiest way is what @daffl had proposed and what i discovered while trying to fix it, which is to call the app.service(...) from within the resolvers, so that we have dynamic state. If services did have a circular invocation-time dependency, i.e. or more clearly stated: if they had 'reciprocal relation' in terms of service A calling service B and vice versa, that is not a problem. If the services were actually dependent on each other in terms of their instantiation then DI would be needed. |
@idibidiart I see your point. Thanks for explaining the example with Graphql, it adds some details. Circular dependency was the most obvious pitfall in this situation, that's why I mentioned it. Also, there will be pending promise if there is no User or Post at all. If a thing like this should be done on framework level, these concerns cannot be ignored, because unexpected pending promise will be a PITA to debug. |
The telling sign that the promise is pending is that the graphql service won't be there when we try to send it a request before the Feathers services it depends on are resolved (due to exported async Resolvers function returning a promise and being a dependency of GraphQL service, so we won't create the GraphQL service till the Resolvers promise resolves (unfortunate mixing of semantics here) We can find out which Feathers service is pending resolution with simple logging following each 'await' statement. But I agree that is not a very formal way of dealing with the issue. |
@idibidiart The situation with pending promises can be always handled with Bluebird |
Yeah like I said the telling sign in the example I shared is that the GraphQL service will not have been instantiated so Express/Feathers will return a 404. Easy to conclude that a promise fora service it depends on didn't resolve. But I'm not high on that approach. A declarative, event-driven state machine could be setup to handle the most complex startup/boot-up sequence. If there is interest I can propose an imperative version based on ES6 generators, which I believe node supports. I've done this sort of complex startup orchestration before in the UI. |
@idibidiart If you have something particular in mind, it would be nice to see it as a repo branch. There is possibly something that we could come up with. |
Just to let you know that I had a similar problem and fixed it with I think a more simple approach, I overrode the
I also simply call the function given as parameter but await for it, so that you can have async operations in your callback. This means that if the lifecycle of your app is sequential you I hope this helps. |
@claustres Yes, this works as temporary solution but unfortunately, breaks API, |
Yes for sure it does not solve the backward compatibility issue... |
@claustres @bisubus Would there be something against it to make it a separate method |
Yes it could be. I was also thinking of a way to make async functions chaining. Strictly speaking you can await in an expression so we could write something like this
The question is how to implement it ? Maybe by analysing the return the |
@claustres Yes, that's the problem. |
I have not added this in v3 (maybe in 3.1) but this should be solvable with a fairly simple plugin: module.exports = function() {
const app = this;
const oldConfigure = app.configure;
app.ready = Promise.resolve();
app.configure = function(fn) {
app.ready = app.ready.then(() => Promise.resolve({
callback.call(this, this);
return this;
}));
return this;
}
} This would allow to return a promise (or use an |
I think I get your point although there is some typo in your code that might mess me up, it is similar to what I was thinking about with my |
Handle it using hooks seems great although I would add a new hook type to handle this use case (eg setup hooks). Indeed using regular hooks raises confusion IMHO because they seem to work like any other hook but actually they don't... |
Hook initiative looks interesting. Seems like there's no support of promises for middlewares. Is a middleware supposed to be wrapped with dummy service to enable promises? |
This functionality will not affect how middleware runs. It is possible to register middleware in a setup: [ doAsyncStuff, context => context.app.use(errorHandler) ] @claustres I was debating that as well, something looking like: app.service('myservice').hooks({
before: {},
after: {},
error: {},
setup: {
before(context) {
await createDatabaseTableIfNotExists();
return context;
}
}
});
app.hooks({
before: {},
after: {},
error: {},
setup: {
before: createDatabaseTableIfNotExists(),
after: startMonitoring(),
error: restartInTenMinutes()
}
}); It adds some inconsistencies when it comes to retrieving and registering hooks and doing codemods but it is more explicit than having some internal exceptions you have to know about. |
@daffl I have a question about this new way of handling the setup: what should go in the setup method and what should go in the hooks? |
I think the same as with service methods. Anything that the service needs to work by itself should be implemented in |
Just to be sure: Would this officially support creating services at runtime anytime after start of the app (in my case user triggered) ? |
This already works for websockets and is more of an Express than a Feathers limitation. You could create an updated |
I'm think is missing returning a promise in app.configure(), that was the original cause for this issue. There is no way of aborting in case of fail during configuration (eg. database not available) |
What's the status on this issue? |
This will be part of v5 using the new hook system (#932). |
This is real issue that I've recently had, but I guess this concerns the framework in whole. I think that it is natural for Feathers services to be asynchronously initialized. They can pick their configuration from asynchronous sources, or it can be something else.
In most cases synchronous application initialization is presumed, but I've encountered design problems when started using Sequelize.
While Mongoose does some good job on accumulating promises inside for initialization actions (connection, index creation), so queries are automatically chained to initial promises, Sequelize does nothing like that, e.g. when
sync()
is called.I would expect that SQL database is ready at the moment when web server has been started, but when the application is defined in synchronous manner, it will not. And this is what happens with typical Feathers setup. In some cases async initialization may take several ms, in other cases it may take seconds, while the server will pollute logs with errors that should have been never happen normally.
Another problem is error handling. If there are uncaught errors during async service initialization, the reasonable move would be to halt app initialization and catch errors in a single place.
Here's a small example that explains the idea and is based on the structure proposed by Feathers generator template.
app.js:
services/index.js:
services/foo-async-service.js:
The places of interest here are
initPromise
andPromise.all(...)
. It would be convenient to just returninitPromise
from service function, like it is done in hooks. AndPromise.all(...)
could probably be lazily executed withapp
objecttoPromise()
method orpromise
getter property. So it could bein service function, and
in entry point.
I suppose that the pattern I've used for async initialization is already simple and clean, but it would be great if the framework would take this job.
My suggestion is to place this feature under consideration.
I would appreciate any thoughts on the subject.
The text was updated successfully, but these errors were encountered: