Skip to content
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

feat: introduce middleware based sequence for REST #5366

Merged
merged 9 commits into from
Aug 4, 2020

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented May 8, 2020

This PR adds a middleware based sequence and wrap existing actions as middleware.

Checklist

👉 Read and sign the CLA (Contributor License Agreement) 👈

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

👉 Check out how to submit a PR 👈

@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch 2 times, most recently from 927c328 to c48f1fd Compare May 8, 2020 06:01
Copy link
Contributor

@deepakrkris deepakrkris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondfeng looks like we are on a roll for adding middleware features :-)

const params: OperationArgs = await ctx.get(
RestBindings.Operation.PARAMS,
);
return this.invokeMethod(route, params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is next not called here ? for this to be an independent middleware function, it should have no knowledge that it is the last in the middleware chain. The invocation part should handle that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the middleware logic's decision not to call next as invokeMethod returns the result.

Copy link
Contributor

@deepakrkris deepakrkris May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I see; so next() is just a customary function for the middleware, it does not really link with the next middleware function in the chain ?

Copy link
Contributor

@deepakrkris deepakrkris May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is why sequence.handle() is forced to invoke the middleware functions one by one ?

for (const opt of this.options) {
     // Invoke registered Express middleware
     debug(
       'Invoking middleware chain %s with groups %s',
       opt.chain,
       opt.orderedGroups,
     );
     const done = await this.invokeMiddleware(context, opt);
     if (done) break;
   } 

Copy link
Contributor Author

@raymondfeng raymondfeng May 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok I see; so next() is just a customary function for the middleware, it does not really link with the next middleware function in the chain ?

It does. But it's up the middleware logic to decide if the next one should be invoked. The invokeMethod is designed to invoke the controller method directly.

why sequence.handle() is forced to invoke the middleware functions one by one?

This is to allow multiple chains to be used one by one. If you need to cascade them, add the chain as a middleware or flatten the chains.

@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch 6 times, most recently from c57d182 to 37525e5 Compare May 11, 2020 06:20
bajtos
bajtos previously requested changes May 11, 2020
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A big -1 from me.

The current Sequence implementation is easy to understand (because it's based on await flow control) and extend. The new proposal is introducing a lot of hidden complexity and behavior that looks like auto-magic to me.

I am willing to consider withdrawing my rejection if there is detailed and clear explanation for:

  • what problems in the current Sequence design are you trying to solve by this pull request
  • why these problems cannot be solved within the current design
  • how can application developers modify the sequence in the new middleware style
  • what is the migration path for existing applications

If we want to go all-in on middleware, then it may be better move away from Sequence entirely and use pure middleware-based composition of request handling flow.

@raymondfeng
Copy link
Contributor Author

@bajtos I know you might reject the PR :-).

Yes, I would like to move to a middleware based pipeline by composition. The sequence can stay as the integration point to the application and it can facilitate the transition too. Here are a few thoughts that drive me to come up this PR:

  1. To avoid future issues similar as LB4 Cors is always enabled after full upgrade of loopback #5368. The framework needs to have some control over the sequence.

  2. I never see a need to override the sequence code so far beyond adding additional actions. The extensibility of the sequence has already partially solved by the introduction of InvokeMiddleware. At bare minimum, we can either get rid of the generated sequence class or should simply have it extend DefaultSequence.

  3. The dependency based ordering is inspired by https://hapipal.com/best-practices/handling-plugin-dependencies.

  4. I intentionally didn't remove action providers. Instead, I created middleware provider to leverage existing action implementations. This is an effort to keep it backward-compatible.

  5. Having a few hard-coded actions beyond other middleware is an inconsistency and it makes it hard to manage all steps in a consistent way.

  6. The PR meant to propose another option and it can co-exist with the current sequence style - which can be removed later if our users are happy with the new one.

@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch from 37525e5 to 9463ac2 Compare May 13, 2020 05:30
@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch 2 times, most recently from eb5563e to 4aa9c3f Compare May 26, 2020 03:37
@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch 2 times, most recently from 06ccea9 to d3303ac Compare May 27, 2020 05:28
@raymondfeng
Copy link
Contributor Author

Here is another reason to unify all actions as middleware - #5562 (comment)

export class MiddlewareSequence implements SequenceHandler {
static defaultOptions: InvokeMiddlewareOptions = {
chain: RestTags.REST_MIDDLEWARE_CHAIN,
orderedGroups: [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this named orderedGroups? Do sendResponse, cors, apiSpec etc., resolve to arrays of middleware? If not, the name is misleading. If yes, the fact has to be clearly documented.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After going through the codebase, it has become clear to me. A name like executionOrder or something like that might make it more obvious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We inherit orderedGroups from interceptors - middleware is a specialized interceptors that are invoked as part of the sequence.

asMiddleware({
group: 'parseParams',
upstreamGroups: 'findRoute',
chain: RestTags.REST_MIDDLEWARE_CHAIN,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseParams, findRoute and other strings are being repeated, I think we should be using constants.

'findRoute',

// authentication
'authentication',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to work, user has to create a Provider<Middleware> and set its group value to authentication; and many such authentication middleware can be added and anyone of them can cause the authentication to fail, right?

Copy link
Contributor Author

@raymondfeng raymondfeng Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A group can have 0-N middleware and its name does not have semantic meaning. It's only for ordering purpose.

@raymondfeng
Copy link
Contributor Author

@hacksparrow Thank you for the feedback. I believe that I have addressed your comments. PTAL.

@hacksparrow
Copy link
Contributor

What purpose does this serve:

orderedGroups: [
  RestMiddlewareGroups.SEND_RESPONSE,
  RestMiddlewareGroups.CORS,
  RestMiddlewareGroups.API_SPEC,
  RestMiddlewareGroups.MIDDLEWARE,
  RestMiddlewareGroups.FIND_ROUTE,
  RestMiddlewareGroups.AUTHENTICATION,
  RestMiddlewareGroups.PARSE_PARAMS,
  RestMiddlewareGroups.INVOKE_METHOD,
],

if we still have to do this?

group: RestMiddlewareGroups.INVOKE_METHOD,
upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS,

I can understand for middleware not in the ordered group, but for those in the group - shouldn't they "know" it? And what happens if there is a conflict:

group: RestMiddlewareGroups.INVOKE_METHOD,
upstreamGroups: RestMiddlewareGroups.SEND_RESPONSE

?

I think we should add tests for the expected behavior.

@raymondfeng
Copy link
Contributor Author

raymondfeng commented Jul 30, 2020

@hacksparrow The final order of middleware is settled using both the orderedGroup and relative upstreamGroups/downstreamGroups.

The orderedGroups defines the overall order so that not all middleware are required to spell out upstreamGroups/downstreamGroups. Both settings contribute partial orders. We combine all rules and calculate the final order using topological sort. If there is a conflict (leading to circular dependencies), an error will be thrown.

I'll try to add some tests.

@raymondfeng raymondfeng force-pushed the sequence-action-as-middleware branch from d2e60b2 to 951408a Compare July 30, 2020 20:38
Copy link
Contributor

@hacksparrow hacksparrow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dhmlau dhmlau added this to the Aug 2020 milestone Aug 4, 2020
Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great effort! I followed the docs and reviewed the updated auth modules, don't see obvious problems.

@raymondfeng raymondfeng merged commit fbf07b8 into master Aug 4, 2020
@raymondfeng raymondfeng deleted the sequence-action-as-middleware branch August 4, 2020 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants