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

Context State and Hookless hooking #139

Closed
ritch opened this issue Apr 13, 2017 · 13 comments
Closed

Context State and Hookless hooking #139

ritch opened this issue Apr 13, 2017 · 13 comments
Assignees
Labels
IoC/Context @loopback/context: Dependency Injection, Inversion of Control stale

Comments

@ritch
Copy link
Contributor

ritch commented Apr 13, 2017

@simonho @Raymond @miroslav

I have a proposal for hooks that re-uses our existing declare context bindings approach.

@bind('res.time')
function getTime(@inject('state.accepted') accepted, @inject('state.finished') finished)  {
  // pause execution until the context is in the `accepted`
  await accepted();
  const start = hrtime();
  // pause until the context is in the `finished` state
  await finished();
  return hrtime() - start;
}

/* states:
   - accepted - the request has been parsed
   - finished - the response has been written
*/

@bind('state.finished')
function finished(@inject('watch') watch) {
  while((await watch('state.history').contains('finished') === false) {
    // wait for finished state to be in the history
  }
}

// in core
function writeResponse(@inject('state') state) {
  state('finished'); // state.history.push('finished');
  // ...
}

The way this works is that you can ensure some state has already occurred, but not define a specific order of execution. Which gives you the benefits of koa without the middleware execution order dependency.

@ritch ritch changed the title States in Contexts Context State and Hookless hooking Apr 13, 2017
@ritch
Copy link
Contributor Author

ritch commented Apr 13, 2017

Here is an example using this approach to implement a logger

// app usage
import {log} from 'my-logger';

@bind('log.immediately')
const immediate = false;

class MyApplication {
    public handle(@inject('writer') writer, @inject('invoke') invoke, @inject('log') log) {
      const result = await invoke();
      await writer(result);
      if (shouldLog()) log();
    }
    public components = [log];
}

// extension
export const log = [writeLog, getTime, logFormat, getStatus, immediate];

@bind('log')
export function writeLog(@inject('log.format') fmt) {
    console.log(fmt);
}

@bind('res.time')
async function getTime(@inject('state.accepted') accepted, @inject('state.finished') finished, @inject('log.immediate') immediate)  {
    if (immediate) return;
    
    await accepted();
    const start = hrtime();
    await finished();
    return hrtime() - start;
}

@bind('log.format')
function logFormat(@inject('res.time') time, @inject('req.url')) {
    return `${url} - ${status} - ${time}`;
}

@bind('log.immediately')
const immediate = true;

@bind('res.status')
async function getStatus(@inject('state.finished') finished, @inject('result.status') status)  {
    await finished();
    return status;
}

@bajtos
Copy link
Member

bajtos commented Apr 19, 2017

I had to re-read the proposal several times to understand it. I think it may work, although I am concerned whether an average LoopBack user will be able to understand this mechanism.

Let's consider an example where the user makes an error and accidentally calls the log function before the response is written, and what's even worse, they await the result, because that's seemingly consistent with the initial code inside handle() method.

class MyApplication {
    public handle(@inject('writer') writer, @inject('invoke') invoke, @inject('log') log) {
      const result = await invoke();
      await log();
      await writer(result);
    }
    public components = [log];
}

IIUC, this will create a deadlock: log method is awaiting state.finished, which won't be reached until log is finished and the response can be written. How are we going to detect this situation programatically in our runtime, so that we can tell the user what mistake they made and how to fix it?

@raymondfeng
Copy link
Contributor

My understanding of @ritch's proposal is to use @inject to express fine-grained events/dependencies, which will be further used to determine the execution order. The proposal also use bind to associate triggers to such events (fulfillment of dependencies). It's a nice idea but might not be very practical for the following reasons:

  1. Fully describing the fine-grained dependencies is hard.
  2. Not all extension developers will explicitly express all dependencies via @inject. A hook can just simply use @inject('context').
  3. The complexity that @bajtos already described.

@bajtos The deadlock is caused by the fact that handle serialize the execution of components. If we want to implement @ritch's proposal, a state machine/rules engine is probably better off.

In general, I like the idea to have each component express some requirements so that LoopBack can use them as partial order to sort or trigger such actions. But we need to keep the metadata as simple as possible. IMO, phase is a just degenerated case to @ritch's proposal.

@bajtos
Copy link
Member

bajtos commented Apr 20, 2017

In general, I like the idea to have each component express some requirements so that LoopBack can use them as partial order to sort or trigger such actions.

I like that idea too. I think your sentence is nicely describing the high-level goal 🎯

@jonathan-casarrubias
Copy link

jonathan-casarrubias commented Apr 22, 2017

@ritch I really like your idea, I think is just clean and extensible... That way we wouldn't entirely depend on you supporting your own provided hooks/states, so if I'm correctly understanding we would be able to also create our own states and bind these custom states, inject these in our controller methods and execute the flow as we desire?

If that is the case, that would be pretty cool, I did read it a couple of times (or more) also to understand and still not entirely sure I understood well, but this looks promising.

So yes it might be a little complicated at the beginning, but with good documentation and a good example I think it might be straight forward.

My two cents.
Jon

@raymondfeng
Copy link
Contributor

raymondfeng commented Apr 22, 2017

@ritch If I remove all the syntax sugars, your proposal becomes much simpler to understand:

  1. Bindings:
{
  'log': writeLog, // function
  'res.time': getTime, // function
  'log.format': logFormat, //function
  'res.status': getStatus, // function
  'log.immediately': immediate // variable?
}
  1. Dependencies
{
  'log': ['log.format']
  'res.time': ['state.accepted', 'state.finished', 'log.immediately']
  'log.format': ['res.time', 'req.url']
  'log.immediately': []
  'res.status': ['state.finished', 'result.status']
}

There are a few issues in your original example:

  1. TS decorators cannot be applied to regular functions/variable declarations
  2. res.time has interesting async dependencies. It is first triggered by state.accepted, then it has to await state.finished to resume.

@raymondfeng
Copy link
Contributor

Furthermore, relations between participants of the request processing can be expressed in one of the following forms:

  1. Dependency graph
    • direct: A depends on B
    • indirect: For example, A consumes x, which is produced by B.
  2. State machine (S0 --> B --> S1 --> A --> S2)
  3. Rules engine (R0 --> B, R1 --> A)
  4. Flow engine (B then A)

@bajtos
Copy link
Member

bajtos commented Jul 13, 2017

I think this proposal has been superseded by https://github.com/strongloop/loopback-next/wiki/Sequence.

@ritch feel free to reopen if you disagree.

@bajtos bajtos closed this as completed Jul 13, 2017
@raymondfeng
Copy link
Contributor

@bajtos I think we should keep this issue open. IMO, Sequence is the primitive to support chain of actions to handle req/res. This issue explores opportunities to allow declarative approach to build up a sequence based on certain metadata. For example, I would like to implement two alternative sequences:

  1. Use produce/consume dependencies (decorators or json/yaml) to create a sequence
  2. Use phases to create sequence

These strategies can be bound to the app ctx to replace the default imperative implementation.

@raymondfeng raymondfeng reopened this Jul 13, 2017
@bajtos
Copy link
Member

bajtos commented Jul 13, 2017

@raymondfeng sounds good, thank you for reopening the issue then.

@dhmlau dhmlau added Core-GA and removed p1 labels Aug 23, 2017
@raymondfeng
Copy link
Contributor

See #598

@dhmlau dhmlau removed the non-DP3 label Aug 23, 2018
@dhmlau dhmlau removed the post-GA label Nov 2, 2018
@stale
Copy link

stale bot commented Oct 28, 2019

This issue has been marked stale because it has not seen activity within six months. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository. This issue will be closed within 30 days of being stale.

@stale stale bot added the stale label Oct 28, 2019
@stale
Copy link

stale bot commented Nov 27, 2019

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.

@stale stale bot closed this as completed Nov 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
IoC/Context @loopback/context: Dependency Injection, Inversion of Control stale
Projects
None yet
Development

No branches or pull requests

6 participants