Node.js Backend Development Bootcamp · Lekcja

Propagacja kontekstu za pomocą AsyncLocalStorage

Przenoś stan związany z żądaniem przez granice asynchroniczności bez przekazywania go przez kolejne warstwy, korzystając z AsyncLocalStorage

Lekcja 4 z 413 kroki

Propagacja kontekstu za pomocą AsyncLocalStorage to bezpłatna lekcja Node.js Backend Development Bootcamp na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Node.js Backend Development Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.

Problem przekazywania parametrów przez wiele warstw

W usłudze backendowej dane przypisane do żądania, takie jak identyfikator żądania, uwierzytelniony użytkownik czy identyfikator dzierżawy, są potrzebne głęboko w stosie wywołań: w repozytoriach, loggerach i wychodzących klientach HTTP.

Naiwnym rozwiązaniem jest prop drilling — przekazywanie argumentu ctx przez każdą funkcję:

  • Sygnatury wszystkich funkcji zostają zaśmiecone parametrem ctx.
  • Jedno pominięte przekazanie powoduje utratę kontekstu przez dalsze wywołanie.
  • Kod biblioteczny, którego nie kontroluje użytkownik, w ogóle nie może otrzymać jego ctx.

Potrzebny jest sposób niejawnego przenoszenia stanu przypisanego do żądania, który przetrwa każde await i każdy callback. Właśnie to zapewnia AsyncLocalStorage.

async function handler(ctx, req) {
  const user = await loadUser(ctx, req.userId);
  return await renderPage(ctx, user);
}

async function loadUser(ctx, id) {
  log(ctx, 'loading user');         // ctx passed again
  return await db.find(ctx, id);    // and again...
}

// Every layer must accept and forward ctx by hand.

Czym jest AsyncLocalStorage

AsyncLocalStorage znajduje się we wbudowanym module node:async_hooks. Można myśleć o nim jak o pamięci lokalnej wątku dla świata asynchronicznego: magazynie pozostającym przypisanym do logicznego łańcucha operacji asynchronicznych.

Wywołanie als.run(store, callback) ustanawia magazyn, a następnie w dowolnym miejscu wewnątrz tego callbacku — niezależnie od liczby zagłębień await, łańcuchów Promise, wywołań setTimeout czy emiterów zdarzeń — als.getStore() zwraca ten sam magazyn.

  • Każde współbieżne żądanie otrzymuje własny izolowany magazyn.
  • Brak zmiennych globalnych i wyścigów między żądaniami.
  • Pod spodem mechanizm korzysta ze śledzenia kontekstu asynchronicznego Node.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

als.run({ requestId: 'abc-123' }, () => {
  setTimeout(() => {
    const store = als.getStore();
    console.log(store.requestId); // 'abc-123'
  }, 10);
});

run() ustanawia kontekst

Podstawowym API jest als.run(store, fn, ...args). Wykonuje ono fn synchronicznie, ale wiąże store z całym drzewem asynchronicznym utworzonym w jego ramach.

  • store może być dowolną wartością — najczęściej zwykłym obiektem lub obiektem Map.
  • run() zwraca wszystko, co zwraca fn (w tym Promise).
  • Poza callbackiem getStore() zwraca undefined.

Ten fragment pokazuje, że magazyn zachowuje się podczas await na rzeczywistym zegarze.

import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as sleep } from 'node:timers/promises';

const als = new AsyncLocalStorage();

async function deep() {
  await sleep(5);
  return als.getStore()?.requestId;
}

await als.run({ requestId: 'r-42' }, async () => {
  const id = await deep();
  console.log('inside run:', id);   // 'r-42'
});

console.log('outside run:', als.getStore()); // undefined

Podłączanie do serwera HTTP

Wzorzec stosowany na prawdziwym serwerze polega na opakowaniu obsługi żądania w als.run() na samym brzegu systemu i zainicjalizowaniu magazynu świeżym identyfikatorem żądania. Cały kod znajdujący się dalej może go odczytywać.

Poniższy przykład korzysta wyłącznie z wbudowanego modułu node:http, więc nie ma zależności od frameworka. Każde przychodzące żądanie otrzymuje własny magazyn, odizolowany od żądań współbieżnych.

import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function currentRequestId() {
  return als.getStore()?.requestId ?? 'no-context';
}

const server = http.createServer((req, res) => {
  als.run({ requestId: randomUUID() }, async () => {
    // deep call needs no ctx argument
    await Promise.resolve();
    res.end('request id: ' + currentRequestId());
  });
});

server.listen(3000, () => console.log('listening on 3000'));

Wzorzec middleware Express

