Rastreamento de contexto assíncrono
[Estável: 2 - Estável]
Estável: 2 Estabilidade: 2 - Estável
Código-fonte: lib/async_hooks.js
Introdução
Estas classes são usadas para associar o estado e propagá-lo através de callbacks e cadeias de promessas. Elas permitem armazenar dados durante todo o ciclo de vida de uma solicitação da web ou qualquer outra duração assíncrona. É semelhante ao armazenamento local de threads em outras linguagens.
As classes AsyncLocalStorage
e AsyncResource
fazem parte do módulo node:async_hooks
:
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks'
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks')
Classe: AsyncLocalStorage
[Histórico]
Versão | Mudanças |
---|---|
v16.4.0 | AsyncLocalStorage agora é Estável. Anteriormente, era Experimental. |
v13.10.0, v12.17.0 | Adicionado em: v13.10.0, v12.17.0 |
Esta classe cria armazenamentos que permanecem coerentes através de operações assíncronas.
Embora você possa criar sua própria implementação sobre o módulo node:async_hooks
, AsyncLocalStorage
deve ser preferido, pois é uma implementação de alto desempenho e segura para a memória que envolve otimizações significativas que não são óbvias de implementar.
O exemplo a seguir usa AsyncLocalStorage
para construir um logger simples que atribui IDs às solicitações HTTP recebidas e os inclui em mensagens registradas em cada solicitação.
import http from 'node:http'
import { AsyncLocalStorage } from 'node:async_hooks'
const asyncLocalStorage = new AsyncLocalStorage()
function logWithId(msg) {
const id = asyncLocalStorage.getStore()
console.log(`${id !== undefined ? id : '-'}:`, msg)
}
let idSeq = 0
http
.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start')
// Imagine qualquer cadeia de operações assíncronas aqui
setImmediate(() => {
logWithId('finish')
res.end()
})
})
})
.listen(8080)
http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Imprime:
// 0: start
// 1: start
// 0: finish
// 1: finish
const http = require('node:http')
const { AsyncLocalStorage } = require('node:async_hooks')
const asyncLocalStorage = new AsyncLocalStorage()
function logWithId(msg) {
const id = asyncLocalStorage.getStore()
console.log(`${id !== undefined ? id : '-'}:`, msg)
}
let idSeq = 0
http
.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start')
// Imagine qualquer cadeia de operações assíncronas aqui
setImmediate(() => {
logWithId('finish')
res.end()
})
})
})
.listen(8080)
http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Imprime:
// 0: start
// 1: start
// 0: finish
// 1: finish
Cada instância de AsyncLocalStorage
mantém um contexto de armazenamento independente. Várias instâncias podem existir com segurança simultaneamente sem risco de interferir nos dados umas das outras.
new AsyncLocalStorage()
[Histórico]
Versão | Mudanças |
---|---|
v19.7.0, v18.16.0 | Removeu a opção experimental onPropagate. |
v19.2.0, v18.13.0 | Adicionou a opção onPropagate. |
v13.10.0, v12.17.0 | Adicionado em: v13.10.0, v12.17.0 |
Cria uma nova instância de AsyncLocalStorage
. O armazenamento é fornecido apenas dentro de uma chamada run()
ou após uma chamada enterWith()
.
Método estático: AsyncLocalStorage.bind(fn)
Adicionado em: v19.8.0, v18.16.0
[Estável: 1 - Experimental]
Estável: 1 Estabilidade: 1 - Experimental
fn
<Function> A função a ser vinculada ao contexto de execução atual.- Retorna: <Function> Uma nova função que chama
fn
dentro do contexto de execução capturado.
Vincula a função fornecida ao contexto de execução atual.
Método estático: AsyncLocalStorage.snapshot()
Adicionado em: v19.8.0, v18.16.0
[Estável: 1 - Experimental]
Estável: 1 Estabilidade: 1 - Experimental
- Retorna: <Function> Uma nova função com a assinatura
(fn: (...args) : R, ...args) : R
.
Captura o contexto de execução atual e retorna uma função que aceita uma função como argumento. Sempre que a função retornada é chamada, ela chama a função passada para ela dentro do contexto capturado.
const asyncLocalStorage = new AsyncLocalStorage()
const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot())
const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore()))
console.log(result) // retorna 123
AsyncLocalStorage.snapshot() pode substituir o uso de AsyncResource para fins simples de rastreamento de contexto assíncrono, por exemplo:
class Foo {
#runInAsyncScope = AsyncLocalStorage.snapshot()
get() {
return this.#runInAsyncScope(() => asyncLocalStorage.getStore())
}
}
const foo = asyncLocalStorage.run(123, () => new Foo())
console.log(asyncLocalStorage.run(321, () => foo.get())) // retorna 123
asyncLocalStorage.disable()
Adicionado em: v13.10.0, v12.17.0
[Estável: 1 - Experimental]
Estável: 1 Estabilidade: 1 - Experimental
Desabilita a instância de AsyncLocalStorage
. Todas as chamadas subsequentes para asyncLocalStorage.getStore()
retornarão undefined
até que asyncLocalStorage.run()
ou asyncLocalStorage.enterWith()
seja chamado novamente.
Ao chamar asyncLocalStorage.disable()
, todos os contextos atuais ligados à instância serão encerrados.
É necessário chamar asyncLocalStorage.disable()
antes que o asyncLocalStorage
possa ser coletado pelo coletor de lixo. Isso não se aplica aos armazenamentos fornecidos pelo asyncLocalStorage
, pois esses objetos são coletados pelo coletor de lixo junto com os recursos assíncronos correspondentes.
Use este método quando o asyncLocalStorage
não estiver mais em uso no processo atual.
asyncLocalStorage.getStore()
Adicionado em: v13.10.0, v12.17.0
- Retorna: <any>
Retorna o armazenamento atual. Se chamado fora de um contexto assíncrono inicializado chamando asyncLocalStorage.run()
ou asyncLocalStorage.enterWith()
, ele retorna undefined
.
asyncLocalStorage.enterWith(store)
Adicionado em: v13.11.0, v12.17.0
[Estável: 1 - Experimental]
Estável: 1 Estabilidade: 1 - Experimental
store
<any>
Faz a transição para o contexto para o restante da execução síncrona atual e, em seguida, persiste o armazenamento por meio de quaisquer chamadas assíncronas subsequentes.
Exemplo:
const store = { id: 1 }
// Substitui o armazenamento anterior pelo objeto de armazenamento fornecido
asyncLocalStorage.enterWith(store)
asyncLocalStorage.getStore() // Retorna o objeto de armazenamento
someAsyncOperation(() => {
asyncLocalStorage.getStore() // Retorna o mesmo objeto
})
Essa transição continuará por toda a execução síncrona. Isso significa que, se, por exemplo, o contexto for inserido em um manipulador de eventos, os manipuladores de eventos subsequentes também serão executados dentro desse contexto, a menos que sejam especificamente vinculados a outro contexto com um AsyncResource
. É por isso que run()
deve ser preferido em vez de enterWith()
, a menos que haja fortes razões para usar o último método.
const store = { id: 1 }
emitter.on('my-event', () => {
asyncLocalStorage.enterWith(store)
})
emitter.on('my-event', () => {
asyncLocalStorage.getStore() // Retorna o mesmo objeto
})
asyncLocalStorage.getStore() // Retorna undefined
emitter.emit('my-event')
asyncLocalStorage.getStore() // Retorna o mesmo objeto
asyncLocalStorage.run(store, callback[, ...args])
Adicionado em: v13.10.0, v12.17.0
store
<qualquer>callback
<Função>...args
<qualquer>
Executa uma função de forma síncrona dentro de um contexto e retorna seu valor de retorno. A store não é acessível fora da função de callback. A store é acessível a quaisquer operações assíncronas criadas dentro do callback.
Os args
opcionais são passados para a função de callback.
Se a função de callback lançar um erro, o erro também é lançado por run()
. O rastreamento de pilha não é afetado por esta chamada e o contexto é encerrado.
Exemplo:
const store = { id: 2 }
try {
asyncLocalStorage.run(store, () => {
asyncLocalStorage.getStore() // Retorna o objeto store
setTimeout(() => {
asyncLocalStorage.getStore() // Retorna o objeto store
}, 200)
throw new Error()
})
} catch (e) {
asyncLocalStorage.getStore() // Retorna indefinido
// O erro será capturado aqui
}
asyncLocalStorage.exit(callback[, ...args])
Adicionado em: v13.10.0, v12.17.0
[Estável: 1 - Experimental]
Estável: 1 Estabilidade: 1 - Experimental
callback
<Função>...args
<qualquer>
Executa uma função de forma síncrona fora de um contexto e retorna seu valor de retorno. A store não é acessível dentro da função de callback ou das operações assíncronas criadas dentro do callback. Qualquer chamada getStore()
feita dentro da função de callback sempre retornará undefined
.
Os args
opcionais são passados para a função de callback.
Se a função de callback lançar um erro, o erro também é lançado por exit()
. O rastreamento de pilha não é afetado por esta chamada e o contexto é reinserido.
Exemplo:
// Dentro de uma chamada para run
try {
asyncLocalStorage.getStore() // Retorna o objeto ou valor da store
asyncLocalStorage.exit(() => {
asyncLocalStorage.getStore() // Retorna indefinido
throw new Error()
})
} catch (e) {
asyncLocalStorage.getStore() // Retorna o mesmo objeto ou valor
// O erro será capturado aqui
}
Uso com async/await
Se, dentro de uma função assíncrona, apenas uma chamada await
deve ser executada dentro de um contexto, o seguinte padrão deve ser usado:
async function fn() {
await asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('key', value)
return foo() // O valor de retorno de foo será aguardado
})
}
Neste exemplo, o armazenamento só está disponível na função de callback e nas funções chamadas por foo
. Fora de run
, chamar getStore
retornará undefined
.
Solução de problemas: Perda de contexto
Na maioria dos casos, AsyncLocalStorage
funciona sem problemas. Em raras situações, o armazenamento atual é perdido em uma das operações assíncronas.
Se o seu código for baseado em callback, é suficiente transformá-lo em promise com util.promisify()
para que comece a funcionar com promises nativas.
Se você precisar usar uma API baseada em callback ou seu código assumir uma implementação thenable personalizada, use a classe AsyncResource
para associar a operação assíncrona ao contexto de execução correto. Encontre a chamada de função responsável pela perda de contexto registrando o conteúdo de asyncLocalStorage.getStore()
após as chamadas que você suspeita serem responsáveis pela perda. Quando o código registra undefined
, o último callback chamado provavelmente é responsável pela perda de contexto.
Classe: AsyncResource
[Histórico]
Versão | Mudanças |
---|---|
v16.4.0 | AsyncResource agora é estável. Anteriormente, era experimental. |
A classe AsyncResource
foi projetada para ser estendida pelos recursos assíncronos do incorporador. Ao usá-la, os usuários podem facilmente acionar os eventos de tempo de vida de seus próprios recursos.
O hook init
será acionado quando um AsyncResource
for instanciado.
A seguir, uma visão geral da API AsyncResource
.
import { AsyncResource, executionAsyncId } from 'node:async_hooks'
// AsyncResource() deve ser estendida. Instanciar um
// novo AsyncResource() também aciona init. Se triggerAsyncId for omitido, então
// async_hook.executionAsyncId() é usado.
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })
// Executa uma função no contexto de execução do recurso. Isso irá
// * estabelecer o contexto do recurso
// * acionar os callbacks before do AsyncHooks
// * chamar a função fornecida `fn` com os argumentos fornecidos
// * acionar os callbacks after do AsyncHooks
// * restaurar o contexto de execução original
asyncResource.runInAsyncScope(fn, thisArg, ...args)
// Chama os callbacks de destruição do AsyncHooks.
asyncResource.emitDestroy()
// Retorna o ID único atribuído à instância AsyncResource.
asyncResource.asyncId()
// Retorna o ID de acionamento para a instância AsyncResource.
asyncResource.triggerAsyncId()
const { AsyncResource, executionAsyncId } = require('node:async_hooks')
// AsyncResource() deve ser estendida. Instanciar um
// novo AsyncResource() também aciona init. Se triggerAsyncId for omitido, então
// async_hook.executionAsyncId() é usado.
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })
// Executa uma função no contexto de execução do recurso. Isso irá
// * estabelecer o contexto do recurso
// * acionar os callbacks before do AsyncHooks
// * chamar a função fornecida `fn` com os argumentos fornecidos
// * acionar os callbacks after do AsyncHooks
// * restaurar o contexto de execução original
asyncResource.runInAsyncScope(fn, thisArg, ...args)
// Chama os callbacks de destruição do AsyncHooks.
asyncResource.emitDestroy()
// Retorna o ID único atribuído à instância AsyncResource.
asyncResource.asyncId()
// Retorna o ID de acionamento para a instância AsyncResource.
asyncResource.triggerAsyncId()
new AsyncResource(type[, options])
type
<string> O tipo de evento assíncrono.options
<Object>triggerAsyncId
<number> O ID do contexto de execução que criou este evento assíncrono. Padrão:executionAsyncId()
.requireManualDestroy
<boolean> Se definido comotrue
, desativaemitDestroy
quando o objeto é coletado pelo garbage collector. Isso geralmente não precisa ser definido (mesmo seemitDestroy
for chamado manualmente), a menos que oasyncId
do recurso seja recuperado e oemitDestroy
da API sensível seja chamado com ele. Quando definido comofalse
, a chamadaemitDestroy
na coleta de lixo só ocorrerá se houver pelo menos um ganchodestroy
ativo. Padrão:false
.
Exemplo de uso:
class DBQuery extends AsyncResource {
constructor(db) {
super('DBQuery')
this.db = db
}
getInfo(query, callback) {
this.db.get(query, (err, data) => {
this.runInAsyncScope(callback, null, err, data)
})
}
close() {
this.db = null
this.emitDestroy()
}
}
Método estático: AsyncResource.bind(fn[, type[, thisArg]])
[Histórico]
Versão | Mudanças |
---|---|
v20.0.0 | A propriedade asyncResource adicionada à função vinculada foi descontinuada e será removida em uma versão futura. |
v17.8.0, v16.15.0 | Alterado o padrão quando thisArg é indefinido para usar this do chamador. |
v16.0.0 | Adicionado thisArg opcional. |
v14.8.0, v12.19.0 | Adicionado em: v14.8.0, v12.19.0 |
fn
<Function> A função a ser vinculada ao contexto de execução atual.type
<string> Um nome opcional para associar aoAsyncResource
subjacente.thisArg
<any>
Vincula a função fornecida ao contexto de execução atual.
asyncResource.bind(fn[, thisArg])
[Histórico]
Versão | Mudanças |
---|---|
v20.0.0 | A propriedade asyncResource adicionada à função vinculada foi depreciada e será removida em uma versão futura. |
v17.8.0, v16.15.0 | Alterado o padrão quando thisArg é indefinido para usar this do chamador. |
v16.0.0 | Adicionado thisArg opcional. |
v14.8.0, v12.19.0 | Adicionado em: v14.8.0, v12.19.0 |
fn
<Function> A função a ser vinculada aoAsyncResource
atual.thisArg
<any>
Vincula a função fornecida para executar no escopo deste AsyncResource
.
asyncResource.runInAsyncScope(fn[, thisArg, ...args])
Adicionado em: v9.6.0
fn
<Function> A função a ser chamada no contexto de execução deste recurso assíncrono.thisArg
<any> O receptor a ser usado para a chamada da função....args
<any> Argumentos opcionais para passar para a função.
Chama a função fornecida com os argumentos fornecidos no contexto de execução do recurso assíncrono. Isso estabelecerá o contexto, acionará os callbacks before do AsyncHooks, chamará a função, acionará os callbacks after do AsyncHooks e, em seguida, restaurará o contexto de execução original.
asyncResource.emitDestroy()
- Retorna: <AsyncResource> Uma referência a
asyncResource
.
Chama todos os hooks destroy
. Isso só deve ser chamado uma vez. Um erro será lançado se for chamado mais de uma vez. Isso deve ser chamado manualmente. Se o recurso for deixado para ser coletado pelo GC, os hooks destroy
nunca serão chamados.
asyncResource.asyncId()
- Retorna: <number> O
asyncId
único atribuído ao recurso.
asyncResource.triggerAsyncId()
- Retorna: <number> O mesmo
triggerAsyncId
que é passado para o construtorAsyncResource
.
Usando AsyncResource
para um pool de threads Worker
O exemplo a seguir mostra como usar a classe AsyncResource
para fornecer adequadamente o rastreamento assíncrono para um pool de Worker
. Outros pools de recursos, como pools de conexões de banco de dados, podem seguir um modelo semelhante.
Assumindo que a tarefa seja adicionar dois números, usando um arquivo chamado task_processor.js
com o seguinte conteúdo:
import { parentPort } from 'node:worker_threads'
parentPort.on('message', task => {
parentPort.postMessage(task.a + task.b)
})
const { parentPort } = require('node:worker_threads')
parentPort.on('message', task => {
parentPort.postMessage(task.a + task.b)
})
Um pool de Worker ao seu redor poderia usar a seguinte estrutura:
import { AsyncResource } from 'node:async_hooks'
import { EventEmitter } from 'node:events'
import { Worker } from 'node:worker_threads'
const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')
class WorkerPoolTaskInfo extends AsyncResource {
constructor(callback) {
super('WorkerPoolTaskInfo')
this.callback = callback
}
done(err, result) {
this.runInAsyncScope(this.callback, null, err, result)
this.emitDestroy() // `TaskInfo`s são usados apenas uma vez.
}
}
export default class WorkerPool extends EventEmitter {
constructor(numThreads) {
super()
this.numThreads = numThreads
this.workers = []
this.freeWorkers = []
this.tasks = []
for (let i = 0; i < numThreads; i++) this.addNewWorker()
// Sempre que o kWorkerFreedEvent é emitido, despacha
// a próxima tarefa pendente na fila, se houver.
this.on(kWorkerFreedEvent, () => {
if (this.tasks.length > 0) {
const { task, callback } = this.tasks.shift()
this.runTask(task, callback)
}
})
}
addNewWorker() {
const worker = new Worker(new URL('task_processor.js', import.meta.url))
worker.on('message', result => {
// Em caso de sucesso: Chama o callback que foi passado para `runTask`,
// remove o `TaskInfo` associado ao Worker e o marca como livre
// novamente.
worker[kTaskInfo].done(null, result)
worker[kTaskInfo] = null
this.freeWorkers.push(worker)
this.emit(kWorkerFreedEvent)
})
worker.on('error', err => {
// Em caso de exceção não capturada: Chama o callback que foi passado para
// `runTask` com o erro.
if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
else this.emit('error', err)
// Remove o worker da lista e inicia um novo Worker para substituir o
// atual.
this.workers.splice(this.workers.indexOf(worker), 1)
this.addNewWorker()
})
this.workers.push(worker)
this.freeWorkers.push(worker)
this.emit(kWorkerFreedEvent)
}
runTask(task, callback) {
if (this.freeWorkers.length === 0) {
// Sem threads livres, aguarde até que uma thread de worker fique livre.
this.tasks.push({ task, callback })
return
}
const worker = this.freeWorkers.pop()
worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
worker.postMessage(task)
}
close() {
for (const worker of this.workers) worker.terminate()
}
}
const { AsyncResource } = require('node:async_hooks')
const { EventEmitter } = require('node:events')
const path = require('node:path')
const { Worker } = require('node:worker_threads')
const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')
class WorkerPoolTaskInfo extends AsyncResource {
constructor(callback) {
super('WorkerPoolTaskInfo')
this.callback = callback
}
done(err, result) {
this.runInAsyncScope(this.callback, null, err, result)
this.emitDestroy() // `TaskInfo`s são usados apenas uma vez.
}
}
class WorkerPool extends EventEmitter {
constructor(numThreads) {
super()
this.numThreads = numThreads
this.workers = []
this.freeWorkers = []
this.tasks = []
for (let i = 0; i < numThreads; i++) this.addNewWorker()
// Sempre que o kWorkerFreedEvent é emitido, despacha
// a próxima tarefa pendente na fila, se houver.
this.on(kWorkerFreedEvent, () => {
if (this.tasks.length > 0) {
const { task, callback } = this.tasks.shift()
this.runTask(task, callback)
}
})
}
addNewWorker() {
const worker = new Worker(path.resolve(__dirname, 'task_processor.js'))
worker.on('message', result => {
// Em caso de sucesso: Chama o callback que foi passado para `runTask`,
// remove o `TaskInfo` associado ao Worker e o marca como livre
// novamente.
worker[kTaskInfo].done(null, result)
worker[kTaskInfo] = null
this.freeWorkers.push(worker)
this.emit(kWorkerFreedEvent)
})
worker.on('error', err => {
// Em caso de exceção não capturada: Chama o callback que foi passado para
// `runTask` com o erro.
if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
else this.emit('error', err)
// Remove o worker da lista e inicia um novo Worker para substituir o
// atual.
this.workers.splice(this.workers.indexOf(worker), 1)
this.addNewWorker()
})
this.workers.push(worker)
this.freeWorkers.push(worker)
this.emit(kWorkerFreedEvent)
}
runTask(task, callback) {
if (this.freeWorkers.length === 0) {
// Sem threads livres, aguarde até que uma thread de worker fique livre.
this.tasks.push({ task, callback })
return
}
const worker = this.freeWorkers.pop()
worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
worker.postMessage(task)
}
close() {
for (const worker of this.workers) worker.terminate()
}
}
module.exports = WorkerPool
Sem o rastreamento explícito adicionado pelos objetos WorkerPoolTaskInfo
, pareceria que os callbacks estão associados aos objetos Worker
individuais. No entanto, a criação dos Worker
s não está associada à criação das tarefas e não fornece informações sobre quando as tarefas foram agendadas.
Este pool pode ser usado da seguinte forma:
import WorkerPool from './worker_pool.js'
import os from 'node:os'
const pool = new WorkerPool(os.availableParallelism())
let finished = 0
for (let i = 0; i < 10; i++) {
pool.runTask({ a: 42, b: 100 }, (err, result) => {
console.log(i, err, result)
if (++finished === 10) pool.close()
})
}
const WorkerPool = require('./worker_pool.js')
const os = require('node:os')
const pool = new WorkerPool(os.availableParallelism())
let finished = 0
for (let i = 0; i < 10; i++) {
pool.runTask({ a: 42, b: 100 }, (err, result) => {
console.log(i, err, result)
if (++finished === 10) pool.close()
})
}
Integrando AsyncResource
com EventEmitter
Os listeners de eventos acionados por um EventEmitter
podem ser executados em um contexto de execução diferente daquele que estava ativo quando eventEmitter.on()
foi chamado.
O exemplo a seguir mostra como usar a classe AsyncResource
para associar corretamente um listener de evento com o contexto de execução correto. A mesma abordagem pode ser aplicada a um Stream
ou uma classe semelhante orientada a eventos.
import { createServer } from 'node:http'
import { AsyncResource, executionAsyncId } from 'node:async_hooks'
const server = createServer((req, res) => {
req.on(
'close',
AsyncResource.bind(() => {
// O contexto de execução está vinculado ao escopo externo atual.
})
)
req.on('close', () => {
// O contexto de execução está vinculado ao escopo que fez com que 'close' emitisse.
})
res.end()
}).listen(3000)
const { createServer } = require('node:http')
const { AsyncResource, executionAsyncId } = require('node:async_hooks')
const server = createServer((req, res) => {
req.on(
'close',
AsyncResource.bind(() => {
// O contexto de execução está vinculado ao escopo externo atual.
})
)
req.on('close', () => {
// O contexto de execução está vinculado ao escopo que fez com que 'close' emitisse.
})
res.end()
}).listen(3000)