acquireRelease
Безопасное определение ресурсов.
Мотивация
Каждая production-система работает с ресурсами, которые нужно корректно освобождать. Типичные примеры:
┌───────────────────────────────────────────────────────────┐
│ Ресурсы в production │
├───────────────────────────────────────────────────────────┤
│ Database connections — pool.acquire() / pool.release() │
│ File handles — open() / close() │
│ HTTP connections — connect() / disconnect() │
│ Locks / Semaphores — acquire() / release() │
│ Temp files — create() / delete() │
│ Child processes — spawn() / kill() │
│ Subscriptions — subscribe() / unsubscribe() │
│ Transactions — begin() / commit() or rollback() │
│ WebSocket connections — open() / close() │
│ gRPC channels — create() / shutdown() │
└───────────────────────────────────────────────────────────┘
У всех этих ресурсов общий жизненный цикл: acquire → use → release. Effect формализует этот паттерн через два примитива.
Концепция ФП: Bracket Pattern
📖 Оба API в Effect основаны на классическом функциональном паттерне bracket, который впервые появился в Haskell:
-- Haskell bracket
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket acquire release use = ...
Bracket гарантирует три вещи:
- Acquire выполняется непрерываемо (uninterruptible) — нельзя прервать процесс на стадии частичного захвата ресурса.
- Release выполняется всегда после acquire — независимо от результата use.
- Use выполняется нормально — может быть прервано или завершиться ошибкой.
Uninterruptible Normal Guaranteed
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ ACQUIRE │─────▶│ USE │─────▶│ RELEASE │
│ │ │ │ │ │
│ Нельзя │ │ Может быть │ │ Всегда │
│ прервать │ │ прервано / │ │ выполнен │
│ │ │ упасть │ │ │
└──────────┘ └──────────────┘ └──────────┘
│ │ ▲
│ │ error/interrupt │
│ └────────────────────┘
│ release всё равно
│ вызывается
▼
Если acquire упал →
release НЕ вызывается
(ресурс не был захвачен)
acquireRelease
Определение
Effect.acquireRelease создаёт scoped-ресурс — ресурс, привязанный к scope и автоматически освобождаемый при его закрытии:
// Signature:
// Effect.acquireRelease: <A, E, R, R2>(
// acquire: Effect<A, E, R>,
// release: (a: A, exit: Exit<unknown, unknown>) => Effect<void, never, R2>
// ) => Effect<A, E, R | R2 | Scope>
const resource: Effect.Effect<MyResource, Error, Scope.Scope> =
Effect.acquireRelease(
acquireEffect, // Как получить ресурс
releaseFunction // Как освободить ресурс
)
Ключевые характеристики:
- Возвращает
Effect<A, E, Scope>— ресурс привязан к scope. - Acquire выполняется uninterruptible — защита от частичного захвата.
- Release вызывается при закрытии scope — не при завершении текущего эффекта.
- Release получает Exit — можно адаптировать поведение к результату.
Базовый пример
// Определение интерфейса ресурса
interface DatabaseConnection {
readonly id: string
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
}
// Acquire: создаём подключение
const acquireConnection = Effect.gen(function* () {
yield* Console.log("Connecting to database...")
const connection: DatabaseConnection = {
id: `conn-${Date.now()}`,
query: (sql) => Effect.succeed([{ result: sql }])
}
yield* Console.log(`Connected: ${connection.id}`)
return connection
})
// Release: закрываем подключение
const releaseConnection = (conn: DatabaseConnection) =>
Console.log(`Disconnected: ${conn.id}`)
// Scoped ресурс
// ┌─── Effect<DatabaseConnection, never, Scope>
// ▼
const dbConnection = Effect.acquireRelease(
acquireConnection,
releaseConnection
)
// Использование
const program = Effect.scoped(
Effect.gen(function* () {
const conn = yield* dbConnection
const result = yield* conn.query("SELECT * FROM users")
yield* Console.log(`Query result: ${JSON.stringify(result)}`)
return result
})
)
Effect.runPromise(program)
/*
Connecting to database...
Connected: conn-1234567890
Query result: [{"result":"SELECT * FROM users"}]
Disconnected: conn-1234567890
*/
Почему acquire — uninterruptible?
Представьте ситуацию без uninterruptible acquire:
Fiber A: acquireConnection
┌──────────────────────────────┐
│ 1. Создали TCP socket │
│ 2. Отправили handshake │
│ 3. ←── INTERRUPT ─── │ ← Прерывание между шагами!
│ 4. Получили auth token │ ← Не выполнилось
│ 5. return connection │ ← Не выполнилось
└──────────────────────────────┘
Результат: TCP socket утёк — release не вызван,
потому что acquire не завершился
Effect защищает от этого, делая весь acquire-блок непрерываемым. Прерывание будет применено только после завершения acquire (и в этом случае release будет вызван).
Release получает Exit
Функция release принимает два аргумента: ресурс A и Exit, описывающий как завершился scope:
const resource = Effect.acquireRelease(
Effect.succeed({ id: "res-1", tempFile: "/tmp/data.bin" }),
(res, exit) => {
// Всегда закрываем основной ресурс
const cleanup = Console.log(`Closing resource ${res.id}`)
// При ошибке — дополнительная очистка
const errorCleanup = Exit.isFailure(exit)
? Console.log(`Cleaning up temp file: ${res.tempFile}`)
: Effect.void
return Effect.all([cleanup, errorCleanup]).pipe(Effect.asVoid)
}
)
acquireUseRelease
Определение
Effect.acquireUseRelease — это самодостаточный вариант bracket, который не требует Scope. Он объединяет acquire, use и release в одну операцию:
// Signature:
// Effect.acquireUseRelease: <A, E, R, A2, E2, R2, R3>(
// acquire: Effect<A, E, R>,
// use: (a: A) => Effect<A2, E2, R2>,
// release: (a: A, exit: Exit<A2, E2>) => Effect<void, never, R3>
// ) => Effect<A2, E | E2, R | R2 | R3>
// ┌─── Effect<string, Error, never> ← НЕТ Scope в Requirements!
// ▼
const result = Effect.acquireUseRelease(
acquire, // Как получить ресурс
use, // Что с ним делать
release // Как освободить
)
Ключевое отличие от acquireRelease:
┌─────────────────────────────────────────────────────────────┐
│ │
│ acquireRelease acquireUseRelease │
│ ┌────────────────┐ ┌────────────────────┐ │
│ │ Требует Scope │ │ НЕ требует Scope │ │
│ │ Release при │ │ Release сразу после│ │
│ │ закрытии scope │ │ завершения use │ │
│ │ Композируемый │ │ Самодостаточный │ │
│ └────────────────┘ └────────────────────┘ │
│ │
│ Ресурс "живёт" Ресурс "живёт" │
│ пока scope открыт только на время use │
│ │
└─────────────────────────────────────────────────────────────┘
Базовый пример
// Интерфейс ресурса
interface MyResource {
readonly contents: string
readonly close: () => Promise<void>
}
// Симуляция получения ресурса
const getMyResource = (): Promise<MyResource> =>
Promise.resolve({
contents: "lorem ipsum",
close: () =>
new Promise((resolve) => {
console.log("Resource released")
resolve()
})
})
// Определяем acquire
const acquire = Effect.tryPromise({
try: () =>
getMyResource().then((res) => {
console.log("Resource acquired")
return res
}),
catch: () => new Error("getMyResourceError")
})
// Определяем release
const release = (res: MyResource) => Effect.promise(() => res.close())
// Определяем use
const use = (res: MyResource) =>
Console.log(`Content is: ${res.contents}`)
// Всё вместе — НЕ требует Scope
// ┌─── Effect<void, Error, never>
// ▼
const program = Effect.acquireUseRelease(acquire, use, release)
Effect.runPromise(program)
/*
Resource acquired
Content is: lorem ipsum
Resource released
*/
Сравнение подходов
Когда использовать acquireRelease
Используйте acquireRelease, когда ресурс нужен на протяжении жизни scope и может быть использован несколькими операциями:
// Ресурс нужен нескольким операциям
const program = Effect.scoped(
Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(
Effect.succeed({ id: "conn-1" }),
(c) => Console.log(`Closing ${c.id}`)
)
// Используем conn многократно
yield* Console.log(`Operation 1 with ${conn.id}`)
yield* Console.log(`Operation 2 with ${conn.id}`)
yield* Console.log(`Operation 3 with ${conn.id}`)
// conn освобождается при выходе из Effect.scoped
})
)
Когда использовать acquireUseRelease
Используйте acquireUseRelease, когда ресурс нужен только для одной операции и его время жизни полностью определяется этой операцией:
// Ресурс нужен для одной задачи
const readFileContents = (path: string) =>
Effect.acquireUseRelease(
// Acquire: открыть файл
Effect.sync(() => {
console.log(`Opening ${path}`)
return { path, fd: 42 }
}),
// Use: прочитать содержимое
(handle) =>
Effect.sync(() => {
console.log(`Reading ${handle.path}`)
return "file contents"
}),
// Release: закрыть файл
(handle) =>
Effect.sync(() => { console.log(`Closing ${handle.path}`) })
)
Таблица сравнения
┌──────────────────────┬──────────────────┬─────────────────────┐
│ Характеристика │ acquireRelease │ acquireUseRelease │
├──────────────────────┼──────────────────┼─────────────────────┤
│ Требует Scope │ Да │ Нет │
│ Момент release │ При закрытии │ Сразу после use │
│ │ scope │ │
│ Множественное │ Да │ Нет │
│ использование │ │ │
│ Композиция ресурсов │ Отличная │ Ограниченная │
│ Типичное применение │ DB connections, │ Одноразовое чтение, │
│ │ HTTP clients, │ кратковременные │
│ │ server sockets │ операции │
│ Интеграция с Layer │ Через Layer. │ Не интегрируется │
│ │ scoped │ напрямую │
│ Acquire │ Uninterruptible │ Uninterruptible │
│ Release гарантия │ При close scope │ После use │
└──────────────────────┴──────────────────┴─────────────────────┘
Условный release через Exit
Одна из мощнейших возможностей — адаптация release-логики в зависимости от результата:
// Паттерн: rollback при ошибке, ничего при успехе
const createBucketSafely = Effect.gen(function* () {
const s3 = yield* S3Service
return yield* Effect.acquireRelease(
s3.createBucket("my-bucket"),
(bucket, exit) =>
Exit.isFailure(exit)
? s3.deleteBucket(bucket).pipe(
Effect.tap(() =>
Console.log(`Rollback: deleted bucket ${bucket.name}`)
)
)
: Effect.void // При успехе — ничего не делаем, bucket нужен
)
})
Этот паттерн критически важен для транзакционных операций, где при ошибке нужно откатить все предыдущие шаги.
API Reference
Effect.acquireRelease [STABLE]
// Создаёт scoped-ресурс
Effect.acquireRelease<A, E, R, R2>(
acquire: Effect<A, E, R>,
release: (a: A, exit: Exit<unknown, unknown>) => Effect<void, never, R2>
): Effect<A, E, R | R2 | Scope>
Параметры:
acquire— эффект захвата ресурса (выполняется uninterruptible)release— функция освобождения, принимает ресурс и Exit
Возвращает: Effect<A, E, R | R2 | Scope> — ресурс, привязанный к scope
Effect.acquireUseRelease [STABLE]
// Самодостаточный bracket
Effect.acquireUseRelease<A, E, R, A2, E2, R2, R3>(
acquire: Effect<A, E, R>,
use: (a: A) => Effect<A2, E2, R2>,
release: (a: A, exit: Exit<A2, E2>) => Effect<void, never, R3>
): Effect<A2, E | E2, R | R2 | R3>
Параметры:
acquire— эффект захвата (uninterruptible)use— функция использования ресурсаrelease— функция освобождения
Возвращает: Effect<A2, E | E2, R | R2 | R3> — без Scope в Requirements
Effect.ensuring [STABLE]
Упрощённый вариант — гарантирует выполнение эффекта при любом исходе, но без доступа к ресурсу:
// Финализатор без ресурса
const program = Effect.succeed(42).pipe(
Effect.ensuring(Console.log("Always runs"))
)
Паттерн: Транзакционная цепочка операций
Один из самых важных production-паттернов — цепочка зависимых операций с полным rollback при любой ошибке. Рассмотрим создание “Workspace”, включающего S3 bucket, ElasticSearch index и запись в БД.
Определение сервисов
// Tagged errors для каждого сервиса
class S3Error extends Data.TaggedError("S3Error")<{
readonly message: string
}> {}
class ElasticSearchError extends Data.TaggedError("ElasticSearchError")<{
readonly message: string
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly message: string
}> {}
// Типы ресурсов
interface Bucket {
readonly name: string
}
interface Index {
readonly id: string
}
interface Entry {
readonly id: string
}
// Сервисы
class S3 extends Context.Tag("S3")<
S3,
{
readonly createBucket: Effect.Effect<Bucket, S3Error>
readonly deleteBucket: (bucket: Bucket) => Effect.Effect<void>
}
>() {}
class ElasticSearch extends Context.Tag("ElasticSearch")<
ElasticSearch,
{
readonly createIndex: Effect.Effect<Index, ElasticSearchError>
readonly deleteIndex: (index: Index) => Effect.Effect<void>
}
>() {}
class Database extends Context.Tag("Database")<
Database,
{
readonly createEntry: (
bucket: Bucket,
index: Index
) => Effect.Effect<Entry, DatabaseError>
readonly deleteEntry: (entry: Entry) => Effect.Effect<void>
}
>() {}
Транзакционные операции с rollback
// Каждая операция использует acquireRelease с условным release
// Создание bucket с rollback при ошибке
const createBucket = Effect.gen(function* () {
const { createBucket, deleteBucket } = yield* S3
return yield* Effect.acquireRelease(
createBucket,
(bucket, exit) =>
Exit.isFailure(exit) ? deleteBucket(bucket) : Effect.void
)
})
// Создание index с rollback при ошибке
const createIndex = Effect.gen(function* () {
const { createIndex, deleteIndex } = yield* ElasticSearch
return yield* Effect.acquireRelease(
createIndex,
(index, exit) =>
Exit.isFailure(exit) ? deleteIndex(index) : Effect.void
)
})
// Создание записи с rollback при ошибке
const createEntry = (bucket: Bucket, index: Index) =>
Effect.gen(function* () {
const { createEntry, deleteEntry } = yield* Database
return yield* Effect.acquireRelease(
createEntry(bucket, index),
(entry, exit) =>
Exit.isFailure(exit) ? deleteEntry(entry) : Effect.void
)
})
// Транзакционная композиция
const createWorkspace = Effect.scoped(
Effect.gen(function* () {
const bucket = yield* createBucket
const index = yield* createIndex
const entry = yield* createEntry(bucket, index)
return { bucket, index, entry }
})
)
Как работает rollback
Успех: Все операции выполнены
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Create │──▶│ Create │──▶│ Create │──▶ ✅ Success
│ S3 Bucket│ │ ES Index │ │ DB Entry │
└──────────┘ └──────────┘ └──────────┘
Ошибка на шаге 3: Rollback шагов 2 и 1
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Create │──▶│ Create │──▶│ Create │──▶ ❌ DatabaseError
│ S3 Bucket│ │ ES Index │ │ DB Entry │
└──────────┘ └──────────┘ └──────────┘
▲ ▲
│ │
┌──────────┐ ┌──────────┐
│ Delete │◀──│ Delete │ Rollback в LIFO-порядке
│ S3 Bucket│ │ ES Index │
└──────────┘ └──────────┘
Ошибка на шаге 2: Rollback только шага 1
┌──────────┐ ┌──────────┐
│ Create │──▶│ Create │──▶ ❌ ElasticSearchError
│ S3 Bucket│ │ ES Index │
└──────────┘ └──────────┘
▲
│
┌──────────┐
│ Delete │ Rollback только S3
│ S3 Bucket│
└──────────┘
Примеры
Пример 1: HTTP-клиент с connection pool
interface HttpClient {
readonly baseUrl: string
readonly get: (path: string) => Effect.Effect<string>
}
const makeHttpClient = (baseUrl: string) =>
Effect.acquireRelease(
// Acquire: создаём клиент
Effect.gen(function* () {
yield* Console.log(`Creating HTTP client for ${baseUrl}`)
const client: HttpClient = {
baseUrl,
get: (path) => Effect.succeed(`Response from ${baseUrl}${path}`)
}
return client
}),
// Release: закрываем клиент
(client) => Console.log(`Closing HTTP client for ${client.baseUrl}`)
)
const program = Effect.scoped(
Effect.gen(function* () {
const client = yield* makeHttpClient("https://api.example.com")
const users = yield* client.get("/users")
const posts = yield* client.get("/posts")
yield* Console.log(`Users: ${users}`)
yield* Console.log(`Posts: ${posts}`)
})
)
Effect.runPromise(program)
/*
Creating HTTP client for https://api.example.com
Users: Response from https://api.example.com/users
Posts: Response from https://api.example.com/posts
Closing HTTP client for https://api.example.com
*/
Пример 2: Чтение файла через acquireUseRelease
interface FileHandle {
readonly path: string
readonly fd: number
}
const readFile = (path: string): Effect.Effect<string, Error> =>
Effect.acquireUseRelease(
// Acquire
Effect.sync(() => {
console.log(`Opening file: ${path}`)
return { path, fd: Math.floor(Math.random() * 1000) } as FileHandle
}),
// Use
(handle) =>
Effect.sync(() => {
console.log(`Reading file: ${handle.path} (fd=${handle.fd})`)
return `Contents of ${handle.path}`
}),
// Release
(handle) =>
Effect.sync(() => {
console.log(`Closing file: ${handle.path} (fd=${handle.fd})`)
})
)
const program = Effect.gen(function* () {
const content = yield* readFile("/etc/config.json")
yield* Console.log(`Got: ${content}`)
})
Effect.runPromise(program)
/*
Opening file: /etc/config.json
Reading file: /etc/config.json (fd=42)
Closing file: /etc/config.json (fd=42)
Got: Contents of /etc/config.json
*/
Пример 3: Несколько ресурсов в одном scope
const makeResource = (name: string) =>
Effect.acquireRelease(
Console.log(`Acquire: ${name}`).pipe(Effect.as(name)),
(n) => Console.log(`Release: ${n}`)
)
const program = Effect.scoped(
Effect.gen(function* () {
const db = yield* makeResource("Database")
const cache = yield* makeResource("Cache")
const queue = yield* makeResource("MessageQueue")
yield* Console.log(`Using: ${db}, ${cache}, ${queue}`)
})
)
Effect.runPromise(program)
/*
Acquire: Database
Acquire: Cache
Acquire: MessageQueue
Using: Database, Cache, MessageQueue
Release: MessageQueue ← LIFO: последний — первый
Release: Cache
Release: Database
*/
Пример 4: Release с обработкой ошибки в acquire
// Если acquire падает — release НЕ вызывается
const program = Effect.scoped(
Effect.gen(function* () {
yield* Console.log("Before acquire")
yield* Effect.acquireRelease(
Effect.fail("Acquire failed!"), // ❌ Acquire падает
() => Console.log("This will NEVER run") // Не вызовется
)
})
)
Effect.runPromiseExit(program).then(console.log)
/*
Before acquire
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Acquire failed!' }
}
// "This will NEVER run" — не выведено
*/
Распространённые ошибки
Ошибка 1: Эффект в release, который может упасть
// ❌ Release не должен возвращать ошибки в канале E
// (тип release: Effect<void, never, R>)
const bad = Effect.acquireRelease(
Effect.succeed("resource"),
// @ts-expect-error — release должен быть Effect<void, never, R>
(_res) => Effect.fail("release error")
)
// ✅ Правильно: обработать ошибку внутри release
const good = Effect.acquireRelease(
Effect.succeed("resource"),
(_res) =>
Effect.gen(function* () {
const result = yield* Effect.try(() => closeConnection())
// Логируем ошибку, но не пробрасываем
}).pipe(
Effect.catchAll((e) => Effect.log(`Release error: ${e}`))
)
)
Ошибка 2: Не обернуть acquireRelease в scoped
const resource = Effect.acquireRelease(
Effect.succeed("data"),
() => Console.log("released")
)
// ❌ Не скомпилируется — Scope не предоставлен
// Effect.runPromise(resource)
// ✅ Обернуть в Effect.scoped
Effect.runPromise(Effect.scoped(resource))
Ошибка 3: Путать acquireRelease и acquireUseRelease
// ❌ Используем acquireUseRelease, когда ресурс нужен за пределами use
const bad = Effect.acquireUseRelease(
Effect.succeed({ id: "conn-1" }),
(conn) => Effect.succeed(conn), // Возвращаем conn, но он уже закрыт!
(conn) => Console.log(`Closed ${conn.id}`)
)
// ✅ Используем acquireRelease, если ресурс нужен дольше
const good = Effect.acquireRelease(
Effect.succeed({ id: "conn-1" }),
(conn) => Console.log(`Closed ${conn.id}`)
)
Упражнения
Упражнение 1: Простой ресурс
Создайте acquireRelease для “ресурса” — таймера, который при acquire выводит “Timer started”, а при release — “Timer stopped with duration: Xms”.
import { Effect, Console } from "effect"
const timer = Effect.acquireRelease(
Effect.sync(() => {
const start = Date.now()
console.log("Timer started")
return start
}),
(start) =>
Effect.sync(() => {
const duration = Date.now() - start
console.log(`Timer stopped with duration: ${duration}ms`)
})
)
const program = Effect.scoped(
Effect.gen(function* () {
const start = yield* timer
yield* Effect.sleep("100 millis")
yield* Console.log("Doing work...")
})
)
Effect.runPromise(program)import { Effect, Console } from "effect"
const timer = Effect.acquireRelease(
Effect.sync(() => {
const start = Date.now()
console.log("Timer started")
return start
}),
(start) =>
Effect.sync(() => {
const duration = Date.now() - start
console.log(`Timer stopped with duration: ${duration}ms`)
})
)
const program = Effect.scoped(
Effect.gen(function* () {
const start = yield* timer
yield* Effect.sleep("100 millis")
yield* Console.log("Doing work...")
})
)
Effect.runPromise(program)Упражнение 2: acquireUseRelease для файла
Используйте acquireUseRelease для симуляции чтения файла: acquire открывает файл, use читает содержимое, release закрывает.
import { Effect } from "effect"
interface FileHandle {
readonly path: string
readonly content: string
}
const readFile = (path: string) =>
Effect.acquireUseRelease(
Effect.sync((): FileHandle => {
console.log(`Open: ${path}`)
return { path, content: `data from ${path}` }
}),
(handle) =>
Effect.sync(() => {
console.log(`Read: ${handle.path}`)
return handle.content
}),
(handle) =>
Effect.sync(() => {
console.log(`Close: ${handle.path}`)
})
)
Effect.runPromise(readFile("/etc/hosts")).then(console.log)
// Open: /etc/hosts
// Read: /etc/hosts
// Close: /etc/hosts
// data from /etc/hostsimport { Effect } from "effect"
interface FileHandle {
readonly path: string
readonly content: string
}
const readFile = (path: string) =>
Effect.acquireUseRelease(
Effect.sync((): FileHandle => {
console.log(`Open: ${path}`)
return { path, content: `data from ${path}` }
}),
(handle) =>
Effect.sync(() => {
console.log(`Read: ${handle.path}`)
return handle.content
}),
(handle) =>
Effect.sync(() => {
console.log(`Close: ${handle.path}`)
})
)
Effect.runPromise(readFile("/etc/hosts")).then(console.log)
// Open: /etc/hosts
// Read: /etc/hosts
// Close: /etc/hosts
// data from /etc/hostsУпражнение 3: Множественные ресурсы с порядком release
Создайте три ресурса (Logger, Database, Cache) с помощью acquireRelease. Убедитесь, что при ошибке после создания всех трёх — все три корректно освобождаются в LIFO-порядке.
import { Effect, Console } from "effect"
const makeNamedResource = (name: string) =>
Effect.acquireRelease(
Console.log(`[${name}] Acquired`).pipe(Effect.as(name)),
(n) => Console.log(`[${n}] Released`)
)
const program = Effect.scoped(
Effect.gen(function* () {
yield* makeNamedResource("Logger")
yield* makeNamedResource("Database")
yield* makeNamedResource("Cache")
// Ошибка после захвата всех ресурсов
return yield* Effect.fail("Application error")
})
)
Effect.runPromiseExit(program).then(console.log)
/*
[Logger] Acquired
[Database] Acquired
[Cache] Acquired
[Cache] Released
[Database] Released
[Logger] Released
{ _tag: 'Failure', cause: { _tag: 'Fail', failure: 'Application error' } }
*/import { Effect, Console } from "effect"
const makeNamedResource = (name: string) =>
Effect.acquireRelease(
Console.log(`[${name}] Acquired`).pipe(Effect.as(name)),
(n) => Console.log(`[${n}] Released`)
)
const program = Effect.scoped(
Effect.gen(function* () {
yield* makeNamedResource("Logger")
yield* makeNamedResource("Database")
yield* makeNamedResource("Cache")
// Ошибка после захвата всех ресурсов
return yield* Effect.fail("Application error")
})
)
Effect.runPromiseExit(program).then(console.log)
/*
[Logger] Acquired
[Database] Acquired
[Cache] Acquired
[Cache] Released
[Database] Released
[Logger] Released
{ _tag: 'Failure', cause: { _tag: 'Fail', failure: 'Application error' } }
*/Упражнение 4: Транзакционная цепочка с условным rollback
Реализуйте создание “инфраструктуры проекта”: Kubernetes Namespace → Helm Chart → DNS Record. При ошибке на любом шаге все предыдущие шаги должны быть откачены. При успехе — ресурсы остаются.
import { Effect, Console, Exit, Data } from "effect"
class InfraError extends Data.TaggedError("InfraError")<{
readonly step: string
readonly reason: string
}> {}
const createNamespace = (name: string) =>
Effect.acquireRelease(
Console.log(`[K8s] Creating namespace: ${name}`).pipe(
Effect.as({ name })
),
(ns, exit) =>
Exit.isFailure(exit)
? Console.log(`[K8s] Rollback: deleting namespace ${ns.name}`)
: Effect.void
)
const deployHelmChart = (namespace: string, chart: string) =>
Effect.acquireRelease(
Console.log(`[Helm] Deploying ${chart} to ${namespace}`).pipe(
Effect.as({ chart, namespace })
),
(release, exit) =>
Exit.isFailure(exit)
? Console.log(`[Helm] Rollback: uninstalling ${release.chart}`)
: Effect.void
)
const createDnsRecord = (domain: string, shouldFail: boolean) =>
Effect.acquireRelease(
shouldFail
? Effect.fail(new InfraError({ step: "DNS", reason: "Zone not found" }))
: Console.log(`[DNS] Creating record: ${domain}`).pipe(
Effect.as({ domain })
),
(record, exit) =>
Exit.isFailure(exit)
? Console.log(`[DNS] Rollback: deleting record ${record.domain}`)
: Effect.void
)
const deployProject = (shouldFailDns: boolean) =>
Effect.scoped(
Effect.gen(function* () {
const ns = yield* createNamespace("my-project")
const helm = yield* deployHelmChart(ns.name, "my-app")
const dns = yield* createDnsRecord("app.example.com", shouldFailDns)
return { namespace: ns, helm, dns }
})
)
// Тест: успех
Effect.runPromiseExit(deployProject(false)).then(console.log)
// [K8s] Creating namespace: my-project
// [Helm] Deploying my-app to my-project
// [DNS] Creating record: app.example.com
// { _tag: 'Success', ... }
// Тест: ошибка на DNS
Effect.runPromiseExit(deployProject(true)).then(console.log)
// [K8s] Creating namespace: my-project
// [Helm] Deploying my-app to my-project
// [Helm] Rollback: uninstalling my-app
// [K8s] Rollback: deleting namespace my-project
// { _tag: 'Failure', cause: { _tag: 'Fail', failure: InfraError } }import { Effect, Console, Exit, Data } from "effect"
class InfraError extends Data.TaggedError("InfraError")<{
readonly step: string
readonly reason: string
}> {}
const createNamespace = (name: string) =>
Effect.acquireRelease(
Console.log(`[K8s] Creating namespace: ${name}`).pipe(
Effect.as({ name })
),
(ns, exit) =>
Exit.isFailure(exit)
? Console.log(`[K8s] Rollback: deleting namespace ${ns.name}`)
: Effect.void
)
const deployHelmChart = (namespace: string, chart: string) =>
Effect.acquireRelease(
Console.log(`[Helm] Deploying ${chart} to ${namespace}`).pipe(
Effect.as({ chart, namespace })
),
(release, exit) =>
Exit.isFailure(exit)
? Console.log(`[Helm] Rollback: uninstalling ${release.chart}`)
: Effect.void
)
const createDnsRecord = (domain: string, shouldFail: boolean) =>
Effect.acquireRelease(
shouldFail
? Effect.fail(new InfraError({ step: "DNS", reason: "Zone not found" }))
: Console.log(`[DNS] Creating record: ${domain}`).pipe(
Effect.as({ domain })
),
(record, exit) =>
Exit.isFailure(exit)
? Console.log(`[DNS] Rollback: deleting record ${record.domain}`)
: Effect.void
)
const deployProject = (shouldFailDns: boolean) =>
Effect.scoped(
Effect.gen(function* () {
const ns = yield* createNamespace("my-project")
const helm = yield* deployHelmChart(ns.name, "my-app")
const dns = yield* createDnsRecord("app.example.com", shouldFailDns)
return { namespace: ns, helm, dns }
})
)
// Тест: успех
Effect.runPromiseExit(deployProject(false)).then(console.log)
// [K8s] Creating namespace: my-project
// [Helm] Deploying my-app to my-project
// [DNS] Creating record: app.example.com
// { _tag: 'Success', ... }
// Тест: ошибка на DNS
Effect.runPromiseExit(deployProject(true)).then(console.log)
// [K8s] Creating namespace: my-project
// [Helm] Deploying my-app to my-project
// [Helm] Rollback: uninstalling my-app
// [K8s] Rollback: deleting namespace my-project
// { _tag: 'Failure', cause: { _tag: 'Fail', failure: InfraError } }