1. Навіщо взагалі думати про «стійкість» у ChatGPT App
У звичайному вебзастосунку користувач принаймні бачить URL, індикатор завантаження (спінер) браузера та може оновити сторінку. У ChatGPT користувач бачить один екран: чат і ваш застосунок. Якщо щось гальмує, він не розуміє, хто винен, — OpenAI, ваш Gateway, платіжний сервіс чи якийсь інший мікросервіс аналітики. Для нього все це — «ChatGPT + ваш застосунок».
Коли tool-call зависає на 30–60 секунд, модель чекає, чекає… і в найкращому разі вибачається за затримку. У гіршому — галюцинує відповідь замість даних від вашого бекенду. Тому стійкість — це не лише про SRE і доступність, а й про якість відповіді, тон поведінки моделі та метрики в Store.
В екосистемі ChatGPT App є кілька незалежних контурів:
- ChatGPT ↔ MCP Gateway.
- Gateway ↔ ваші бекенд-/REST-сервіси (Gift REST API, Commerce REST API, Analytics Service тощо).
- Ваші сервіси ↔ зовнішні API (LLM, платежі, каталоги).
- Вхідні вебхуки (ACP, Stripe, будь-які інтеграції) ↔ ваші обробники.
Проблема в тому, що збій в одному місці може спричинити каскад. Gateway чесно чекає на завислий сервіс, воркери заповнюються, зʼєднання вичерпуються, клієнти починають робити повторні спроби — і за кілька хвилин у вас класичний «системний біль»: усе горить і тоне одночасно. Саме від цього захищають чотири патерни (шаблони), про які ми сьогодні говоримо:
- Timeouts — ми ніколи не чекаємо вічно.
- Circuit breaker — ми не бʼємося в зачинені двері.
- Bulkheads — ми будуємо «відсіки» й не даємо потонути всьому кораблю.
- Захист від штормів вебхуків — ми визнаємо, що вебхуки приходять із дублікатами, сплесками та повторними спробами, і готуємося до цього.
2. Timeouts: ми не чекаємо вічно
Що таке тайм-аут (timeout) і чому без нього все погано
Timeout — це максимальний час, який ваш код готовий чекати на відповідь від залежності: бази даних, MCP-сервера, зовнішнього HTTP API, моделі. Якщо відповідь не прийшла за заданий час, ми вважаємо виклик невдалим, звільняємо ресурси й повертаємо зрозумілу помилку або fallback.
Без тайм-аутів запити можуть:
- нескінченно висіти в очікуванні,
- займати зʼєднання та пул потоків,
- блокувати наступні запити,
- спричиняти каскадні відмови.
Патерн простий: «краще передбачувана відмова через 3–5 секунд, ніж незрозуміла тиша 5 хвилин».
Варто памʼятати, що тайм-аути є на кількох рівнях:
- на рівні проксі/балансувальника (Cloudflare, Nginx),
- на рівні MCP Gateway (HTTP-клієнти до мікросервісів),
- у самих сервісах (виклики до БД, зовнішніх API, LLM).
Для ChatGPT загалом варто прагнути, щоб повний час tool-call був у діапазоні 5–10 секунд для звичайних операцій і максимум 20–30 секунд для особливо важких. Усе, що довше, — майже гарантовано поганий користувацький досвід.
Простий fetchWithTimeout у TypeScript
Почнемо з практики. У GiftGenius MCP Gateway є допоміжний HTTP-клієнт, який звертається до сервісу підбору подарунків, до commerce-сервісу та до аналітики. Обгорнемо стандартний fetch у функцію з тайм-аутом:
// src/gateway/httpClient.ts
export async function fetchWithTimeout(
url: string,
opts: RequestInit & { timeoutMs?: number } = {}
) {
const { timeoutMs = 5000, ...rest } = opts;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...rest, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
Тепер у коді Gateway ми не робимо «голий» fetch, а використовуємо лише цю допоміжну функцію:
// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";
export async function callGiftService(path: string) {
const res = await fetchWithTimeout(
process.env.GIFT_SERVICE_URL + path,
{ timeoutMs: 4000 }
);
if (!res.ok) {
throw new Error(`gift_service_${res.status}`);
}
return res.json();
}
Такий підхід гарантує: навіть якщо gift-сервіс зависне, за 4 секунди ми обірвемо зʼєднання й зможемо віддати MCP-помилку в ChatGPT, а не тримати зʼєднання до останнього.
Де саме ставити тайм-аути в GiftGenius
У нашому прикладі GiftGenius:
- На рівні Gateway: тайм-аути на виклики Gift REST API, Commerce REST API, Analytics Service / REST API.
- Усередині цих сервісів: тайм-аути на виклики до БД, ACP/платіжних сервісів, зовнішніх рекомендаційних API.
- На вході в Gateway: загальний тайм-аут запиту від ChatGPT, щоб tool-call не перетворювався на «вічний спінер».
Важливо, щоб час очікування на верхньому рівні був трохи більший, ніж на внутрішніх. Наприклад, якщо Gateway чекає бекенд 5 секунд, а бекенд чекає БД 3 секунди, у нас залишається запас на обробку й серіалізацію результату.
Як пояснювати тайм-аути моделі ChatGPT
Для ChatGPT важливо повертати семантичні помилки, а не мовчки рвати зʼєднання. Замість абстрактного 500 краще повернути структуровану MCP-помилку, яку модель зможе озвучити користувачеві: «Сервіс підбору подарунків зараз перевантажений, спробуйте ще раз трохи пізніше» — і так далі.
Це означає, що в Gateway під час тайм-ауту треба:
- Спіймати AbortError або наш timeout_….
- Сформувати MCP-відповідь зі змістовним кодом і коротким описом.
- Дати моделі можливість вирішити, як пояснити це людині.
Тайм-аути розвʼязують проблему завислих запитів. Але якщо залежність почала масово падати, вони не врятують від лавини однакових невдалих спроб. Тут потрібен наступний рівень захисту — circuit breaker.
3. Circuit breaker: «автомат» проти сервісів, які «вмирають»
Інтуїція: чому одного тайм-ауту замало
Ми вже навчилися обмежувати час очікування окремих викликів за допомогою тайм-аутів. Тайм-аут захищає один конкретний виклик. Але якщо залежність остаточно «померла» (наприклад, commerce-сервіс падає через OOM (Out Of Memory) на кожному запиті), ми все одно продовжимо до неї звертатися: щоразу чекатимемо 3–5 секунд, ловитимемо помилку, навантажуватимемо мережу й CPU — і знову чекатимемо.
Circuit breaker (автомат) додає «памʼять»: він відстежує помилки й тайм-аути та, коли їх стає надто багато, перестає взагалі надсилати запити до цього сервісу. Натомість він одразу повертає швидку відмову або fallback. А через певний час обережно пробує знову в режимі half-open.
Класичні стани автомата:
- Closed — усе нормально, запити проходять.
- Open — сервіс вважається «мертвим», запити не йдуть, одразу повертається помилка.
- Half-open — пробуємо обмежену кількість запитів; якщо вони успішні — повертаємося до closed, якщо знову впали — знову до open.
Проста схема circuit breaker
Невелика діаграма:
stateDiagram-v2
[*] --> Closed
Closed --> Open: занадто багато помилок
Open --> HalfOpen: минув cooldown
HalfOpen --> Closed: кілька успіхів поспіль
HalfOpen --> Open: знову помилки
Open --> Open: швидка відмова
Міні-реалізація circuit breaker у TypeScript
У реальних проєктах зазвичай використовують готові бібліотеки (для Node.js є, наприклад, opossum або легкі саморобні рішення). Але щоб зрозуміти механіку, достатньо компактного класу.
Дуже спрощений приклад breaker навколо виклику commerce-модуля:
// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";
export class CircuitBreaker {
private state: State = "closed";
private failureCount = 0;
private nextAttemptAt = 0;
constructor(
private readonly failureThreshold = 5,
private readonly cooldownMs = 30_000
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
const now = Date.now();
if (this.state === "open") {
if (now < this.nextAttemptAt) {
throw new Error("circuit_open");
}
this.state = "half-open";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "closed";
}
private onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
this.nextAttemptAt = Date.now() + this.cooldownMs;
}
}
}
І використання в клієнті commerce-сервісу:
// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);
export async function callCommerce(path: string) {
return commerceBreaker.call(async () => {
const res = await fetchWithTimeout(
process.env.COMMERCE_URL + path,
{ timeoutMs: 3000 }
);
if (!res.ok) throw new Error(`commerce_${res.status}`);
return res.json();
});
}
Тут, коли commerce починає масово відповідати помилками або не встигає до тайм-ауту, після кількох невдач breaker переходить у open. У цьому стані протягом cooldownMs ми взагалі не намагаємося звертатися до сервісу й одразу повертаємо помилку circuit_open.
Що має бачити ChatGPT, коли breaker «відключив» сервіс
З точки зору ChatGPT краще, якщо ви:
- Швидко відповідаєте MCP-помилкою «commerce_unavailable» або «gift_service_overloaded».
- Додаєте зрозумілий опис: «Сервіс оплати тимчасово недоступний, давайте спробуємо пізніше».
- Не ховаєте помилку за нескінченними повторними спробами.
Це якраз той випадок, коли «швидка, чесна відмова» краща за довге підвисання. Особливо під час checkout (оформлення замовлення): користувач швидше переживе зрозуміле повідомлення, ніж 40 секунд дивитиметься на спінер і отримає «щось пішло не так».
Тайм-аути й breaker захищають нас від «поганих» або недоступних залежностей. Але вони не розвʼязують проблему, коли один тип навантаження зʼїдає всі ресурси й починає душити інші частини системи. Для цього потрібен ще один шар — bulkheads.
4. Bulkheads: ізоляція «відсіків», щоб один не потопив увесь корабель
Аналогія з кораблем
Патерн bulkhead названий на честь перебірок у кораблі: якщо в одному відсіку пробоїна, вода не розтечеться по всьому кораблю. В архітектурі це означає: розділити ресурси між різними напрямами роботи, щоб один перевантажений сервіс не «зʼїв» усе — CPU, зʼєднання, пули — і не зламав критичні шляхи.
У мікросервісах це зазвичай роблять через окремі:
- пули HTTP-зʼєднань,
- пули потоків/воркерів,
- черги/топіки,
- навіть окремі БД-кластери для критично важливих операцій.
Ідея проста: якщо сервіс рекомендацій подарунків починає працювати повільніше й підгальмовувати, він вичерпає лише свої ресурси, але не зламає checkout і авторизацію.
Bulkheads у світі Node.js і MCP Gateway
У Node.js немає потоків у класичному сенсі (є event loop і воркери), але ми можемо обмежувати кількість паралельних завдань для кожного напряму.
Приклад: у Gateway є три зовнішні залежності:
- Gift-сервіс (підбір подарунків, важкі LLM-виклики).
- Commerce-сервіс (checkout, ACP).
- Analytics-сервіс (логування подій).
Ми можемо запровадити прості ліміти на одночасні запити до кожного з них.
Наприклад, невеликий «семафор» для обмеження паралельності:
// src/gateway/bulkhead.ts
export class Bulkhead {
private active = 0;
private queue: (() => void)[] = [];
constructor(private readonly maxConcurrent: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.active >= this.maxConcurrent) {
await new Promise<void>((resolve) => this.queue.push(resolve));
}
this.active++;
try {
return await fn();
} finally {
this.active--;
const next = this.queue.shift();
if (next) next();
}
}
}
І використання для сервісів:
// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";
const giftBulkhead = new Bulkhead(10); // до 10 паралельних
const commerceBulkhead = new Bulkhead(3); // чекаут суттєво обмежений
const analyticsBulkhead = new Bulkhead(50); // можна багато
export async function callGiftWithBulkhead(fn: () => Promise<any>) {
return giftBulkhead.run(fn);
}
export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
return commerceBulkhead.run(fn);
}
Таким чином, навіть якщо GPT масово попросить «зроби мені 30 складних підборів подарунків», вони виконуватимуться максимум по 10 одночасно. А checkout зможе продовжувати працювати, використовуючи свій окремий ліміт.
GiftGenius: які «відсіки» ми хочемо
У GiftGenius розумно зробити окремі «відсіки» для:
- підбору подарунків (LLM-важкі, менш критичні — їх можна сповільнювати),
- checkout/ACP (дуже критично, тож треба захищати максимально),
- аналітики/логів (важливо, але затримки можна певний час терпіти).
У більш просунутій архітектурі ви ще й розгортаєте їх як різні кластери з окремими ресурсами. Але в межах цієї лекції важлива саме ідея: не дозволяти другорядним фічам «зʼїсти» весь кисень.
Усі три ці патерни — тайм-аути, circuit breaker і bulkheads — стосуються того, як ви ходите назовні, до своїх залежностей. Але є ще один клас загроз стійкості: вхідні потоки подій, які можуть «завалити» вас навіть за ідеально налаштованих вихідних викликів. Найтиповіший приклад — шторми вебхуків.
5. Шторми вебхуків: коли світ надсилає вам події частіше, ніж ви готові
Як поводяться вебхуки в реальності
Четверте джерело проблем зі стійкістю — вхідні події: вебхуки від ACP, Stripe та інших систем. Саме вони можуть влаштувати справжній «шторм», навіть якщо у вас уже налаштовані тайм-аути, circuit breaker-и та bulkhead-и.
Вебхуки — це не HTTP-запит «на вимогу», а push-події від зовнішніх систем (Stripe, ACP, зовнішні магазини тощо). Вони мають кілька неприємних властивостей:
- Доставка щонайменше один раз (at-least-once) — отже, дублікати неминучі.
- Порядок доставки не гарантується.
- У разі помилок вони люблять робити повторні спроби: спочатку через секунду, потім через 10 секунд, потім через хвилину… доки ви не відповісте 2xx.
- У піку (наприклад, на розпродажі) вони приходять пачками, створюючи «шторм».
Якщо ваш обробник неідемпотентний і працює надто довго, він стає вузьким місцем: уся черга забивається, а повторні спроби лише підсилюють шторм. У результаті ви можете «покласти» базу, чергу, пул воркерів — а за ними, ланцюжком, і решту системи.
Базові принципи захисту від штормів
Є кілька ідей, які помітно підвищують шанси «пережити» шторм:
По-перше, queue-first, process-later. В ідеалі вхідний вебхук не має синхронно виконувати важку роботу. Замість цього він якнайшвидше перевіряє підпис/формат, кладе завдання в чергу й відповідає 200 OK. Обробка йде асинхронно у воркері. Якщо вам потрібне «швидке підтвердження» для ChatGPT, ви можете тримати окремий контур сповіщень.
По-друге, ідемпотентність обробника. Повторний вебхук за тією самою операцією не має «створити замовлення ще раз» або «списати гроші двічі». Зазвичай це розвʼязують зберіганням ключа ідемпотентності або eventId і перевіркою, чи обробляли ми вже цю подію.
По-третє, rate limiting і circuit breaker на стороні приймача. Навіть якщо відправник «штормить», ви можете:
- обмежити RPS за IP/підпискою/кінцевою точкою (endpoint),
- тимчасово віддавати 429 або 503, щоб уповільнити повторні спроби,
- використовувати breaker, щоб не «лити» потік у зламаний downstream (наприклад, у БД замовлень).
Приклад Next.js-обробника вебхука в GiftGenius
Припустімо, що в нас є ACP/платіжний сервіс, який надсилає вебхук про статус замовлення в POST /api/commerce/webhook. Ми хочемо:
- швидко прийняти подію й покласти її в чергу,
- не обробляти її синхронно,
- не зламатися через дублікати.
Спрощений приклад (без перевірки підпису й реальної черги — це буде в модулях про безпеку та черги):
// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
// Тут у нас могла б бути Redis/черга, поки імітуємо масив
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // ідемпотентність (для демо)
export async function POST(req: NextRequest) {
const event = await req.json();
const eventId = event.id as string;
if (processedEvents.has(eventId)) {
return NextResponse.json({ ok: true, duplicate: true });
}
// В реальності тут буде перевірка підпису і схеми
inMemoryQueue.push(event); // кладемо в чергу для фонової обробки
// Фоновий воркер пізніше обробить і позначить ID як оброблений
return NextResponse.json({ ok: true });
}
Поки це псевдореалізація, але важливі два моменти:
- Синхронна частина має бути максимально легкою.
- Ми закладаємо ідемпотентність довкола event.id.
У реальному житті ви будете:
- використовувати зовнішню чергу (SQS, RabbitMQ, Kafka),
- зберігати оброблені події в БД,
- перевіряти підпис вебхука й версію payload-у,
- можливо, застосовувати окремий Bulkhead/Breaker довкола обробника.
Як це виглядає в контексті GiftGenius
Для GiftGenius, інтегрованого з ACP/Stripe через вебхуки, захист від штормів особливо важливий у пікові сезони (Новий рік, Чорна пʼятниця). У ці періоди подій багато:
- створення intent-ів,
- підтвердження платежів,
- скасування,
- повернення.
Якщо ваш обробник почне «розростатися» (наприклад, через запити до зовнішнього API), ви ризикуєте тим, що:
- ACP почне робити повторні спроби,
- події прийдуть пачками,
- БД замовлень і пул воркерів будуть забиті.
Патерн «queue first» + ідемпотентність + rate limiting на вході якраз і слугує страховкою від таких сценаріїв.
6. Як ці патерни працюють разом
Тепер зберемо всі ці патерни в один сценарій і подивімося, як вони працюють у реальному ланцюжку «Підбери подарунок і одразу оформи замовлення».
Розглянемо ланцюжок «ChatGPT → Gateway → Gift Service → Commerce → вебхуки» на прикладі сценарію:
Користувач у чаті каже: «Підбери подарунок і одразу оформи замовлення».
- Модель вирішує викликати ваш tool suggest_and_checkout.
- Gateway викликає gift-сервіс через fetchWithTimeout і bulkhead gift-сервісу.
- Якщо gift-сервіс завис, спрацьовує тайм-аут; breaker довкола нього після певної кількості помилок перейде в open, і наступні запити одразу отримуватимуть MCP-помилку «gift_service_unavailable».
- Якщо gift-сервіс відповідає, Gateway викликає commerce-сервіс (знову з тайм-аутом і окремим bulkhead).
- Будь-які проблеми з commerce викликають окремий circuit breaker, налаштований суворіше, ніж у gift (бо checkout критичний).
- Успішне замовлення призводить до вебхука від ACP у ваш /api/commerce/webhook, який кладе подію в чергу й швидко відповідає; фонові воркери обробляють оплату, а повторні вебхуки з тим самим eventId ігноруються як дублікати.
У підсумку:
- Сервіс підбору, що зависає, не «кладе» checkout.
- Commerce, що зависає, не перетворює всі tool-calls на хвилинний спінер — ChatGPT швидко отримує змістовну помилку.
- Шторми вебхуків не ламають ваш основний HTTP-контур.
- Ви контролюєте точки деградації: краще тимчасово вимкнути персоналізовані рекомендації, ніж «звалити» оплату.
7. Невеликий практичний чекліст для вашого App (у розповідній формі)
Якщо узагальнити, у типовому ChatGPT App з MCP/Gateway має сенс послідовно пройтися такими питаннями.
Спочатку перевірте, чи є тайм-аути на всіх зовнішніх викликах. Увесь код fetch, запити до БД і до LLM мають використовувати обгортку на кшталт fetchWithTimeout з прийнятними значеннями. Важливо, щоб не було місць, де запит може висіти нескінченно.
Далі визначте найкрихкіші залежності. Як правило, це платіжні сервіси, ACP, великі зовнішні API й іноді ваша ж БД замовлень. Навколо них варто додати circuit breaker, щоб захиститися від лавини повторних запитів у вже «мертвий» сервіс. Водночас одразу вирішіть, як ChatGPT поводитиметься, коли breaker у стані open.
Після цього подивіться на ресурси як на «відсіки». Чи все у вас іде через один connection pool і один пул воркерів? Чи критичні операції (вхід, checkout) мають власні обмеження паралельності, незалежні від сервісу рекомендацій та аналітики? Якщо ні — додайте найпростішу реалізацію bulkhead-ів, хоча б як грубий ліміт паралельних завдань.
Нарешті проведіть аудит усіх вхідних вебхуків. Перевірте, чи є в них idempotency key або eventId, чи не намагаєтеся ви виконувати важку роботу синхронно в HTTP-обробнику, і чи вмієте пережити хвилю повторних спроб, якщо ваш downstream тимчасово «впаде». Якщо ні — перенесіть логіку в чергу й фонові воркери.
Така послідовність кроків дає дуже відчутне зростання стійкості навіть без надскладної інфраструктури.
8. Типові помилки під час роботи з timeouts, circuit breakers, bulkheads і штормами вебхуків
Помилка № 1: відсутність тайм-аутів «десь унизу».
Розробники часто ставлять тайм-аут лише на Gateway або лише на фронтенд, забуваючи, що всередині бекенду є ще БД, зовнішні API і LLM. У підсумку зовнішній запит начебто має тайм-аут 5 секунд, але всередині один виклик до БД або платіжного сервісу може висіти хвилинами. Це блокує пул зʼєднань і спричиняє каскадні відмови.
Помилка № 2: гігантські тайм-аути «про всяк випадок».
Іноді ставлять тайм-аут 60–120 секунд: «нехай уже дотягне». У контексті ChatGPT це майже завжди погано. Користувач іде, модель починає галюцинувати, а ваші ресурси весь цей час заблоковані. Набагато краще — чесна відмова через 5–10 секунд із зрозумілим описом.
Помилка № 3: circuit breaker без продуманого UX.
Іноді breaker додають «для формальності», але коли він спрацьовує, користувачеві або моделі повертається незрозумілий 500, «ECONNREFUSED» або «axios error». У підсумку GPT не може адекватно пояснити, що відбувається, і починає вигадувати. Варто одразу продумати формулювання помилок, які будуть зрозумілі і людям, і моделі.
Помилка № 4: змішування ресурсів без підходу bulkhead.
Класичний сценарій: один сервіс рекомендацій (або аналітики) починає гальмувати, «зʼїдає» весь пул зʼєднань до БД або пул потоків — і слідом за ним «падають» checkout і вхід. Усе тому, що ресурси не розділені. Відсутність бодай якогось bulkhead-підходу призводить до того, що другорядна фіча може «покласти» весь продакшн.
Помилка № 5: обробка вебхуків як звичайних запитів.
Новачки часто пишуть вебхук-обробник так само, як звичайний контролер: довга бізнес-логіка, запити до сторонніх API, відсутність ідемпотентності. В умовах повторних спроб і дублікатів це призводить до подвійної обробки подій, дивних станів замовлень і падінь під навантаженням під час шторму.
Помилка № 6: ігнорування ідемпотентності в commerce-сценаріях.
Особливо небезпечно, коли вебхук оплати може створити замовлення ще раз або повторно змінити його стан. Без перевірки idempotency key і зберігання статусу обробки події ви рано чи пізно отримаєте подвійне списання або дивні дублікати замовлень.
Помилка № 7: спроба полагодити все setTimeout-ами і «магічними затримками».
Іноді хочуть обійти race condition і проблеми шторму через «почекати 100 мс — і буде нормально». На практиці це робить поведінку ще нестабільнішою й жодним чином не захищає від реальних збоїв. Правильний шлях — явні тайм-аути, circuit breaker, черги та ідемпотентність, а не «шаманство» із затримками.
Помилка № 8: відсутність пріоритизації критичних шляхів.
Коли checkout і вхід живуть у тих самих лімітах, що й аналітика або рекомендаційна логіка, будь-яке перевантаження може однаково «покласти» і критичні, і другорядні частини. У стійкому дизайні checkout і автентифікація — найкритичніші: для них потрібні окремі ресурси, окремі ліміти, окремі алерти й SLO.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