-
Notifications
You must be signed in to change notification settings - Fork 92
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
Implement a faster REST router #282
Conversation
handler.methodName = sharedMethod.name; | ||
handler.path = route.fullPath; | ||
customRouter.registerPathAndHandlers({ verb: verb, fullPath: route.fullPath }, handler); | ||
} | ||
router[verb](route.path, handler); |
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 if
block (L417-421) is causing trouble :( All tests with npm test
passed on local, CI fails
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.
Moving L422 in else
block of L417 causes failures on local. We need to define a clear strategy to port custom router.
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.
I would expect that L422 is not invoked when the method was registered with the new fast router. I'll check.
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.
Here is a clue - I added some logging to RestRouter
.
$ mocha -g "strong-remoting-rest call of constructor method should allow arguments in the query"
strong-remoting-rest
call of constructor method
REGISTER all /testclass
HANDLE get /testclass/
1) should allow arguments in the query
Please add debug()
logs to both registerPathAndHandlers
and handle
so that users can easily get logs like that without any code changes. Take a look at how express.Router
logs debug messages and add similar logs to your new router.
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.
So the thing is, the handler is registered for all verbs on /testclass
. The request for get /testclass
does not recognise this handler.
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.
I could not find this comment on GH, so I am responding here.
the built-in router places all verb handler on the top of the handler stack, and keeps a flag to check if the path has an all handler, if yes, it > invokes the all handler first and runs other registered handlers> In our case, trie has a placeholder for all handler, and we can check, but we are only inovking single handler.
That makes perfect sense to me, now we just need to improve error logging so that the user understands what's happening under the hood.
- If there is an "all" handler already registered for a path, and users is attempting to add another handler, then we should do the same thing we do now when the second handler is registered for the same "path+verb"
- When registering an "all" handler, check if there are any existing verb-specific handlers for the same path. If there are, then report a warning for each of them, since they won't be invoked.
BTW are you sure the "all" verb handler is on the top? I would expect it's at the bottom, i.e. more specific handlers (GET, POST) are run before generic ones (ALL).
I'm thinking of changing it in a way that:> in all: handler(req,res,next) , next points to the second handler in the stack for the path and so on.. I will commit this approach in the next commit.> Or, should you have any better idea for this scenario -- after looking at recent commit -- please advise. Thank you :)
AFAIK, all strong-remoting handler functions either send back the response (and never call next), or fail with an error and call next(err). In both cases the flow control skips all subsequent handler functions.
This is why we keep only the first handler for each path+verb (instead of an array), and that's why I believe we don't need to chain handlers for "all" verb either. Am I missing something?
Miroslav
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.
BTW are you sure the "all" verb handler is on the top? I would expect it's at the bottom, i.e. more specific handlers (GET, POST) are run before generic ones (ALL).
Yes, I'm pretty sure about it from express API docs:
http://expressjs.com/en/4x/api.html#router.all
and it's also the matter of registering the router.all()
first to execute it first in middleware chain.
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.
As I understand docs, all
is treated the same way as other, regular verbs. If you put your route handler as the first one, it will work as you describe, but if you put it as the last one, it will work as I described it. So the task for you is to find how strong-remoting is dealing with this. IIRC there is a custom sort of routes, does it take verbs into account too?
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.
@bajtos : strong-remoting sorts routes in the following custom way: It first sorts by verb
and then sorts path-part by part.
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.
Oh, I see. And because "all" starts with the first letter in the alphabet, it will end up being registered as the first one.
To be honest, this looks like a bug to me, but I am not sure if it's a good idea to fix it now, when we are possibly introducing other breaking changes caused by the new router implementation.
Could you please leave a comment in the code, explaining why we are treating "all" verb handlers as the first ones and open a GH issue to track the task of changing this to the more sensible approach where "all" becomes a fall-back only? The code comment should link to this new issue too.
Thanks,
Miroslav
@gunjpan please review my comments in our earlier conversations (e.g. in the Spike/POC pull request) and add all unit-tests that I mentioned there. It's ok to add them as "it.skip" initially, I just want to make sure we don't forget about any of them. |
@gunjpan on more task from our earlier conversations: please add the test suite that will run the same code (tests) against both |
|
||
if (fastRouter.canRegister(route, handler)) { | ||
handler.methodName = sharedMethod.name; | ||
fastRouter[verb](route, handler); |
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.
I think adding extra properties on the handler
method makes it more difficult to understand what's going on, because the other parts of the code are depending on the fact that fn.methodName
is defined, which is not obvious.
That's why I proposed an API that is an extension of the original Express API:
// express:
router[verb](route, handler)
// proposed extension:
router[verb](route, handler, options)
OTOH, this is a minor detail. Please considered my comment, and if you still feel that handler.methodName
is better, then let's do it your way.
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.
If you decide to keep the name on the handler
function, then please choose a more descriptive property name. The problem with methodName
is that it's not clear whether it refers to an HTTP method (like GET
) or a shared/remote method (like Product.create
).
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.
@bajtos : I will set it to sharedMethod.stringName
thus it would be clear if it's a prototype method or a static method, and as you recommended, will use displayName
as:
handler.displayName = sharedMethod.stringName
Is that fine?
@bajtos, this is still very much a work in progress and there are still a few changes that need to be made that you had originally recommended for @gunjpan, but I would like you to please review before I continue to work on this. I would also like your opinion on any more test cases that should be added. |
@gunjpan @davidcheung, could you also please take a look. |
@@ -425,6 +433,11 @@ RestAdapter.prototype._registerMethodRouteHandlers = function(router, | |||
// Express 4.x only supports delete | |||
verb = 'delete'; | |||
} | |||
|
|||
// if (fastRouter.canRegister(route, handler)) { |
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.
I don't think it's reasonable to expect that fastRouter will support all different route flavours supported by express (e.g. regular expressions). That's why it's important to call canRegister
(or perhaps canHandle
is a better name?) here.
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.
Did you look at the code for canRegister
? It only registers with express if the handler function has more than 3 arguments or if there is a ':' in the path (in other words, any route with a parameter).
So I just want to be clear and say that you are not suggesting to use the function that exists but instead to make sure that the router can handle the route, otherwise pass it to the express router.
Note: this should be done with an if else
statement instead of just an if
.
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.
So I just want to be clear and say that you are not suggesting to use the function that exists but instead to make sure that the router can handle the route, otherwise pass it to the express router.
Correct.
Note: this should be done with an
if else
statement instead of just anif
.
👍
I am not sure what exactly to look out for during review. I did a quick check to see if there is anything obvious and left few comments above.
For me, it's most important to have an integration test suite to verify that the new fast router behaves exactly same as the default express router. I think the best approach is to create a parameterized suite that will be run against the new fastRouter and against the Express router. For example, something along these lines: describe('fast router', function() {
testRouter(FastRouter);
});
describe('express Router', function() {
testRouter(express.Router);
});
function testRouter(Router) {
var router, request;
before(setupHttpServer);
after(stopHttpServer);
// individual tests calling the provided `router` under the hood
it('...', function(done) {
router.get('/', function(req, res, next) {
// ...
});
request.get('/')
.expect(..., done);
});
function setupHttpServer(done) {
router = new Router();
// create http server handling requests via router
// create supertest instance invoking this server
}
} As for test cases, we should test all HTTP verbs, then different URL paths ( |
We should also verify that when you register a handler for a given verb+path, other endpoints will not trigger the handler. Verify the priority of paths with path parameters vs. paths with static names, think about |
} | ||
}; | ||
|
||
// For now: exclude paths with parameters like :id |
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.
^ remove this
I wanted you to look out for the things that you found. It's better to catch those mistakes early instead of having to do a big re-factor at the end. It's a much faster process to review as I move forward instead of one big repeating review/re-factor cycle at the end. |
Also, I keep getting the same failure for multiple tests on Node 0.10 and 0.12 TypeError: undefined is not a function
at Trie.find (lib/trie.js:9:2363)
at Function.RestRouter.handle (lib/rest-router.js:9:1870)
at router (lib/rest-router.js:9:688)
at corsHandler (lib/rest-adapter.js:9:13002)
at lib/rest-adapter.js:9:11460
at test/phase-handlers.test.js:20:30
at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:492:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:111:23)
at Socket.socketOnData (_http_server.js:343:22)
at readableAddChunk (_stream_readable.js:163:16)
at Socket.Readable.push (_stream_readable.js:126:10)
at TCP.onread (net.js:540:20) |
Few more things to test: values of path parameters should be url-decoded, i.e. |
That's weird, because Trie.prototype.add = function(route, handler) { @richardpringle can you reproduce the problem locally on your machine too? (I personally use and recommend nvm for quick & easy switching between node versions.) |
segment = parts[i]; | ||
if (!(segment in node) && !node.param) return false; | ||
if (node.param) { | ||
segment = Object.keys(node).find(function(key) { |
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.
array.prototype.find
is not an ES5 function
@gunjpan I am re-assigning this back to you. Please ping me when there patch is ready for another round of review. |
@bajtos : Thank you. I may need to spend some time to see what richard did, rebase and see which way to go. Will update you when done. Thanks. |
@gunjpan, let me know if you need any help! |
I am closing this patch for now, we can re-open it when the time to work on this feature comes again. |
Faster REST router implementation - iteration 1
Includes:
/api/[model]
&/api/[model]/[method]
Excludes:
:parameters
e.x./api/[model]/:id
handlerFn(err, req, res, next){..}
Connect to https://github.com/strongloop-internal/scrum-loopback/issues/730