08 - FinalizationRegistry : savoir quand le GC passe
Ce que tu vas apprendre
- Ce que fait FinalizationRegistry et son API (register, unregister, heldValue)
- Les cas d'usage concrets : nettoyage de file handles, connexions DB, WebSocket
- Pourquoi tu ne dois pas t'y fier pour du code critique
- La comparaison avec les destructeurs C++ et le trait Drop de Rust
Prerequisites
Avoir lu l'article 07 sur WeakRef et WeakMap.
En C++ ou en Rust, quand un objet sort du scope, tu sais exactement quand son destructeur s'exécuté. En JavaScript, le garbage collector passe quand il veut. Pendant des annees, on n'avait aucun moyen de savoir qu'un objet venait d'etre ramasse. Puis FinalizationRegistry est arrive.
Le concept
FinalizationRegistry te permet d'enregistrer un callback qui sera appele quand un objet est collecte par le GC. Tu ne contrôles pas quand ca arrive, mais au moins tu es prevenu.
typescriptconst registry = new FinalizationRegistry((heldValue: string) => {
console.log(`L'objet associe a "${heldValue}" a ete collecte`);
});
let obj: { data: string } | null = { data: "important" };
registry.register(obj, "mon-objet-important");
// Plus tard, quand on n'en a plus besoin
obj = null;
// ... a un moment indetermine, le GC passe et le callback s'execute
Le deuxieme argument de register est le heldValue. C'est la valeur que le callback recoit. Ca ne doit pas etre l'objet lui-meme (sinon tu créés une référencé qui empeche le GC de le collecter).
L'API complète
typescriptconst registry = new FinalizationRegistry<string>((heldValue) => {
console.log(`Cleanup pour: ${heldValue}`);
});
// Enregistrer un objet avec un token de desinscription
const target = { name: "ressource" };
const unregisterToken = {};
registry.register(target, "ressource-42", unregisterToken);
// Si on veut annuler l'enregistrement avant la collecte
registry.unregister(unregisterToken);
Trois paramètres pour register :
- target : l'objet a surveiller
- heldValue : la valeur passee au callback (string, number, ce que tu veux)
- unregisterToken (optionnel) : un objet qui permet d'annuler l'enregistrement
Cas d'usage : nettoyage de ressources externes
Le cas le plus pertinent, c'est le nettoyage de ressources que JavaScript ne gere pas tout seul. Les fichiers ouverts, les connexions base de donnees, les WebSocket.
typescriptclass ManagedConnection {
private static registry = new FinalizationRegistry<number>((connId) => {
console.warn(`Connexion ${connId} n'a pas ete fermee proprement`);
ConnectionPool.forceClose(connId);
});
private connectionId: number;
constructor(host: string) {
this.connectionId = ConnectionPool.open(host);
ManagedConnection.registry.register(this, this.connectionId);
}
close(): void {
ConnectionPool.close(this.connectionId);
// Pas besoin du callback de finalisation si on ferme proprement
}
}
L'idee est de traiter FinalizationRegistry comme un filet de sécurité. La méthode close() reste la facon normale de libérer la ressource. Le registry n'est la que pour attraper les oublis.
Exemple avec des fichiers
typescriptclass FileWrapper {
private static registry = new FinalizationRegistry<number>((fd) => {
console.warn(`File descriptor ${fd} leake - fermeture d'urgence`);
fs.closeSync(fd);
});
private fd: number;
private token = {};
constructor(path: string) {
this.fd = fs.openSync(path, "r");
FileWrapper.registry.register(this, this.fd, this.token);
}
read(buffer: Buffer): number {
return fs.readSync(this.fd, buffer);
}
close(): void {
fs.closeSync(this.fd);
FileWrapper.registry.unregister(this.token);
}
}
Quand close() est appele, on desinscrit avec unregister. Si le dev oublie d'appeler close() et que l'objet est collecte, le callback ferme le fichier.
Pourquoi tu ne dois PAS t'y fier
Le GC est non-déterministe. Ca veut dire :
- Le callback peut s'exécuter des secondes, des minutes, voire jamais
- Si le programme se termine avant que le GC passe, le callback ne s'exécuté pas
- L'ordre d'exécution des callbacks n'est pas garanti
- Le GC peut décider de ne jamais collecter certains objets
typescript// MAUVAIS : s'appuyer sur FinalizationRegistry pour de la logique metier
const registry = new FinalizationRegistry(() => {
// Ceci peut ne jamais s'executer
database.commitTransaction();
});
// BON : l'utiliser comme diagnostic
const registry = new FinalizationRegistry((label: string) => {
metrics.increment("leaked_resources", { label });
});
La spec ECMAScript le dit clairement : une implementation JavaScript conforme n'est pas obligee d'appeler les callbacks de finalisation. Jamais.
Comparaison avec C++ et Rust
En C++, les destructeurs sont deterministes. Quand un objet sort du scope, le destructeur s'exécuté immédiatement. Avec RAII (Resource Acquisition Is Initialization), c'est la base de la gestion de ressources :
cpp{
std::ifstream file("data.txt");
// utilisation du fichier
} // Le destructeur ferme le fichier ICI, de facon garantie
En Rust, le trait Drop fonctionne pareil. Deterministe, garanti, dans l'ordre inverse de création :
rustimpl Drop for Connection {
fn drop(&mut self) {
self.close();
}
}
FinalizationRegistry ne remplace ni les destructeurs C++ ni Drop. C'est un outil de diagnostic et de filet de sécurité, pas un mecanisme de gestion de ressources.
| Aspect | C++ destructeur | Rust Drop | FinalizationRegistry |
|---|---|---|---|
| Deterministe | Oui | Oui | Non |
| Garanti | Oui | Oui | Non |
| Ordre | Inverse du scope | Inverse du scope | Indefini |
| Usage | Gestion ressources | Gestion ressources | Diagnostic/filet |
En pratique sur un vrai projet
Sur paltemps.fr, j'ai utilise FinalizationRegistry pour traquer les connexions WebSocket qui n'etaient pas fermees proprement. Le callback n'effectue aucune action critique, il log un warning dans les metriques. Le vrai nettoyage se fait via des méthodes explicites dans les handlers de deconnexion.
C'est la bonne mentalite : explicit cleanup d'abord, FinalizationRegistry en observateur.
typescriptconst leakTracker = new FinalizationRegistry<string>((socketId) => {
logger.warn(`WebSocket ${socketId} collecte sans close()`);
leakCounter.increment();
});
Quand le compteur de leaks monte, je sais qu'il y a un bug a corriger dans le code applicatif. FinalizationRegistry me donne la visibilité, pas la solution.
Résumé
- FinalizationRegistry exécuté un callback quand un objet est collecte par le GC
- L'API :
register(target, heldValue, unregisterToken),unregister(token) - Utile comme filet de sécurité pour les ressources externes (fichiers, connexions, sockets)
- Le GC est non-déterministe : le callback peut ne jamais s'exécuter
- Ce n'est pas un remplacement des destructeurs C++ ou du trait Drop de Rust
- Toujours préférer le nettoyage explicite, utiliser le registry pour le diagnostic
Article précédent : 07 - WeakRef et WeakMap Article suivant : 09 - DevTools Memory : investiguer dans Chrome