W Express idiomatycznym miejscem wywołania als.run() jest middleware zamontowane jako pierwsze. Musi ono wywołać next() wewnątrz callbacku, aby reszta łańcucha odziedziczyła magazyn.

  • Należy odczytać przychodzący nagłówek x-request-id, jeśli proxy lub usługa nadrzędna go dostarczyła; w przeciwnym razie trzeba wygenerować nowy UUID.
  • Każdy kolejny handler, serwis i logger może odczytać magazyn bez otrzymywania go jako argumentu.
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

export const als = new AsyncLocalStorage();

export function contextMiddleware(req, res, next) {
  const store = {
    requestId: req.headers['x-request-id'] || randomUUID(),
    startedAt: Date.now(),
  };
  als.run(store, () => next()); // next() runs inside the context
}

export function getStore() {
  const store = als.getStore();
  if (!store) throw new Error('No request context');
  return store;
}

Logowanie strukturalne świadome kontekstu

Największa korzyść: logger, który automatycznie opatruje każdą linię identyfikatorem żądania — osoba wywołująca nigdy go nie przekazuje.

Logger sam odczytuje dane ze store. Jeśli nie ma aktywnego kontekstu (np. podczas uruchamiania), działa bez zakłóceń w trybie awaryjnym.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function log(level, msg, extra = {}) {
  const store = als.getStore();
  const line = {
    ts: new Date().toISOString(),
    level,
    msg,
    requestId: store?.requestId ?? null,
    ...extra,
  };
  console.log(JSON.stringify(line));
}

als.run({ requestId: 'req-7' }, () => {
  log('info', 'user fetched', { userId: 99 });
});

log('warn', 'no request context here');

Modyfikowanie Store w trakcie żądania

Ponieważ store jest referencją (obiektem lub Map), można go wzbogacić po zakończeniu uwierzytelniania. Kolejne wpisy w logu automatycznie uwzględnią nowe pola.

  • Na brzegu aplikacji zainicjalizować minimalne dane (identyfikator żądania).
  • Po wykonaniu middleware uwierzytelniania dodać userId i tenantId do tego samego obiektu store.
  • Wybrać Map, jeśli potrzebny jest przejrzysty interfejs oparty na kluczach; zwykły obiekt również się sprawdzi i jest nieco szybszy.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function set(key, value) {
  const store = als.getStore();
  if (store) store.set(key, value);
}
function get(key) {
  return als.getStore()?.get(key);
}

als.run(new Map([['requestId', 'r-1']]), () => {
  // ... later, after authenticating:
  set('userId', 42);
  set('tenantId', 'acme');
  console.log(get('requestId'), get('userId'), get('tenantId'));
});

enterWith a run

Store można ustawić na dwa sposoby:

  • als.run(store, fn) — ogranicza store do zakresu fn i jego asynchronicznych elementów potomnych. Po zakończeniu działania fn kontekst znika. Preferowane rozwiązanie.
  • als.enterWith(store) — ustawia store dla bieżącego wykonania synchronicznego oraz wszystkiego, co nastąpi później w ramach tego samego zasobu asynchronicznego, bez automatycznego opuszczenia kontekstu.

enterWith jest zdradliwe: wywołanie go w długotrwałym zasobie asynchronicznym może spowodować przeniknięcie kontekstu do niezwiązanych z nim późniejszych operacji. Należy używać run, chyba że istnieje konkretny powód (np. nie można opakować callbacku).

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

// Scoped and safe — context ends with the callback:
als.run({ id: 'A' }, () => {
  console.log(als.getStore().id); // 'A'
});
console.log(als.getStore());      // undefined

// enterWith persists with no clear boundary — easy to leak:
als.enterWith({ id: 'B' });
console.log(als.getStore().id);   // 'B' (and stays set!)

Gdzie kontekst może zostać utracony

AsyncLocalStorage obsługuje natywne promisy, async/await, timery i większość emiterów zdarzeń. Jednak w kilku sytuacjach kontekst może zostać utracony:

  • Operacje zaplanowane przed wywołaniem run() — np. pula połączeń lub kolejka utworzona podczas uruchamiania wykonuje callbacki poza store żądania.
  • Niektóre starsze biblioteki, które przechowują lub ponownie wykorzystują zasoby między żądaniami, mogą przenosić nieaktualny store.
  • Ręcznie odłączone callbacki przechowywane w globalnej tablicy i wywoływane później.

Podczas integrowania takiego kodu należy użyć als.bind(fn) (lub AsyncResource.bind), które zapisuje migawkę bieżącego kontekstu i ponownie go stosuje przy każdym późniejszym wywołaniu funkcji.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const queue = [];

