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

Register process.on('exit') without leaking an handler #48058

Closed
mcollina opened this issue May 18, 2023 · 8 comments
Closed

Register process.on('exit') without leaking an handler #48058

mcollina opened this issue May 18, 2023 · 8 comments
Labels
feature request Issues that request new features to be added to Node.js. stale

Comments

@mcollina
Copy link
Member

What is the problem this feature will solve?

In some cases, modules might want to set up some clean-up scripts for clearing some resources gracefully before everything is shut down. However, they do not want to install a global 'exit' handler because then they would not know when to remove it and they do not want to add the burden to their user to call a close() function.

What is the feature you are proposing to solve the problem?

A few years back I wrote on-exit-leak-free to do just that. Using it is straightfoward.

'use strict'

const { register, unregister } = require('on-exit-leak-free')
const assert = require('assert')

function setup () {
  // This object can be safely garbage collected,
  // and the resulting shutdown function will not be called.
  // There are no leaks.
  const obj = { foo: 'bar' }
  register(obj, shutdown)
  // use registerBeforeExit(obj, shutdown) to execute the function only
  // on beforeExit
  // call unregister(obj) to remove
}

let shutdownCalled = false

// Please make sure that the function passed to register()
// does not create a closure around unnecessary objects.
function shutdown (obj, eventName) {
  console.log(eventName) // beforeExit
  shutdownCalled = true
  assert.strictEqual(obj.foo, 'bar')
}

setup()

process.on('exit', function () {
  assert.strictEqual(shutdownCalled, true)
})

Moreover in mcollina/on-exit-leak-free#31 (comment), @jimmywarting is proposing an even better syntax:

new FinalizationRegistry(fn, {
  runOnExit: true
})

What alternatives have you considered?

No response

@mcollina mcollina added the feature request Issues that request new features to be added to Node.js. label May 18, 2023
@jimmywarting
Copy link

The problem i had when using on-exit-leak-free was that the cleanup job only happened when the process did exit.
And If a object have already been collected before exit then it would not call the shutdown function.

I did truly want to know when a reference was garbage collected durning a long living process that never shutdown so that i could go ahead and delete/unlink a temporary Blob backed up by the filesystem when there where no longer any javascript variable that hold on to that variable any longer. So i wanted to construct my own FinalizationRegistry and remove those file when the references was GC'ed.

That was not something that on-exit-leak-free could handle for me. But i also wanted to delete those files when the process did exited. And that was what on-exit-leak-free was truly good at.

But what would have been even better is if the things i registered in FinalizationRegistry could also execute the callback function for each registered objected when the process do exit. so it callback on all finalizer registered with runOnExit: true first before exiting.
Then i could have the option to delete temporary files both when process exit and when they are garbage collected.

hence my proposal of:

new FinalizationRegistry(fn, {
  runOnExit: true
})
import { createTemporaryBlob, createTemporaryFile } from 'fetch-blob/from.js'

const response = await fetch('https://httpbin.org/image/png')

// pipes the data to OS- temp folder, with a unic/random name, and return a disk- based blob
let blob = await createTemporaryBlob(response.body, { type: 'image/png' })

// Sometime later
// loosing references to blob would then delete the file from disk
// with `runOnExit: true` It would also clean things up when process exit 
blob = undefined 

the createTemporaryBlob would kind of look like:

const registry = new FinalizationRegistry(unlink)
const tempDir = await mkdtemp(realpathSync(tmpdir()) + sep)
  
/**
 * Creates a temporary blob backed by the filesystem.
 * NOTE: requires node.js v14 or higher to use FinalizationRegistry
 *
 * @param {*} data Same as fs.writeFile data
 * @param {BlobPropertyBag & {signal?: AbortSignal}} options
 * @param {AbortSignal} [signal] in case you wish to cancel the write operation
 * @returns {Promise<Blob>}
 */
async function createTemporaryBlob (data, {signal, type} = {}) {
  const destination = join(tempDir, crypto.randomUUID())
  await writeFile(destination, data, { signal })
  const blob = await openAsBlob(destination, { type })
  registry.register(blob, destination)
  return blob
}

@jimmywarting
Copy link

jimmywarting commented May 18, 2023

A good use case for having this runOnExit would be for eg Response.formData() when parsing large multipart/form-data to write all files to a temporary location on the disk, and once you no longer have a references to this FormData or the files within. then it would remove them once it's done with them.

Even for response.blob() it would also be useful

@tniessen
Copy link
Member

Given that FinalizationRegistry is a standardized feature (that should be avoided at all cost), I am not convinced that we should add options or behavior that are specific to Node.js.

@jimmywarting Relying on FinalizationRegistry to clean up resources such as files is an anti-pattern and should be avoided. There is no guarantee that any particular cleanup callback will be called. In other words, if your service really never stops running, there is a chance that it will accumulate temporary files that are not deleted properly. runOnExit does not magically resolve this anti-pattern.

@jimmywarting
Copy link

jimmywarting commented May 19, 2023

FinalizationRegistry is a standardized feature (that should be avoided at all cost)

Why should FinalizationRegistry be avoided at all cost?
We did not get it without a reason if we can't use it for something good.

