hapijs / hapi

The Simple, Secure Framework Developers Trust

Home Page:https://hapi.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Resource cleanup

matthieusieben opened this issue · comments

Support plan

  • is this issue currently blocking your project? (yes/no): no
  • is this issue affecting a production system? (yes/no): no

Context

  • node version: v16.15.1
  • module version: 20.2.2
  • environment (e.g. node, browser, native): node
  • used with (e.g. hapi application, another framework, standalone, ...):custom plugin
  • any other relevant information:

How can we help?

I am writing a plugin. I want that plugin to expose a method that can create a particular resource on a given request (e.g. a db connection). This is easily achieved (e.g. by decorating the toolkit).

I would like that:

  1. Multiple calls to request the same resource return the same instance. This is easy.
  2. The resource gets cleaned up (once) at the end of the request. This I am not sure how to achieve.

There is a finish event but this one is not triggered for requests that do not return a payload (GET/HEAD). There is an onPostResponse lifecycle hook but this requires to either define the listener for every route, or at the server level.

Is there a way to dynamically add a listener to be run during the onPostResponse life cycle ?
Is there a "flag" that can be used to detect that onPostResponse should not be called anymore ?

Perhaps there is something you can do using:

That way the method defined by your plugin decorating the request object (or whatever you're decorating) could store its data in the request.plugins.yourPlugin sandbox indexing it with the request id. That way on the onPostResponse you can check whether something exists for the current request.info.id and cleaning it up how you see fit then clear it and it shouldn't be called another time.

Thank you for your answer. That is indeed how I tried to implement this.

My question really is "is there a built-in way to add onPostResponse listeners on a per-request basis (instead of globally + logic to determine if the listener should actually run) ? And if there is, is there a built-in way to detect that the onPostResponse event for that particular request was already emitted (ie without having to add an onPostResponse event to set a flag myself) ?"

My initial solution was to rely on the raw res close event. But that one can be triggered way earlier than onPostResponse when the connection is closed by the client.

The reason I am asking this is because the pattern that consists of adding a global listener that should not run most of the time seems less elegant than dynamically adding a listener that will only be executed if needed.

Hapi & friends are built in such a clean way that usually allows this kind of dynamic pattern but I don't find it for this particular use case.

Hmm, this seems like an oversight in the hapi API. It makes sense to enable a plugin to dynamically call some cleanup after a request is submitted.

Maybe hapi just needs a new event that triggers right after the response has been submitted, but before the data is written? Eg. a request.events 'responded' event with a response parameter.

This would allow any logic at any point in the life-cycle to add some cleanup logic – and if it needs to wait until the data has been transmitted, it can add another listener to response.events 'finish'.

I expect this event could be hooked right after writing the headers here:

internals.writeHead(response);

Note that this event is not guaranteed to be called, so cleanup logic would need to also hook the 'disconnect' event.

Maybe hapi just needs a new event that triggers right after the response has been submitted, but before the data is written? Eg. a request.events 'responded' event with a response parameter.

In the instance where the resource to cleanup is a db connection, and the response is a Stream (async iterable) that uses the db connection to generate the stream data, we would really require that hook to be triggered "once no more data can be sent in the http response and after all Hapi lifecycle hooks have been executed".

My current use case for this is to be able to get a db connection from a pool and "bind" it to a request. Lazily getting that db connection (once per request) during any lifecycle hook (e.g. during auth if auth requires access to the db). And then use that db connection to generate the response payload using a stream (to reduce memory pressure), and to allow performing "post request" async actions (logging) before actually releasing the connection to the pool.

My current understanding of Hapi requires me to use wiered hack to perform this.

I agree that in general we could use a cleaner interface to cleanup per-request resources.

But if this case is all about generating a response then you have another option! You can use request.generateResponse({ marshal, prepare, close }). This is used e.g. by inert to open a file resource, and ensure it ends-up closed in the case of a disconnect, etc. There's no further you can get into the request lifecycle! I think we would want to keep this in mind when designing any new APIs, too.

Here's an example in inert: https://github.com/hapijs/inert/blob/68a0a36022241d3cd136fa3aa5a9331593612000/lib/file.js#L91
And here are some docs: https://hapi.dev/api/?v=20.2.2#-requestgenerateresponsesource-options

I thought about generateResponse() for this use case, but it doesn't apply. It can only be used when your plugin controls the response, which this case doesn't (but it might be possible to re-design it for it).

For plain cleanup cases a request.events 'closed' event is probably most appropriate (which also waits firing until the response has been transmitted).

Though, from a plugin POV, it is not possible to fully know when the user logic is completely done with the request, unless you track the lifetime using something like the FinalizationRegistry. Eg. the user might keep it around to do some post response logging.

FYI, Hapi used to have a mechanism to track post response logic, but it was removed with v17.

I am currently using FinalizationRegistry to perform my plugin cleanups.

tail would be perfect for my use case. What would it take to bring it back ?