INTEGRITY Dokumentace

Životní cyklus

Životnosti, paměť a správa prostředků

Když přes RPC voláte jiný Worker pomocí Service bindingu, spotřebováváte paměť ve volaném Workeru. Uvažujme následující příklad:

let user = await env.USER_SERVICE.findUser(id);

Předpokládejme, že findUser() na straně serveru vrací objekt, který rozšiřuje RpcTarget, a proto user na straně klienta nakonec je stub odkazující na tento vzdálený objekt.

Dokud stub existuje na klientovi, odpovídající objekt na serveru nelze uvolnit garbage collectorem. Každý izolát má ovšem vlastní garbage collector, který nevidí do ostatních izolátů. Aby server věděl, že objekt lze uvolnit, musí mu volající izolát poslat explicitní signál. Tomuto signálu se říká „disposing“ stubu.

V mnoha případech (popsaných níže) systém sám rozpozná, že stub už není potřeba, a automaticky jej zruší. Pro nejlepší výkon by však měl váš kód stuby po jejich použití zrušit explicitně.

Explicitní správa prostředků

Aby se prostředky správně uvolnily, měli byste použít Explicitní správa prostředků, a new JavaScript language feature that allows you to explicitly signal when resources can be disposed of. Explicit Resource Management is a Stage 3 TC39 proposal, it is brzy dostupné v V8.

Explicitní správa prostředků přidává do jazyka následující funkce:

Pokud je proměnná deklarována pomocí using, jakmile proměnná přestane být v platnosti, zavolá se její disposer. Například:

function sendEmail(id, message) {
  using user = await env.USER_SERVICE.findUser(id);
  await user.sendEmail(message);

  // user[Symbol.dispose]() is implicitly called at the end of the scope.
}

using deklarace jsou užitečné, abyste nezapomněli stuby zlikvidovat, i když je váš kód přerušen výjimkou.

Jak používat using deklarace ve vašem Workeru

Wrangler v4+ podporuje using klíčové slovo nativně. Pokud používáte starší verzi Wrangleru, budete muset prostředky uvolňovat ručně.

Následující kód:

{
	using counter = await env.COUNTER_SERVICE.newCounter();
	await counter.increment(2);
	await counter.increment(4);
}

...odpovídá:

{
	const counter = await env.COUNTER_SERVICE.newCounter();
	try {
		await counter.increment(2);
		await counter.increment(4);
	} finally {
		counter[Symbol.dispose]();
	}
}

Automatické uvolňování a kontexty provádění

Systém RPC automaticky uvolňuje stuby v následujících případech:

Konec obslužné rutiny události / kontextu spouštění

Jakmile je handler události "done", všechny stuby vytvořené v rámci dané události se automaticky uvolní.

Uvažujme například fetch() handler který zpracovává příchozí HTTP události. Handler může v rámci zpracování události provádět odchozí RPC volání, která mohou vracet stuby. Jakmile je odeslána finální HTTP odpověď, je handler "hotový" a všechny stuby jsou okamžitě uvolněny.

Přesněji řečeno, událost má takzvaný „kontext provádění“, který začíná ve chvíli prvního vyvolání handleru a končí odesláním HTTP odpovědi. Kontext provádění může skončit i dříve, pokud se klient odpojí ještě před přijetím odpovědi, nebo jej lze prodloužit za jeho běžný konec zavoláním ctx.waitUntil().

Worker níže například nevyužívá using deklarace, ale stuby budou zlikvidovány, jakmile fetch() handler vrátí odpověď:

export default {
	async fetch(request, env, ctx) {
		let authResult = await env.AUTH_SERVICE.checkCookie(
			req.headers.get("Cookie"),
		);
		if (!authResult.authorized) {
			return new Response("Not authorized", { status: 403 });
		}
		let profile = await authResult.user.getProfile();

		return new Response(`Hello, ${profile.name}!`);
	},
};

Worker vyvolaný přes RPC má také svůj kontext provádění. Kontext začíná ve chvíli, kdy metoda RPC na WorkerEntrypoint se vyvolá. Pokud v parametrech ani ve výsledcích tohoto RPC nejsou předány žádné zástupné objekty, kontext končí (událost je "done") ve chvíli, kdy RPC vrátí výsledek. Pokud jsou ale předány nějaké zástupné objekty, kontext provádění se implicitně prodlužuje, dokud se všechny tyto zástupné objekty neuvolní (a všechna volání provedená jejich prostřednictvím nevrátí výsledek). Stejně jako u HTTP platí, že pokud se klient odpojí, kontext provádění na serveru se okamžitě zruší bez ohledu na to, zda zástupné objekty stále existují. Klient, který je sám o sobě dalším Workerem, je považován za odpojený ve chvíli, kdy skončí jeho vlastní kontext provádění. Kontext lze opět prodloužit pomocí ctx.waitUntil().

