INTEGRITY Документация

Жизненный цикл

Время жизни, память и управление ресурсами

Когда вы вызываете другой Worker через RPC с помощью Service binding, вы используете память того Worker, который вызываете. Рассмотрим следующий пример:

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

Предположим, что findUser() на стороне сервера возвращает объект, расширяющий RpcTarget, таким образом user на стороне клиента становится заглушкой, указывающей на этот удалённый объект.

Пока stub всё ещё существует на клиенте, соответствующий объект на сервере не может быть удалён сборщиком мусора. Но у каждого изолята есть собственный сборщик мусора, который не видит другие изоляты. Поэтому, чтобы изолят сервера узнал, что объект можно удалить, вызывающий изолят должен отправить ему явный сигнал, который называется «освобождением» stub.

Во многих случаях (описанных ниже) система автоматически определяет, что стаб больше не нужен, и освобождает его сама. Однако для лучшей производительности рекомендуется явно освобождать стабы в коде, когда они больше не нужны.

Явное управление ресурсами

Чтобы гарантировать корректное освобождение ресурсов, используйте Явное управление ресурсами, 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 скоро появится в V8.

Явное управление ресурсами добавляет следующие возможности языка:

Если переменная объявлена с помощью using, когда переменная выходит из области видимости, вызывается её disposer. Например:

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 объявления полезны, чтобы не забыть освободить заглушки, даже если выполнение кода прервётся исключением.

Как использовать using объявление в вашем Worker

Wrangler v4+ поддерживает using ключевое слово нативно. Если вы используете более раннюю версию Wrangler, вам потребуется освобождать ресурсы вручную.

Следующий код:

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

...эквивалентно:

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

Автоматическое освобождение ресурсов и контексты выполнения

Система RPC автоматически освобождает заглушки (stubs) в следующих случаях:

Конец обработчика события / контекста выполнения

Когда работа обработчика события завершена («done»), все стабы, созданные в рамках этого события, освобождаются автоматически.

Например, рассмотрим fetch() обработчик который обрабатывает входящие HTTP события. В рамках обработки события обработчик может выполнять исходящие RPC вызовы, которые могут возвращать заглушки. Когда итоговый HTTP ответ отправлен, обработчик считается «завершён», и все заглушки немедленно уничтожаются.

Точнее говоря, у события есть «контекст выполнения», который начинается при первом вызове обработчика и заканчивается при отправке HTTP-ответа. Контекст выполнения может также завершиться раньше, если клиент отключится до получения ответа, либо его можно продлить за пределы обычной точки завершения, вызвав ctx.waitUntil().

Например, приведённый ниже Worker не использует using объявление, но заглушки будут освобождены после того, как fetch() обработчик возвращает ответ:

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, вызванный через RPC, также имеет контекст выполнения. Этот контекст начинается, когда RPC метод у WorkerEntrypoint вызывается. Если в параметрах или результатах этого RPC не передано ни одного стаба, контекст завершается (событие получает статус «готово»), как только RPC возвращает результат. Однако если переданы какие-либо стабы, контекст выполнения неявно продлевается до тех пор, пока все эти стабы не будут уничтожены (и все вызовы через них не завершатся). Как и в HTTP, если клиент отключается, контекст выполнения на сервере отменяется немедленно, независимо от того, существуют ли еще стабы. Клиент, который сам является другим Worker, считается отключившимся, когда завершается его собственный контекст выполнения. И в этом случае контекст можно продлить с помощью ctx.waitUntil().

Заглушки, полученные в качестве параметров вызова RPC

Если стабы (stubs) получены в параметрах RPC, они автоматически освобождаются после завершения вызова. Чтобы сохранить их дольше, необходимо вызвать dup() метод к ним.

Освобождение объекта RPC приводит к освобождению stubs, входящих в состав этого объекта

Когда RPC возвращает объект любого типа, система добавляет к нему disposer. Его вызов освобождает все стабы (stubs), возвращенные этим вызовом. Например, если RPC возвращает массив из четырех стабов, сам массив получает disposer, который освобождает все четыре стаба. Единственный случай, когда возвращаемое RPC значение не имеет disposer, это примитивное значение, например число или строка. Для таких типов disposer не добавляется, но поскольку они не могут содержать стабы, disposer им и не нужен.

Это означает, что результат RPC почти всегда следует сохранять в using объявление:

using result = stub.foo();

Таким образом, если результат содержит заглушки, они будут освобождены. Даже если вы не ожидаете, что RPC вернет заглушки, если он возвращает объект любого рода, стоит сохранить его в using объявление. Таким образом, если в будущем RPC будет расширен и начнёт возвращать заглушки, ваш код уже будет к этому готов.

Если вы хотите сохранить возвращенную заглушку за пределами области действия using объявление, вы можете вызвать dup() для заглушки до конца области видимости. (Не забудьте позже явно освободить дубликат.)

Disposers и RpcTarget классы

Класс, который расширяет RpcTarget может при необходимости реализовать disposer:

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

Метод disposer у RpcTarget выполняется после освобождения последней заглушки. Обратите внимание, что вызов disposer заглушки на стороне клиента не ожидает вызова disposer на стороне сервера: серверный disposer вызывается позже. Из-за этого любые исключения, возникшие в disposer, не передаются клиенту, а регистрируются как необработанные исключения. Обратите внимание, что RpcTarget: disposer должен быть объявлен как Symbol.dispose. Symbol.asyncDispose не поддерживается.

dup() метод

Иногда нужно передать заглушку в функцию, которая освободит её после завершения работы, но при этом саму заглушку нужно сохранить для дальнейшего использования. Чтобы решить эту задачу, можно сделать "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

Можно представить себе dup() как Одноимённый системный вызов Unix: создаётся новый дескриптор, указывающий на ту же цель, который необходимо закрывать (освобождать) отдельно.

Если экземпляр RpcTarget класс на который указывают заглушки, имеет disposer, этот disposer будет вызван только тогда, когда будут уничтожены все дубликаты. Однако это распространяется только на дубликаты, порождённые одной и той же заглушкой. Если тот же экземпляр RpcTarget передается через RPC несколько раз, каждый раз создается новый стаб, и они не считаются дубликатами друг друга. Поэтому disposer будет вызываться каждый раз, когда RpcTarget был отправлен.

Чтобы избежать такой ситуации, можно вручную создать стаб локально и затем несколько раз передавать его через RPC. При передаче стаба через RPC владение им переходит к получателю, поэтому необходимо сделать dup() при каждой отправке:

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.