Relying on FinalizationRegistry to clean up resources such as files is an anti-pattern and should be avoided

By who? Why is it a "anti-pattern"?
The perception of an anti-pattern can vary among individuals based on their perspectives, experiences, and the specific context of the problem at hand. What one person may consider an anti-pattern, another person may see as a valid or even preferred solution.

Different developers or teams may have different opinions on what constitutes good or bad practices in software development. What is considered an anti-pattern in one situation may be an appropriate solution in another, depending on factors such as project requirements, constraints, trade-offs, and the specific technologies or frameworks being used.

There are many things that are considered as a "anti-pattern" like deffered promises, but ppl use it anyway.
I think extension-less path in require and imports is a anti pattern. but a large community don't like to think so. it's just a preferences of taste. but i see it as something useful for remote http-resolver and less stat'ing of what file you meant to include. ESM also requires you to be strict about the path


I could tell you that browser dose this magic all the time.
When you fetch something using a blob that is rather quite large (10MB+)

const url = 'https://freetestdata.com/wp-content/uploads/2022/02/Free_Test_Data_10MB_MOV.mov'
fetch(url).then(res => res.blob())

Then browser will store this in a "blob-storage" on the disk.
The blob's are not allocated in memory cuz you don't need to read the raw data in your application.

They are saved in ~/Library/Application\ Support/Google/Chrome/Default/blob_storage/<uuid> on my Mac on the disk
and when you reload the page or GC'ed then they are deleted as well.

They are magically resolved for developers.

(if you would have tried to get it as .arrayBuffer() then it would be allocated in the memory instead cuz you need to be able to read it synchronously and handle the data in some form)

if your service really never stops running

I haven't build any service that is long lived. I have built the node-fetch lib that gives the user the choice to get resources as a Blob any of those 45M downloads / weel could technically have a server that's long lived.

And the fetch api dose not provide any mean of cleaning this file up after itself. And it would be bad if we tried to allocate really large constructed Blobs in memory. and it would be bad if we only cleaned them up only when the process exit. it's a good practise to also clean them up when they are no longer needed.

Developers that passes this blob around to other libraries in order to parse it and process it like a zip library wouldn't know when it would be safe to delete the blob from the disk and calling a close function.

And we don't want to add the burden to their user to call a close() function either.
we created this temporary blob in a tmp directory so we should be responsible for cleaning it up after ourself when the developers don't need it anymore.


i can understand your view point on not doing specialized NodeJS behavior on standardized web api's.
but both @mcollina and i seem to agree upon that it's a fundamental missing feature of both Node.js and the web to not have something such as runOnExit

@tniessen
Copy link
Member

FinalizationRegistry is a standardized feature (that should be avoided at all cost)

Why should FinalizationRegistry be avoided at all cost?
We did not get it without a reason if we can't use it for something good.

Relying on FinalizationRegistry to clean up resources such as files is an anti-pattern and should be avoided

By who? Why is it a "anti-pattern"?
The perception of an anti-pattern can vary among individuals based on their perspectives, experiences, and the specific context of the problem at hand. What one person may consider an anti-pattern, another person may see as a valid or even preferred solution.

@jimmywarting If you insist on 👎'ing my comment and don't want to take my word for it, how about that of the folks who invented FinalizationRegistry? Let me quote a few statements by the authors of said API from proposal-weakrefs:

Warning - Avoid Where Possible

It's also important to avoid relying on any specific behaviors not guaranteed by the specification. When, how, and whether garbage collection occurs is down to the implementation of any given JavaScript engine.

Developers shouldn't rely on cleanup callbacks for essential program logic. Cleanup callbacks may be useful for reducing memory usage across the course of a program, but are unlikely to be useful otherwise.

A conforming JavaScript implementation, even one that does garbage collection, is not required to call cleanup callbacks.

Important logic should not be placed in the code path of a finalizer.

the W3C TAG Design Principles recommend against creating APIs that expose garbage collection. It's best if WeakRef objects and FinalizationRegistry objects are used as a way to avoid excess memory usage, or as a backstop against certain bugs, rather than as a normal way to clean up external resources or observe what's allocated.

Finalizers are tricky business and it is best to avoid them. They can be invoked at unexpected times, or not at all

The proposed specification allows conforming implementations to skip calling finalization callbacks for any reason or no reason.

Note, it's not a good idea to close files automatically through a finalizer, as this technique is unreliable and may lead to resource exhaustion.

That last sentence explicitly says that your idea is considered an anti-pattern by the very authors of FinalizationRegistry.

@aduh95
Copy link
Contributor

aduh95 commented May 19, 2023

+1 on not touching the FinalizationRegistry constructor which is defined by the ECMAScript spec as accepting exactly 1 argument, and is implemented by V8 not Node.js anyway. Any changes on this would need to be directed at TC39.

We could expose a NodeJSFinalizationRegistry sub-class which could accept custom options, but we would need a strong use-case for such API, and a volunteer to champion its implementation.

Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions github-actions bot added the stale label Nov 16, 2023
Copy link
Contributor

There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.

For more information on how the project manages feature requests, please consult the feature request management document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request Issues that request new features to be added to Node.js. stale
Projects
None yet
Development

No branches or pull requests

4 participants