hapijs / hapi

The Simple, Secure Framework Developers Trust
https://hapi.dev
Other
14.63k stars 1.34k forks source link

Resource cleanup #4358

Open matthieusieben opened 2 years ago

matthieusieben commented 2 years ago

Support plan

Context

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 ?

Nargonath commented 2 years ago

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.

matthieusieben commented 2 years ago

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.

matthieusieben commented 2 years ago

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.

kanongil commented 2 years ago

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: https://github.com/hapijs/hapi/blob/b8ba0adc7c3255995cb56a9a740c4f9750b80e6b/lib/transmit.js#L104

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

matthieusieben commented 2 years ago

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".

matthieusieben commented 2 years ago

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.

devinivy commented 2 years ago

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

kanongil commented 2 years ago

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).

kanongil commented 2 years ago

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.

matthieusieben commented 2 years ago

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 ?