Async hook
[Stable: 1 - Experimental]
Stable: 1 Stability: 1 - 실험적입니다. 가능하다면 이 API에서 마이그레이션하십시오. createHook
, AsyncHook
, 및 executionAsyncResource
API는 사용성 문제, 안전 위험 및 성능 문제가 있으므로 사용하지 않는 것이 좋습니다. 비동기 컨텍스트 추적 사용 사례는 안정적인 AsyncLocalStorage
API를 사용하는 것이 더 좋습니다. AsyncLocalStorage
또는 Diagnostics Channel에서 현재 제공하는 진단 데이터로 해결되는 컨텍스트 추적 요구 사항을 넘어 createHook
, AsyncHook
또는 executionAsyncResource
에 대한 사용 사례가 있는 경우 https://github.com/nodejs/node/issues에서 사용 사례를 설명하는 문제를 제기하여 더욱 목적에 맞는 API를 만들 수 있도록 하십시오.
소스 코드: lib/async_hooks.js
async_hooks
API의 사용을 강력하게 권장하지 않습니다. 대부분의 사용 사례를 처리할 수 있는 다른 API는 다음과 같습니다.
AsyncLocalStorage
비동기 컨텍스트 추적process.getActiveResourcesInfo()
활성 리소스 추적
node:async_hooks
모듈은 비동기 리소스를 추적하기 위한 API를 제공합니다. 다음을 사용하여 액세스할 수 있습니다.
import async_hooks from 'node:async_hooks'
const async_hooks = require('node:async_hooks')
용어
비동기 리소스는 연결된 콜백이 있는 객체를 나타냅니다. 이 콜백은 net.createServer()
의 'connection'
이벤트와 같이 여러 번 호출될 수도 있고, fs.open()
과 같이 한 번만 호출될 수도 있습니다. 리소스는 콜백이 호출되기 전에 닫힐 수도 있습니다. AsyncHook
은 이러한 다양한 경우를 명시적으로 구분하지 않지만 추상적인 개념인 리소스로 표현합니다.
Worker
를 사용하는 경우 각 스레드에는 독립적인 async_hooks
인터페이스가 있으며 각 스레드는 새로운 비동기 ID 세트를 사용합니다.
개요
다음은 공개 API의 간략한 개요입니다.
import async_hooks from 'node:async_hooks'
// 현재 실행 컨텍스트의 ID를 반환합니다.
const eid = async_hooks.executionAsyncId()
// 현재 실행 범위의 콜백을 호출하도록 트리거하는 핸들의 ID를 반환합니다.
const tid = async_hooks.triggerAsyncId()
// 새로운 AsyncHook 인스턴스를 만듭니다. 이러한 콜백은 모두 선택 사항입니다.
const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve })
// 이 AsyncHook 인스턴스의 콜백이 호출되도록 허용합니다. 이것은 생성자를 실행한 후 암시적인 동작이 아니며, 콜백 실행을 시작하려면 명시적으로 실행해야 합니다.
asyncHook.enable()
// 새로운 비동기 이벤트에 대한 수신을 비활성화합니다.
asyncHook.disable()
//
// 다음은 createHook()에 전달할 수 있는 콜백입니다.
//
// init()은 객체 생성 중에 호출됩니다. 이 콜백이 실행될 때 리소스의 생성이 완료되지 않았을 수 있습니다. 따라서 "asyncId"가 참조하는 리소스의 모든 필드가 채워지지 않았을 수 있습니다.
function init(asyncId, type, triggerAsyncId, resource) {}
// before()는 리소스의 콜백이 호출되기 바로 전에 호출됩니다. 핸들(예: TCPWrap)의 경우 0~N번 호출될 수 있으며, 요청(예: FSReqCallback)의 경우 정확히 1번 호출됩니다.
function before(asyncId) {}
// after()는 리소스의 콜백이 완료된 직후에 호출됩니다.
function after(asyncId) {}
// destroy()는 리소스가 파괴될 때 호출됩니다.
function destroy(asyncId) {}
// promiseResolve()는 Promise 생성자에 전달된 resolve() 함수가 호출될 때(직접 또는 Promise를 해결하는 다른 방법을 통해) Promise 리소스에 대해서만 호출됩니다.
function promiseResolve(asyncId) {}
const async_hooks = require('node:async_hooks')
// 현재 실행 컨텍스트의 ID를 반환합니다.
const eid = async_hooks.executionAsyncId()
// 현재 실행 범위의 콜백을 호출하도록 트리거하는 핸들의 ID를 반환합니다.
const tid = async_hooks.triggerAsyncId()
// 새로운 AsyncHook 인스턴스를 만듭니다. 이러한 콜백은 모두 선택 사항입니다.
const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve })
// 이 AsyncHook 인스턴스의 콜백이 호출되도록 허용합니다. 이것은 생성자를 실행한 후 암시적인 동작이 아니며, 콜백 실행을 시작하려면 명시적으로 실행해야 합니다.
asyncHook.enable()
// 새로운 비동기 이벤트에 대한 수신을 비활성화합니다.
asyncHook.disable()
//
// 다음은 createHook()에 전달할 수 있는 콜백입니다.
//
// init()은 객체 생성 중에 호출됩니다. 이 콜백이 실행될 때 리소스의 생성이 완료되지 않았을 수 있습니다. 따라서 "asyncId"가 참조하는 리소스의 모든 필드가 채워지지 않았을 수 있습니다.
function init(asyncId, type, triggerAsyncId, resource) {}
// before()는 리소스의 콜백이 호출되기 바로 전에 호출됩니다. 핸들(예: TCPWrap)의 경우 0~N번 호출될 수 있으며, 요청(예: FSReqCallback)의 경우 정확히 1번 호출됩니다.
function before(asyncId) {}
// after()는 리소스의 콜백이 완료된 직후에 호출됩니다.
function after(asyncId) {}
// destroy()는 리소스가 파괴될 때 호출됩니다.
function destroy(asyncId) {}
// promiseResolve()는 Promise 생성자에 전달된 resolve() 함수가 호출될 때(직접 또는 Promise를 해결하는 다른 방법을 통해) Promise 리소스에 대해서만 호출됩니다.
function promiseResolve(asyncId) {}
async_hooks.createHook(callbacks)
추가됨: v8.1.0
callbacks
<Object> 등록할 Hook Callbacksinit
<Function>init
콜백.before
<Function>before
콜백.after
<Function>after
콜백.destroy
<Function>destroy
콜백.promiseResolve
<Function>promiseResolve
콜백.
반환값: <AsyncHook> 후크를 비활성화 및 활성화하는 데 사용되는 인스턴스
각 비동기 작업의 다양한 수명주기 이벤트에 대해 호출될 함수를 등록합니다.
init()
/before()
/after()
/destroy()
콜백은 리소스 수명 동안 해당 비동기 이벤트에 대해 호출됩니다.
모든 콜백은 선택 사항입니다. 예를 들어, 리소스 정리만 추적해야 하는 경우 destroy
콜백만 전달하면 됩니다. callbacks
에 전달할 수 있는 모든 함수의 세부 정보는 Hook Callbacks 섹션에 나와 있습니다.
import { createHook } from 'node:async_hooks'
const asyncHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {},
destroy(asyncId) {},
})
const async_hooks = require('node:async_hooks')
const asyncHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {},
destroy(asyncId) {},
})
콜백은 프로토타입 체인을 통해 상속됩니다.
class MyAsyncCallbacks {
init(asyncId, type, triggerAsyncId, resource) {}
destroy(asyncId) {}
}
class MyAddedCallbacks extends MyAsyncCallbacks {
before(asyncId) {}
after(asyncId) {}
}
const asyncHook = async_hooks.createHook(new MyAddedCallbacks())
프로미스는 비동기 후크 메커니즘을 통해 수명주기가 추적되는 비동기 리소스이므로 init()
, before()
, after()
, destroy()
콜백은 프로미스를 반환하는 비동기 함수가 아니어야 합니다.
에러 처리
AsyncHook
콜백에서 예외가 발생하면 애플리케이션은 스택 추적을 출력하고 종료됩니다. 종료 경로는 처리되지 않은 예외의 경로를 따르지만, 모든 'uncaughtException'
리스너는 제거되므로 프로세스가 강제로 종료됩니다. 애플리케이션이 --abort-on-uncaught-exception
으로 실행되지 않는 한 'exit'
콜백은 여전히 호출됩니다. --abort-on-uncaught-exception
으로 실행되는 경우 스택 추적이 출력되고 애플리케이션이 종료되어 코어 파일이 남습니다.
이러한 에러 처리 동작의 이유는 이러한 콜백이 객체의 수명 주기에서 잠재적으로 불안정한 시점(예: 클래스 생성 및 소멸 중)에 실행되기 때문입니다. 이 때문에 향후 의도하지 않은 중단을 방지하기 위해 프로세스를 신속하게 종료하는 것이 필요하다고 간주됩니다. 예외가 의도하지 않은 부작용 없이 일반적인 제어 흐름을 따를 수 있도록 포괄적인 분석을 수행하는 경우 향후 변경될 수 있습니다.
AsyncHook
콜백에서의 출력
콘솔에 출력하는 것은 비동기 작업이므로 console.log()
는 AsyncHook
콜백이 호출되도록 합니다. AsyncHook
콜백 함수 내에서 console.log()
또는 유사한 비동기 작업을 사용하면 무한 재귀가 발생합니다. 디버깅 시 이 문제에 대한 간단한 해결책은 fs.writeFileSync(file, msg, flag)
와 같은 동기식 로깅 작업을 사용하는 것입니다. 이렇게 하면 파일에 출력되고 동기식이므로 AsyncHook
를 재귀적으로 호출하지 않습니다.
import { writeFileSync } from 'node:fs'
import { format } from 'node:util'
function debug(...args) {
// AsyncHook 콜백 내부에서 디버깅할 때 이와 같은 함수를 사용하십시오.
writeFileSync('log.out', `${format(...args)}\n`, { flag: 'a' })
}
const fs = require('node:fs')
const util = require('node:util')
function debug(...args) {
// AsyncHook 콜백 내부에서 디버깅할 때 이와 같은 함수를 사용하십시오.
fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' })
}
로깅에 비동기 작업이 필요한 경우 AsyncHook
자체에서 제공하는 정보를 사용하여 비동기 작업의 원인을 추적할 수 있습니다. 그런 다음 로깅 자체가 AsyncHook
콜백이 호출되도록 한 원인인 경우 로깅을 건너뛰어야 합니다. 이렇게 하면 무한 재귀가 방지됩니다.
클래스: AsyncHook
AsyncHook
클래스는 비동기 작업의 수명 주기 이벤트를 추적하기 위한 인터페이스를 제공합니다.
asyncHook.enable()
- 반환값: <AsyncHook>
asyncHook
에 대한 참조.
주어진 AsyncHook
인스턴스에 대한 콜백을 활성화합니다. 콜백이 제공되지 않으면 활성화는 무효입니다.
AsyncHook
인스턴스는 기본적으로 비활성화됩니다. AsyncHook
인스턴스를 생성 직후 활성화해야 하는 경우 다음 패턴을 사용할 수 있습니다.
import { createHook } from 'node:async_hooks'
const hook = createHook(callbacks).enable()
const async_hooks = require('node:async_hooks')
const hook = async_hooks.createHook(callbacks).enable()
asyncHook.disable()
- 반환값: <AsyncHook>
asyncHook
에 대한 참조.
실행될 AsyncHook
콜백의 전역 풀에서 주어진 AsyncHook
인스턴스에 대한 콜백을 비활성화합니다. 후크가 비활성화되면 활성화될 때까지 다시 호출되지 않습니다.
API 일관성을 위해 disable()
도 AsyncHook
인스턴스를 반환합니다.
후크 콜백
비동기 이벤트의 수명 주기의 주요 이벤트는 인스턴스화, 콜백 호출 전/후, 인스턴스가 파괴될 때의 네 가지 영역으로 분류되었습니다.
init(asyncId, type, triggerAsyncId, resource)
asyncId
<number> 비동기 리소스의 고유 ID.type
<string> 비동기 리소스의 유형.triggerAsyncId
<number> 이 비동기 리소스가 생성된 비동기 리소스의 실행 컨텍스트의 고유 ID.resource
<Object> 비동기 작업을 나타내는 리소스에 대한 참조. destroy 중에 해제되어야 합니다.
비동기 이벤트를 발생시킬 가능성이 있는 클래스가 생성될 때 호출됩니다. 이는 인스턴스가 destroy
가 호출되기 전에 before
/after
를 호출해야 함을 의미하지 않고, 가능성만 존재함을 의미합니다.
이 동작은 리소스를 열었다가 리소스를 사용하기 전에 닫는 것과 같은 작업을 수행하여 관찰할 수 있습니다. 다음 코드 스니펫은 이를 보여줍니다.
import { createServer } from 'node:net'
createServer().listen(function () {
this.close()
})
// OR
clearTimeout(setTimeout(() => {}, 10))
require('node:net')
.createServer()
.listen(function () {
this.close()
})
// OR
clearTimeout(setTimeout(() => {}, 10))
새로운 각 리소스에는 현재 Node.js 인스턴스의 범위 내에서 고유한 ID가 할당됩니다.
type
type
은 init
호출을 유발한 리소스의 유형을 식별하는 문자열입니다. 일반적으로 리소스 생성자의 이름에 해당합니다.
Node.js 자체에서 생성된 리소스의 type
은 Node.js 릴리스마다 변경될 수 있습니다. 유효한 값으로는 TLSWRAP
, TCPWRAP
, TCPSERVERWRAP
, GETADDRINFOREQWRAP
, FSREQCALLBACK
, Microtask
및 Timeout
이 포함됩니다. 전체 목록을 얻으려면 사용 중인 Node.js 버전의 소스 코드를 검사하십시오.
또한 AsyncResource
사용자는 Node.js 자체와 독립적인 비동기 리소스를 생성합니다.
Promise
인스턴스 및 해당 인스턴스에서 예약된 비동기 작업을 추적하는 데 사용되는 PROMISE
리소스 유형도 있습니다.
사용자는 공용 임베더 API를 사용할 때 자체 type
을 정의할 수 있습니다.
유형 이름 충돌이 발생할 수 있습니다. 후크를 수신할 때 충돌을 방지하려면 임베더는 npm 패키지 이름과 같은 고유 접두사를 사용하는 것이 좋습니다.
triggerAsyncId
triggerAsyncId
는 새 리소스를 초기화하고 init
호출을 유발한(또는 "트리거한") 리소스의 asyncId
입니다. 이것은 리소스가 생성된 시점만 표시하는 async_hooks.executionAsyncId()
와 다릅니다. triggerAsyncId
는 리소스가 생성된 이유를 보여줍니다.
다음은 triggerAsyncId
의 간단한 데모입니다.
import { createHook, executionAsyncId } from 'node:async_hooks'
import { stdout } from 'node:process'
import net from 'node:net'
import fs from 'node:fs'
createHook({
init(asyncId, type, triggerAsyncId) {
const eid = executionAsyncId()
fs.writeSync(stdout.fd, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
}).enable()
net.createServer(conn => {}).listen(8080)
const { createHook, executionAsyncId } = require('node:async_hooks')
const { stdout } = require('node:process')
const net = require('node:net')
const fs = require('node:fs')
createHook({
init(asyncId, type, triggerAsyncId) {
const eid = executionAsyncId()
fs.writeSync(stdout.fd, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
}).enable()
net.createServer(conn => {}).listen(8080)
nc localhost 8080
으로 서버에 접속했을 때 출력:
TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0
TCPSERVERWRAP
은 연결을 수신하는 서버입니다.
TCPWRAP
은 클라이언트의 새 연결입니다. 새 연결이 생성되면 TCPWrap
인스턴스가 즉시 생성됩니다. 이는 모든 JavaScript 스택 외부에서 발생합니다. (executionAsyncId()
가 0
이라는 것은 C++에서 위에 JavaScript 스택이 없이 실행되고 있음을 의미합니다.) 이 정보만으로는 리소스가 생성된 이유를 기준으로 리소스를 연결하는 것이 불가능하므로 triggerAsyncId
에 새 리소스의 존재에 대한 책임이 있는 리소스를 전파하는 작업이 주어집니다.
resource
resource
는 초기화된 실제 비동기 리소스를 나타내는 객체입니다. 객체에 접근하는 API는 리소스 생성자가 지정할 수 있습니다. Node.js 자체에서 생성된 리소스는 내부적이며 언제든지 변경될 수 있습니다. 따라서 이러한 리소스에 대한 API는 지정되어 있지 않습니다.
성능상의 이유로 리소스 객체가 재사용되는 경우가 있습니다. 따라서 WeakMap
의 키로 사용하거나 속성을 추가하는 것은 안전하지 않습니다.
비동기 컨텍스트 예시
컨텍스트 추적 사용 사례는 안정적인 API AsyncLocalStorage
에서 다룹니다. 이 예시는 비동기 후크 동작만 보여주지만 AsyncLocalStorage
가 이 사용 사례에 더 적합합니다.
다음은 before
와 after
호출 사이의 init
호출에 대한 추가 정보, 특히 listen()
에 대한 콜백이 어떻게 생겼는지에 대한 예시입니다. 호출 컨텍스트를 더 쉽게 볼 수 있도록 출력 형식이 약간 더 자세합니다.
import async_hooks from 'node:async_hooks'
import fs from 'node:fs'
import net from 'node:net'
import { stdout } from 'node:process'
const { fd } = stdout
let indent = 0
async_hooks
.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId()
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}${type}(${asyncId}):` + ` trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
before(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`)
indent += 2
},
after(asyncId) {
indent -= 2
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`)
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`)
},
})
.enable()
net
.createServer(() => {})
.listen(8080, () => {
// Let's wait 10ms before logging the server started.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId())
}, 10)
})
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
const net = require('node:net')
const { fd } = process.stdout
let indent = 0
async_hooks
.createHook({
init(asyncId, type, triggerAsyncId) {
const eid = async_hooks.executionAsyncId()
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}${type}(${asyncId}):` + ` trigger: ${triggerAsyncId} execution: ${eid}\n`)
},
before(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`)
indent += 2
},
after(asyncId) {
indent -= 2
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`)
},
destroy(asyncId) {
const indentStr = ' '.repeat(indent)
fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`)
},
})
.enable()
net
.createServer(() => {})
.listen(8080, () => {
// Let's wait 10ms before logging the server started.
setTimeout(() => {
console.log('>>>', async_hooks.executionAsyncId())
}, 10)
})
서버 시작만으로 나오는 출력:
TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before: 6
Timeout(7): trigger: 6 execution: 6
after: 6
destroy: 6
before: 7
>>> 7
TickObject(8): trigger: 7 execution: 7
after: 7
before: 8
after: 8
예시에서 보듯이, executionAsyncId()
와 execution
은 각각 현재 실행 컨텍스트의 값을 지정합니다. 이는 before
와 after
호출로 구분됩니다.
execution
만 사용하여 리소스 할당을 그래프로 나타내면 다음과 같습니다.
root(1)
^
|
TickObject(6)
^
|
Timeout(7)
console.log()
가 호출된 이유였음에도 불구하고 TCPSERVERWRAP
은 이 그래프에 포함되어 있지 않습니다. 이는 호스트 이름 없이 포트에 바인딩하는 것이 동기 작업이지만, 완전히 비동기 API를 유지하기 위해 사용자의 콜백이 process.nextTick()
에 배치되기 때문입니다. 따라서 TickObject
가 출력에 나타나고 .listen()
콜백의 '부모'가 됩니다.
이 그래프는 리소스가 언제 생성되었는지만 보여주고 왜 생성되었는지는 보여주지 않습니다. 따라서 왜 생성되었는지 추적하려면 triggerAsyncId
를 사용하십시오. 다음 그래프로 나타낼 수 있습니다.
bootstrap(1)
|
˅
TCPSERVERWRAP(5)
|
˅
TickObject(6)
|
˅
Timeout(7)
before(asyncId)
asyncId
<number>
비동기 작업이 시작되거나(예: TCP 서버가 새 연결을 수신하거나) 완료될 때(예: 디스크에 데이터 쓰기), 사용자에게 알리기 위해 콜백이 호출됩니다. before
콜백은 해당 콜백이 실행되기 바로 직전에 호출됩니다. asyncId
는 콜백을 실행하려는 리소스에 할당된 고유 식별자입니다.
before
콜백은 0~N회 호출됩니다. 비동기 작업이 취소되거나 예를 들어 TCP 서버가 연결을 수신하지 않는 경우 before
콜백은 일반적으로 0회 호출됩니다. TCP 서버와 같은 지속적인 비동기 리소스는 일반적으로 before
콜백을 여러 번 호출하는 반면, fs.open()
과 같은 다른 작업은 한 번만 호출합니다.
after(asyncId)
asyncId
<number>
before
에서 지정된 콜백이 완료된 직후에 호출됩니다.
콜백 실행 중에 처리되지 않은 예외가 발생하면 after
는 'uncaughtException'
이벤트가 발생하거나 domain
의 처리기가 실행된 후에 실행됩니다.
destroy(asyncId)
asyncId
<number>
asyncId
에 해당하는 리소스가 파괴된 후에 호출됩니다. 또한 임베디드 API emitDestroy()
에서 비동기적으로 호출됩니다.
일부 리소스는 정리가 가비지 콜렉션에 의존하므로 init
에 전달된 resource
객체에 대한 참조가 있으면 destroy
가 절대 호출되지 않아 애플리케이션에서 메모리 누수가 발생할 수 있습니다. 리소스가 가비지 콜렉션에 의존하지 않는 경우에는 문제가 되지 않습니다.
destroy 후크를 사용하면 가비지 콜렉터를 통해 Promise
인스턴스 추적이 가능해지므로 추가 오버헤드가 발생합니다.
promiseResolve(asyncId)
추가됨: v8.6.0
asyncId
<number>
Promise
생성자에 전달된 resolve
함수가 호출될 때(직접 또는 Promise를 해결하는 다른 방법을 통해) 호출됩니다.
resolve()
는 관찰 가능한 동기 작업을 수행하지 않습니다.
Promise
가 다른 Promise
의 상태를 가정하여 해결된 경우, 이 시점에서 Promise
가 반드시 이행되거나 거부되는 것은 아닙니다.
new Promise(resolve => resolve(true)).then(a => {})
다음 콜백을 호출합니다.
init for PROMISE with id 5, trigger id: 1
promise resolve 5 # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
before 6 # the then() callback is entered
promise resolve 6 # the then() callback resolves the promise by returning
after 6
async_hooks.executionAsyncResource()
추가됨: v13.9.0, v12.17.0
- 반환값: <Object> 현재 실행을 나타내는 리소스. 리소스 내에 데이터를 저장하는 데 유용합니다.
executionAsyncResource()
가 반환하는 리소스 객체는 대부분 문서화되지 않은 API를 가진 내부 Node.js 핸들 객체입니다. 객체의 함수나 속성을 사용하면 애플리케이션이 충돌할 수 있으므로 피해야 합니다.
최상위 실행 컨텍스트에서 executionAsyncResource()
를 사용하면 사용할 핸들 또는 요청 객체가 없으므로 빈 객체가 반환되지만, 최상위를 나타내는 객체가 있으면 유용할 수 있습니다.
import { open } from 'node:fs'
import { executionAsyncId, executionAsyncResource } from 'node:async_hooks'
console.log(executionAsyncId(), executionAsyncResource()) // 1 {}
open(new URL(import.meta.url), 'r', (err, fd) => {
console.log(executionAsyncId(), executionAsyncResource()) // 7 FSReqWrap
})
const { open } = require('node:fs')
const { executionAsyncId, executionAsyncResource } = require('node:async_hooks')
console.log(executionAsyncId(), executionAsyncResource()) // 1 {}
open(__filename, 'r', (err, fd) => {
console.log(executionAsyncId(), executionAsyncResource()) // 7 FSReqWrap
})
이는 추적 Map
을 사용하여 메타데이터를 저장하지 않고도 연속 로컬 저장소를 구현하는 데 사용할 수 있습니다.
import { createServer } from 'node:http'
import { executionAsyncId, executionAsyncResource, createHook } from 'node:async_hooks'
const sym = Symbol('state') // 오염을 방지하기 위한 개인 심볼
createHook({
init(asyncId, type, triggerAsyncId, resource) {
const cr = executionAsyncResource()
if (cr) {
resource[sym] = cr[sym]
}
},
}).enable()
const server = createServer((req, res) => {
executionAsyncResource()[sym] = { state: req.url }
setTimeout(function () {
res.end(JSON.stringify(executionAsyncResource()[sym]))
}, 100)
}).listen(3000)
const { createServer } = require('node:http')
const { executionAsyncId, executionAsyncResource, createHook } = require('node:async_hooks')
const sym = Symbol('state') // 오염을 방지하기 위한 개인 심볼
createHook({
init(asyncId, type, triggerAsyncId, resource) {
const cr = executionAsyncResource()
if (cr) {
resource[sym] = cr[sym]
}
},
}).enable()
const server = createServer((req, res) => {
executionAsyncResource()[sym] = { state: req.url }
setTimeout(function () {
res.end(JSON.stringify(executionAsyncResource()[sym]))
}, 100)
}).listen(3000)
async_hooks.executionAsyncId()
[히스토리]
버전 | 변경 사항 |
---|---|
v8.2.0 | currentId 에서 이름 변경 |
v8.1.0 | 추가됨: v8.1.0 |
- 반환값: <number> 현재 실행 컨텍스트의
asyncId
. 무엇이 호출했는지 추적하는 데 유용합니다.
import { executionAsyncId } from 'node:async_hooks'
import fs from 'node:fs'
console.log(executionAsyncId()) // 1 - 부트스트랩
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(executionAsyncId()) // 6 - open()
})
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')
console.log(async_hooks.executionAsyncId()) // 1 - 부트스트랩
const path = '.'
fs.open(path, 'r', (err, fd) => {
console.log(async_hooks.executionAsyncId()) // 6 - open()
})
executionAsyncId()
에서 반환된 ID는 인과 관계(이는 triggerAsyncId()
에서 다룹니다)가 아닌 실행 타이밍과 관련이 있습니다.
const server = net
.createServer(conn => {
// 콜백이 서버의 MakeCallback() 실행 범위에서 실행되므로 새 연결의 ID가 아니라 서버의 ID를 반환합니다.
async_hooks.executionAsyncId()
})
.listen(port, () => {
// .listen()에 전달된 모든 콜백은 nextTick()으로 래핑되지만,
// 콜백 자체는 서버의 .listen() 호출 때문에 존재합니다. 따라서 반환값은 서버의 ID가 됩니다.
async_hooks.executionAsyncId()
})
Promise 컨텍스트는 기본적으로 정확한 executionAsyncIds
를 가져오지 못할 수 있습니다. Promise 실행 추적 섹션을 참조하십시오.
async_hooks.triggerAsyncId()
- 반환값: <number> 현재 실행 중인 콜백을 호출하는 데 책임이 있는 리소스의 ID.
const server = net
.createServer(conn => {
// 이 콜백을 호출한 원인(또는 트리거)이 된 리소스는 새 연결의 리소스였습니다. 따라서 triggerAsyncId()의 반환값은 "conn"의 asyncId입니다.
async_hooks.triggerAsyncId()
})
.listen(port, () => {
// .listen()에 전달된 모든 콜백은 nextTick()으로 래핑되지만,
// 콜백 자체는 서버의 .listen() 호출 때문에 존재합니다. 따라서 반환값은 서버의 ID가 됩니다.
async_hooks.triggerAsyncId()
})
Promise 컨텍스트는 기본적으로 유효한 triggerAsyncId
를 가져오지 못할 수 있습니다. Promise 실행 추적 섹션을 참조하십시오.
async_hooks.asyncWrapProviders
추가됨: v17.2.0, v16.14.0
- 반환값: 제공자 유형과 해당 숫자 ID의 매핑. 이 매핑에는
async_hooks.init()
이벤트에서 발생할 수 있는 모든 이벤트 유형이 포함됩니다.
이 기능은 process.binding('async_wrap').Providers
의 더 이상 사용되지 않는 사용을 억제합니다. 참조: DEP0111
Promise 실행 추적
기본적으로 Promise 실행에는 V8에서 제공하는 Promise 내부 검사 API의 상대적으로 비용이 많이 드는 특성으로 인해 asyncId
가 할당되지 않습니다. 즉, Promise 또는 async
/await
를 사용하는 프로그램은 기본적으로 올바른 실행 및 Promise 콜백 컨텍스트에 대한 트리거 ID를 가져오지 않습니다.
import { executionAsyncId, triggerAsyncId } from 'node:async_hooks'
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// 출력:
// eid 1 tid 0
const { executionAsyncId, triggerAsyncId } = require('node:async_hooks')
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// 출력:
// eid 1 tid 0
then()
콜백이 비동기 호핑이 포함되어 있음에도 불구하고 외부 범위의 컨텍스트에서 실행되었다고 주장하는 것을 확인하십시오. 또한 triggerAsyncId
값은 0
이며, 이는 then()
콜백의 실행을 유발한 리소스에 대한 컨텍스트가 누락되었음을 의미합니다.
async_hooks.createHook
를 통해 비동기 후크를 설치하면 Promise 실행 추적이 가능합니다.
import { createHook, executionAsyncId, triggerAsyncId } from 'node:async_hooks'
createHook({ init() {} }).enable() // PromiseHooks를 활성화합니다.
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// 출력:
// eid 7 tid 6
const { createHook, executionAsyncId, triggerAsyncId } = require('node:async_hooks')
createHook({ init() {} }).enable() // PromiseHooks를 활성화합니다.
Promise.resolve(1729).then(() => {
console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`)
})
// 출력:
// eid 7 tid 6
이 예에서 실제 후크 함수를 추가하면 Promise 추적이 활성화되었습니다. 위의 예에는 Promise.resolve()
에 의해 생성된 Promise와 then()
호출에 의해 반환된 Promise의 두 가지 Promise가 있습니다. 위의 예에서 첫 번째 Promise는 asyncId
6
을 얻었고 후자는 asyncId
7
을 얻었습니다. then()
콜백의 실행 중에 asyncId
7
을 가진 Promise의 컨텍스트에서 실행됩니다. 이 Promise는 비동기 리소스 6
에 의해 트리거되었습니다.
Promise의 또 다른 미묘한 점은 before
및 after
콜백이 체인된 Promise에서만 실행된다는 것입니다. 즉, then()
/catch()
에 의해 생성되지 않은 Promise에는 before
및 after
콜백이 실행되지 않습니다. 자세한 내용은 V8 PromiseHooks API의 세부 정보를 참조하십시오.
JavaScript 임베더 API
I/O, 연결 풀링 또는 콜백 큐 관리와 같은 작업을 수행하는 자체 비동기 리소스를 처리하는 라이브러리 개발자는 모든 적절한 콜백이 호출되도록 AsyncResource
JavaScript API를 사용할 수 있습니다.
클래스: AsyncResource
이 클래스에 대한 설명서는 AsyncResource
로 이동되었습니다.
클래스: AsyncLocalStorage
이 클래스에 대한 설명서는 AsyncLocalStorage
로 이동되었습니다.