Stuby přijaté jako parametry v RPC volání

Když jsou stuby přijaty jako parametry RPC, po návratu volání se automaticky uvolní. Chcete-li si stuby ponechat déle, musíte zavolat dup() metodu na nich.

Uvolnění RPC objektu uvolní i stuby, které jsou jeho součástí

Když RPC vrátí jakýkoli objekt, systém k němu automaticky přidá disposer. Jeho zavoláním se uvolní (dispose) všechny stuby vrácené daným voláním. Pokud například RPC vrátí pole čtyř stubů, bude mít disposer i samotné pole, a ten uvolní všechny čtyři stuby najednou. Jediný případ, kdy hodnota vrácená z RPC disposer nemá, nastává tehdy, když jde o primitivní hodnotu, například číslo nebo řetězec. Těmto typům nelze disposer přidat, ale protože samy o sobě nemohou obsahovat stuby, disposer v tomto případě není potřeba.

To znamená, že výsledek RPC volání byste téměř vždy měli uložit do using deklarace:

using result = stub.foo();

Díky tomu se případné stuby obsažené ve výsledku uvolní. I když neočekáváte, že RPC vrátí stuby, pokud vrací jakýkoli objekt, je vhodné jej uložit do using deklarace. Díky tomu bude váš kód připravený i pro případ, že by RPC bylo v budoucnu rozšířeno o vracení stubů.

Pokud se rozhodnete vrácený stub ponechat i mimo rozsah using deklarace můžete zavolat dup() na stub před koncem rozsahu platnosti. (Nezapomeňte duplikát později explicitně uvolnit pomocí dispose.)

Disposery a RpcTarget třídy

Třída, která rozšiřuje RpcTarget může volitelně implementovat disposer:

class Foo extends RpcTarget {
	[Symbol.dispose]() {
		// ...
	}
}

Disposer objektu RpcTarget se spustí až po uvolnění posledního stubu. Všimněte si, že volání disposeru stubu na straně klienta nečeká na zavolání disposeru na straně serveru, disposer serveru se volá později. Kvůli tomu se výjimky vyvolané disposerem nepropagují ke klientovi, ale jsou hlášeny jako nezachycené výjimky. Upozorňujeme, že RpcTarget's disposer must be declared as Symbol.dispose. Symbol.asyncDispose není podporováno.

dup() metoda

Někdy potřebujete předat stub funkci, která ho po dokončení práce zruší, ale zároveň si ho chcete ponechat pro pozdější použití. Tento problém můžete vyřešit tak, že na stub zavoláte "dup":

let stub = await env.SOME_SERVICE.getThing();

// Create a duplicate.
let stub2 = stub.dup();

// Call some function that will dispose the stub.
await func(stub);

// stub2 is still valid

Můžete si to představit jako dup() jako Systémové volání Unixu se stejným názvem: vytvoří nový handle ukazující na stejný cíl, který je nutné nezávisle uzavřít (disposed).

Pokud instance RpcTarget třída na kterou stuby odkazují, má disposer, tento disposer se vyvolá až po zlikvidování všech duplicit. To se však týká pouze duplicit pocházejících ze stejného stubu. Pokud stejná instance RpcTarget se přes RPC předává vícekrát, pokaždé se vytvoří nový zástupný objekt a tyto objekty se nepovažují za vzájemné duplicity. Disposer se proto vyvolá pokaždé, kdy se RpcTarget byla odeslána.

Abyste se této situaci vyhnuli, můžete stub vytvořit ručně lokálně a poté ho přes RPC předat vícekrát. Při předání stubu přes RPC přechází jeho vlastnictví na příjemce, proto musíte vytvořit dup() pokaždé, když jej odešlete:

import { RpcTarget, RpcStub } from "cloudflare:workers";

class Foo extends RpcTarget {
	// ...
}

let obj = new Foo();
let stub = new RpcStub(obj);
await rpc1(stub.dup()); // sends a dup of `stub`
await rpc2(stub.dup()); // sends another dup of `stub`
stub[Symbol.dispose](); // disposes the original stub

// obj's disposer will be called when the other two stubs
// are disposed remotely.