als.run({ requestId: 'r-9' }, () => {
  // bind captures the current store for later execution
  queue.push(als.bind(() => {
    console.log('later:', als.getStore()?.requestId);
  }));
});

// Runs outside the run() callback, but context is preserved:
queue.forEach((fn) => fn()); // later: r-9

Moduł wielokrotnego użytku dla kontekstu

W praktyce store jest centralizowany w jednym niewielkim module, dzięki czemu reszta bazy kodu importuje wyłącznie funkcje pomocnicze — bez bezpośredniego dostępu do instancji AsyncLocalStorage.

Dzięki temu interfejs API pozostaje uporządkowany: runWithContext() na brzegu aplikacji, a requestId() / getUser() w pozostałych miejscach.

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage();

export function runWithContext(seed, fn) {
  const store = { requestId: randomUUID(), ...seed };
  return als.run(store, fn);
}

export function context() {
  return als.getStore() ?? null;
}

export function requestId() {
  return context()?.requestId ?? null;
}

export function setUser(user) {
  const store = als.getStore();
  if (store) store.user = user;
}

Wydajność i dobre praktyki

AsyncLocalStorage w nowoczesnym Node (v16+) korzysta z wydajnej implementacji natywnej, a narzut jest niewielki — akceptowalny praktycznie dla wszystkich obciążeń aplikacji webowych. Mimo to należy przestrzegać kilku dobrych praktyk:

  • Używać jednej długotrwałej instancji AsyncLocalStorage na każdy zakres odpowiedzialności, a nie jednej na żądanie.
  • Przechowywać w store niewielką ilość danych; jest to stan przypisany do żądania, a nie pamięć podręczna.
  • Nie przechowywać sekretów, których nie chcieliby Państwo zobaczyć w logach odczytujących store.
  • Preferować run() zamiast enterWith(), aby uzyskać automatyczne czyszczenie.
  • Zawsze obsługiwać przypadek store o wartości undefined w kodzie, który może działać poza żądaniem.

Szybkie sprawdzenie

Sprawdź swoją wiedzę na temat bezpiecznego ustanawiania kontekstu żądania.

Podsumowanie

Dowiedział się Pan/Pani, jak propagować stan przypisany do żądania przez granice asynchroniczne bez przekazywania go przez kolejne warstwy:

  • AsyncLocalStorage z node:async_hooks to magazyn lokalny dla wątków świata asynchronicznego.
  • als.run(store, fn) wiąże store z fn i wszystkimi jego asynchronicznymi elementami potomnymi; als.getStore() odczytuje go w dowolnym miejscu; poza zakresem run ma on wartość undefined.
  • Należy skonfigurować go raz na brzegu aplikacji (serwerze HTTP lub w pierwszym middleware), inicjalizując identyfikator żądania, a następnie wzbogacić store po uwierzytelnieniu.
  • Logger świadomy kontekstu może automatycznie opatrywać każdą linię identyfikatorem żądania.
  • Należy preferować run() zamiast enterWith() ze względu na automatyczne czyszczenie oraz używać als.bind() / AsyncResource.bind() do zachowania kontekstu w odłączonych callbackach.
  • Store powinien być niewielki, należy obsługiwać brak kontekstu, a dostęp do niego centralizować w jednym module.
Bezpłatny start

Ucz się JavaScript dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
22
Lekcje
92

Często zadawane pytania

Czy lekcja „Propagacja kontekstu za pomocą AsyncLocalStorage” jest bezpłatna?

Tak — pełny tekst „Propagacja kontekstu za pomocą AsyncLocalStorage” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Node.js Backend Development Bootcamp, przejdź na CoddyKit PRO. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.

Co nauczysz się w „Propagacja kontekstu za pomocą AsyncLocalStorage”?

Przenoś stan związany z żądaniem przez granice asynchroniczności bez przekazywania go przez kolejne warstwy, korzystając z AsyncLocalStorage Ćwiczysz Node.js Backend Development Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Node.js Backend Development Bootcamp?

Nie wymagamy żadnego doświadczenia. Node.js Backend Development Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Propagacja kontekstu za pomocą AsyncLocalStorage”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Node.js Backend Development Bootcamp?

Tak. Każda lekcja Node.js Backend Development Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Ustrukturyzowane logowanie z identyfikatorami korelacji
  2. Śledzenie rozproszone za pomocą spanów OpenTelemetry
  3. Udostępnianie metryk aplikacji i metoda RED
  4. Propagacja kontekstu za pomocą AsyncLocalStorage
← Powrót do Node.js Backend Development Bootcamp