Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Bayroq nima va qayerda saqlanadi
- 2.1 Deploy va release
- 2.2 Uch saqlash joyi
- 3. Baholash tartibi
- 4. Deterministik foizli rollout
- 4.1 Savat
- 4.2 Uch xususiyat
- 4.3 SQL bilan tekshirish
- 5. Loyiha qadami: feature flag'lar
- 5.1 Qaysi fayllar
- 5.2 sql/306-bayroqlar.sql
- 5.3 src/bayroq/bayroqlar.ts
- 5.4 Ulash
- 5.5 /v1/auth/men da rol
- 5.6 Testlar
- 5.7 Versiya, hujjat va commit
- 6. Prodakshnda: kesh, ko'p nusxa va tozalash
- 6.1 30 soniyalik kesh
- 6.2 Tashqi vositalar
- 6.3 Eski bayroqlarni tozalash
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): nega yoqiq?
- 3-mashq (qiyin): xavfsiz rollout rejasi
- 9. Real ishda
- Xulosa
- Manbalar
Feature flag'lar backend'da: foizli rollout, segmentlar va kill switch
Qisqacha: Feature flag (bayroq) — kodni serverga chiqarish va xususiyatni foydalanuvchiga yoqishni ajratadigan sozlama.
vazifalar-apida bayroq bazadagi bir qator:yoqilgan(kill switch), ruxsat ro'yxati, rol segmenti va foiz. Foiz deterministik:sha256("kalit:id") % 100 < foiz— bitta foydalanuvchi har safar bir xil natija oladi, foiz 30 dan 50 ga oshsa avvalgilar yoqiq qoladi. O'chiq xususiyat yo'li 404 qaytaradi. Shu dars bilan audit jurnali va bayroqlar birga commit qilinadi.
Bu darsda
- Deploy va release farqini, bayroq turlarini va ularni qayerda saqlashni tushuntirasiz.
- Baholash tartibini (kill switch → ruxsat ro'yxati → rol → foiz) quriysiz.
- Deterministik foizli rollout'ni brauzerda va SQL'da hisoblab, uning uch xususiyatini tekshirasiz.
vazifalar-apiga bayroqlar jadvalini, xizmatini va admin yo'lini qo'shasiz.- Bayroq keshining ko'p nusxadagi oqibatini va eski bayroqlarni tozalash zaruratini bilasiz.
Oldin bilishingiz kerak: Feature flag va A/B testlar, Audit log, AI xususiyatlarini prodakshnda ishlatish, Guard'lar, LISTEN/NOTIFY.
Oldingi darsda multi-tenancy modellarini o'lchadik. Endi loyiha qadamiga qaytamiz: Audit log darsida boshlangan o'zgarish shu dars bilan tugaydi va bitta commit bo'ladi.
1. Nega bu kerak?
Kechasi Gemini API sekinlashdi. "Vazifani qadamlarga bo'lish" tugmasi 30 soniya aylanib, 504 beryapti. Foydalanuvchilar asabiylashyapti. Nima qilasiz?
Variant 1: kodni o'zgartirib, AI yo'lini o'chirib, test, build, deploy — kamida yarim soat. Variant 2: admin panelda bitta katakni o'chirasiz — 30 soniyada xususiyat hammaga yopiladi. Ikkinchi variantni beradigan narsa — feature flag.
Ertasi kuni teskari vaziyat: yangi AI yordamchi tayyor. Uni birdaniga 100 % foydalanuvchiga ochish xavfli — xato bo'lsa hamma ko'radi, AI kvotasi bir kunda tugaydi. Yaxshiroq yo'l: avval adminlarga, keyin 10 % foydalanuvchiga, keyin 50 %, keyin hammaga. Bu — foizli rollout.
Feature flag va A/B testlar darsida (21-qism) bayroqni frontend'da ishlatgandik: savat (bucket), foizli rollout, miltillash. U yerda frontend bayroqqa qarab tugmani ko'rsatardi yoki yashirardi. Lekin tugmani yashirish — himoya emas: API yo'li ochiq bo'lsa, curl bilan baribir chaqirish mumkin. Haqiqiy qaror backend'da bo'lishi kerak.
O'xshatish: binodagi elektr shiti. Chiroqlar (kod) devorga allaqachon o'rnatilgan, lekin har xonaning o'z avtomati bor. Avtomatni tushirsangiz — xona qorong'i, devorni buzish shart emas.
2. Bayroq nima va qayerda saqlanadi
2.1 Deploy va release
Deploy — kodni serverga chiqarish. Release — xususiyatni foydalanuvchiga ochish. Bayroqsiz ikkalasi bir vaqtda bo'ladi. Bayroq bilan ular ajraladi: kod bir hafta oldin deploy qilingan, lekin o'chiq turadi. Release — admin bir katakni o'zgartirgan payt.
Bayroqlar maqsadiga ko'ra to'rt xil:
| Tur | Maqsad | Umri |
|---|---|---|
| Release | yangi xususiyatni bosqichma-bosqich ochish | qisqa (hafta) |
| Ops / kill switch | muammo bo'lsa tez o'chirish | uzoq |
| Tajriba (A/B) | ikki variantni solishtirish | qisqa |
| Ruxsat | faqat ma'lum guruh (beta, admin) | uzoq |
vazifalar-api dagi ai-qadamlar va ai-yordamchi — ham kill switch, ham release bayrog'i.
2.2 Uch saqlash joyi
| Joy | O'zgartirish | Kamchilik |
|---|---|---|
Muhit o'zgaruvchisi (AI_PROVAYDER) |
qayta ishga tushirish | foiz va segment yo'q |
| Baza jadvali (kanon) | admin yo'li, soniyalar | o'zingiz yozasiz, kesh kerak |
| Tashqi xizmat (Unleash va boshqalar) | veb-panel | yana bitta bog'liqlik |
Kanonda ikkalasi ham bor. AI_PROVAYDER=yoq — AI butunlay o'chiq, provayder ham yaratilmaydi (LLM API asoslari darsidan). Bayroq esa provayder sozlangan bo'lsa ham kim ko'rishini boshqaradi.
3. Baholash tartibi
Bitta bayroq to'rtta qoidaga ega. Ular ziddiyatga kirsa — qaysi biri ustun? Kanondagi tartib:
flowchart TD
S["So'rov: kalit + foydalanuvchi"] --> J{"Jadvalda bormi?"}
J -->|"yo'q"| Y1["yoqiq (cheklanmagan)"]
J -->|"bor"| K{"yoqilgan?"}
K -->|"false"| O["o'chiq — hammaga"]
K -->|"true"| R{"ruxsat ro'yxatida?"}
R -->|"ha"| Y2["yoqiq"]
R -->|"yo'q"| L{"rol segmentida?"}
L -->|"ha"| Y3["yoqiq"]
L -->|"yo'q"| F{"savat < foiz?"}
F -->|"ha"| Y4["yoqiq"]
F -->|"yo'q"| O2["o'chiq"]Diagrammani yuqoridan o'qing. Uch qaror ataylab qilingan:
- Kill switch hammadan ustun. Avariyada "hammaga o'chir" deganda, ruxsat ro'yxatidagi beta-testerlar ham o'chadi. Aks holda kill switch yarim ishlaydi.
- Ruxsat ro'yxati foizdan ustun. Foiz 0 bo'lsa ham, ro'yxatdagi foydalanuvchi xususiyatni ko'radi — ichki sinov uchun.
- Jadvalda yo'q bayroq — yoqiq. Kod bayroqni kutyapti, lekin hali hech kim qator qo'shmagan bo'lsa — eski xulq saqlanadi. Bu tanlov munozarali: teskarisi ("yo'q — o'chiq") xavfsizroq, lekin migratsiyani unutsangiz xususiyat jim yo'qoladi. Kanon boshlang'ich qatorlarni migratsiyaning o'zida yaratadi.
Bir misol bilan mustahkamlaymiz. Bayroq: yoqilgan: true, foiz 0, rollar — ["admin"], ruxsat ro'yxatida — Malika. Admin Jasur so'rov yuboradi. Diagramma bo'yicha yuramiz: jadvalda bor → yoqilgan → Jasur ro'yxatda yo'q → lekin roli admin → yoqiq. Foizga umuman yetib bormadik. Oddiy foydalanuvchi Ali esa foiz 0 bo'lgani uchun o'chiq.
Tekshirib ko'ring: Avariya paytida admin
yoqilgannifalseqildi. Ruxsat ro'yxatidagi Malika xususiyatni ko'radimi?
Javob
Yo'q. yoqilgan — birinchi tekshiruv, undan keyin hech narsa qaralmaydi. Ruxsat ro'yxati faqat bayroq yoqiq bo'lgandagina ishlaydi. Shuning uchun kill switch "hammaga o'chir" degan va'dasini to'liq bajaradi — beta-testerlar ham istisno emas.
4. Deterministik foizli rollout
4.1 Savat
Foiz 30 bo'lsa, qaysi 30 %? Har so'rovda Math.random() < 0.3 desak, Aziz bir sahifada tugmani ko'radi, keyingisida yo'q. Kerak: bitta foydalanuvchi — doim bir xil javob.
Yechim — foydalanuvchini 0 dan 99 gacha bo'lgan savatga (bucket) joylash: sha256("kalit:id") ning birinchi 4 bayti, 100 ga qoldiq. Natija tasodifiyga o'xshaydi, lekin bir xil kirishga doim bir xil chiqadi. Savat foizdan kichik bo'lsa — yoqiq.
Kanon kodida savatni hisoblaydigan funksiya chelak() deb nomlangan. Chelak va savat bu yerda bir narsa: matnda "savat" deymiz, kodda esa kanondagi nom qoladi.
Kanon funksiyasi Node'ning createHash bilan yozilgan. Brauzerda Web Crypto bilan aynan shu hisobni qilamiz. Kodning birinchi qismi to'rtta foydalanuvchini baholaydi, ikkinchisi 10 000 kishida foizlarni sanaydi, uchinchisi ikki bayroqni solishtiradi:
async function chelak(kalit, id) {
const b = new TextEncoder().encode(`${kalit}:${id}`);
const h = await crypto.subtle.digest("SHA-256", b);
return new DataView(h).getUint32(0) % 100;
}
function baholash(b, kim, ch) {
if (b === undefined) return true; // jadvalda yo'q — cheklanmagan
if (!b.yoqilgan) return false; // kill switch
if (b.foydalanuvchilar.includes(kim.id)) return true;
if (kim.rol !== undefined && b.rollar.includes(kim.rol)) {
return true;
}
return ch < b.foiz;
}
const b = { kalit: "ai-qadamlar", yoqilgan: true, foiz: 30,
rollar: ["admin"], foydalanuvchilar: [7] };
for (const id of [1, 2, 3, 7]) {
const ch = await chelak(b.kalit, id);
console.log(`id ${id}: chelak ${ch} → ${baholash(b, { id }, ch)}`);
}
const idlar = Array.from({ length: 10_000 }, (_, i) => i + 1);
const chelaklar = await Promise.all(
idlar.map((id) => chelak(b.kalit, id)));
const yoqiq = (foiz) => idlar.filter((_, i) => chelaklar[i] < foiz);
const y30 = yoqiq(30);
const y50 = new Set(yoqiq(50));
console.log("30 %:", y30.length, "| 50 %:", y50.size);
console.log("30 dagilar 50 da ham:", y30.every((id) => y50.has(id)));
const boshqa = await Promise.all(
idlar.map((id) => chelak("yangi-dizayn", id)));
const ikkalasi = idlar.filter((_, i) =>
chelaklar[i] < 10 && boshqa[i] < 10).length;
console.log("ikki bayroqning 10 % i ustma-ust:", ikkalasi);Konsolda:
id 1: chelak 44 → false
id 2: chelak 84 → false
id 3: chelak 8 → true
id 7: chelak 87 → true
30 %: 2983 | 50 %: 4994
30 dagilar 50 da ham: true
ikki bayroqning 10 % i ustma-ust: 101getUint32(0) — DataView sukut bo'yicha big-endian o'qiydi, Node'dagi readUInt32BE(0) bilan bir xil. Biz natijani Node'da ham tekshirdik: 1, 2, 3, 7 uchun savatlar 44, 84, 8, 87 — aynan shunday.
4.2 Uch xususiyat
Chiqish rollout'ning uchta muhim xususiyatini ko'rsatadi:
- Deterministik. id 3 — savat 8, har safar 8. id 7 ning savati 87 (30 dan katta), lekin u ruxsat ro'yxatida — yoqiq.
- Monoton. Foiz 30 dan 50 ga oshganda, 30 % dagi hamma (2 983 kishi) 50 % ichida qoldi. Rollout'ni kengaytirganda hech kim xususiyatni "yo'qotmaydi" — chunki shart
chelak < foiz, savat esa o'zgarmaydi. - Bayroqlar mustaqil. Hash'ga bayroq kaliti ham kiradi. Ikkala bayroqning 10 % i bir xil odamlar bo'lganda, 1 000 ta ustma-ust tushardi. Haqiqatda 101 ta — taxminan 10 % × 10 % = 1 %. Agar faqat
idhash'lansa, "eng omadli 10 %" hamma tajribaga tushardi va natijalar aralashib ketardi.
Taqsimot qanchalik tekis? 10 000 foydalanuvchida turli foizlar uchun yoqilganlar soni:
- foiz 1kutilgan 10094 kishi
- foiz 10kutilgan 1 0001 011 kishi
- foiz 30kutilgan 3 0002 983 kishi
- foiz 50kutilgan 5 0004 994 kishi
Manba: Biz ishga tushirdik (IlmHamroh, 2026-11-03): Node 24.21.0, sha256("ai-qadamlar:id") birinchi 4 bayt % 100, id 1…10 000
Grafikni o'qing: har foizda haqiqiy son kutilganga juda yaqin. Kanon testi ham shuni tekshiradi: 30 % uchun 2 800–3 200 oralig'i. Kichik foizda (1 %) nisbiy farq kattaroq — 100 o'rniga 94. Kam foydalanuvchili ilovada 1 % rollout "taxminan" bo'ladi.
4.3 SQL bilan tekshirish
Savatni Postgres ham hisoblay oladi — masalan, "hozir kimlarda yoqiq?" degan hisobot uchun. Biz o'z bazamizda tekshirdik:
SELECT id, ('x' || left(encode(sha256(convert_to(
'ai-qadamlar:' || id, 'UTF8')), 'hex'), 8))::bit(32)::bigint % 100
AS chelak
FROM generate_series(1, 3) id; id | chelak
----+--------
1 | 44
2 | 84
3 | 8
(3 rows)Uch muhitda — brauzer, Node, Postgres — bir xil raqam. Hash'ning birinchi 8 ta o'n oltilik belgisi bit(32) orqali songa aylantirildi.
Tekshirib ko'ring: Rollout 50 % da edi, keyin xato topildi va foizni 20 ga tushirdingiz. 20 % da qolganlar kimlar?
Javob
Savati 20 dan kichiklar — ular 50 % ichida ham bor edi. Ya'ni tushirishda ham monotonlik ishlaydi: yangi odam qo'shilmaydi, faqat savati 20–49 bo'lganlar chiqadi. Xatoni ko'rganlarning bir qismi xususiyatni ko'rishda davom etadi — buni bilib qaror qiling. Xato jiddiy bo'lsa — foiz emas, kill switch.
5. Loyiha qadami: feature flag'lar
5.1 Qaysi fayllar
| Fayl | Nima |
|---|---|
sql/306-bayroqlar.sql + migratsiya 20261102146000_bayroqlar |
jadval va boshlang'ich bayroqlar |
prisma/schema.prisma |
Bayroq modeli (drift tekshiruvi uchun) |
src/bayroq/bayroqlar.ts |
baholash, xizmat, controller'lar, modul |
src/tokenlar.ts, src/app.module.ts |
token va modul ulanadi |
src/ai/ai.controller.ts |
AI yo'llari bayroq bilan |
src/auth/auth.controller.ts, test/oidc.test.ts |
/v1/auth/men da rol |
test/prodakshn.test.ts |
bayroq testlari qo'shiladi |
package.json, API.md |
versiya va hujjat |
Kanon fayllaridagi uzun qatorlar telefon uchun bo'lindi, izohlar qisqartirildi, kod ma'nosi aynan. Bo'lingan nusxani loyihaning shu bosqichida tsc --noEmit, ESLint va Vitest bilan tekshirdik — o'zgarishsiz o'tdi.
5.2 sql/306-bayroqlar.sql
Faylni o'z bazamizda ishga tushirdik (oldingi sql/ fayllari va Audit log darsidagi 305 qo'llangan):
-- 306-bayroqlar.sql — feature flag'lar: kodni chiqarish (deploy) va
-- xususiyatni yoqish ALOHIDA. Baholash ilovada (bayroqlar.ts):
-- yoqilgan=false — hammaga o'chiq (kill switch); foydalanuvchilar
-- ro'yxatida — yoqiq; rollar segmentida — yoqiq; aks holda foizli
-- rollout: sha256(kalit:foydalanuvchi_id) → 0..99 < foiz
-- (deterministik; foiz oshsa — eski "yoqiq"lar qoladi).
CREATE TABLE bayroqlar (
kalit text NOT NULL,
yoqilgan boolean NOT NULL DEFAULT false,
foiz smallint NOT NULL DEFAULT 0,
rollar text[] NOT NULL DEFAULT '{}',
foydalanuvchilar bigint[] NOT NULL DEFAULT '{}',
tavsif text,
yangilangan_vaqt timestamptz NOT NULL DEFAULT now(),
CONSTRAINT bayroqlar_pkey PRIMARY KEY (kalit),
CONSTRAINT bayroqlar_kalit_check
CHECK (kalit ~ '^[a-z][a-z0-9-]{1,49}$'),
CONSTRAINT bayroqlar_foiz_check CHECK (foiz BETWEEN 0 AND 100)
);
-- boshlang'ich bayroqlar: AI xususiyatlari yoqiq (100 %); rollout —
-- admin yo'li bilan (PUT /v1/admin/bayroqlar/:kalit)
INSERT INTO bayroqlar (kalit, yoqilgan, foiz, rollar, tavsif) VALUES
('ai-qadamlar', true, 100, '{}', 'POST /v1/vazifalar/:id/qadamlar'),
('ai-yordamchi', true, 100, '{}',
'GET /v1/qidiruv, POST /v1/yordamchi/savol');
GRANT SELECT ON bayroqlar TO vazifalar_ilova;
-- admin yo'li (auth pool)
GRANT SELECT, INSERT, UPDATE ON bayroqlar TO vazifalar_auth;
COMMENT ON TABLE bayroqlar IS
'feature flag''lar — kill switch, ruxsat ro''yxati, rol segmenti, '
'deterministik foizli rollout';CREATE TABLE
INSERT 0 2
GRANT
GRANT
COMMENTCHECK cheklovlari kalit formatini va foiz oralig'ini baza darajasida ushlaydi — API'dagi Zod sxemasi bilan bir xil qoida, ikki qatlamda:
\set VERBOSITY terse
UPDATE bayroqlar SET foiz = 101 WHERE kalit = 'ai-qadamlar';
SELECT kalit, yoqilgan, foiz FROM bayroqlar ORDER BY kalit;ERROR: new row for relation "bayroqlar" violates check constraint "bayroqlar_foiz_check"
kalit | yoqilgan | foiz
--------------+----------+------
ai-qadamlar | t | 100
ai-yordamchi | t | 100
(2 rows)Tarjimasi: "bayroqlar jadvalidagi yangi qator bayroqlar_foiz_check cheklovini buzadi". \set VERBOSITY terse xatoning DETAIL qatorini yashiradi — unda vaqt bor, har yugurishda boshqa.
Huquqlar: vazifalar_ilova faqat o'qiydi (baholash uchun), vazifalar_auth (admin yo'li) o'zgartiradi. Migratsiya fayli — shu matnning aynan nusxasi.
prisma/schema.prisma oxiriga model qo'shiladi. Ilova bayroqlarni toza SQL bilan o'qiydi — model faqat sxema drift tekshiruvi uchun (Prisma Migrate chuqur darsidagi migrate diff):
/// feature flag'lar (sql/306). Ilova SQL bilan o'qiydi,
/// Prisma modeli — sxema drift tekshiruvi uchun
model Bayroq {
kalit String @id
yoqilgan Boolean @default(false)
foiz Int @default(0) @db.SmallInt
rollar String[] @default([])
foydalanuvchilar BigInt[] @default([])
tavsif String?
yangilanganVaqt DateTime @default(now())
@map("yangilangan_vaqt") @db.Timestamptz(6)
@@map("bayroqlar")
}yangilanganVaqt qatori telefon uchun ikkiga bo'lindi — Prisma'da atributlar keyingi qatorda davom etishi mumkin; faylingizda bitta qatorda yozsangiz ham bo'ladi.
5.3 src/bayroq/bayroqlar.ts
Yangi fayl to'liq:
// bayroqlar.ts — feature flag'lar (sql/306). Baholash ilovada,
// bayroqlar jadvali jarayon xotirasida 30 s keshlanadi (har so'rovda
// DB emas). Kesh oqibati: admin o'zgartirgani boshqa nusxalarda
// (instance) 30 s gacha kechikadi — o'zgartirgan nusxada darhol
// (tozala()). Ko'p nusxada darhol kerak bo'lsa — Redis Pub/Sub yoki
// LISTEN/NOTIFY (darsda). OpenFeature/Unleash/GrowthBook —
// sinalmagan.
import {
Body, Controller, Get, Global, HttpCode, Inject, Module, Param, Put,
Req,
} from "@nestjs/common";
import type { DynamicModule, Provider, Type } from "@nestjs/common";
import { createHash } from "node:crypto";
import type { Request } from "express";
import type { Pool } from "pg";
import { z } from "zod";
import { AUTH, BAYROQLAR } from "../tokenlar.ts";
import type { AuthBogliqliklari } from "../auth/xizmat.ts";
import { Rollar } from "../auth/qoriqchi.ts";
import { JsonTana } from "../umumiy/json-tana.guard.ts";
import { TopilmadiXatosi } from "../umumiy/xatolar.ts";
import { Audit } from "../audit/audit.ts";
export type Bayroq = {
kalit: string; yoqilgan: boolean; foiz: number;
rollar: string[]; foydalanuvchilar: number[];
};
export type Kim = { id: number; rol?: string | undefined };
/** 0..99 — kalit va foydalanuvchi bo'yicha deterministik (bir
* bayroqdagi 10 % boshqa bayroqdagi 10 % bilan bir xil odamlar
* emas — kalit qo'shilgan) */
export function chelak(kalit: string, id: number): number {
return createHash("sha256").update(`${kalit}:${String(id)}`)
.digest().readUInt32BE(0) % 100;
}
export function baholash(b: Bayroq | undefined, kim: Kim): boolean {
// jadvalda yo'q bayroq — xususiyat cheklanmagan
if (b === undefined) return true;
// kill switch — hammaga, ruxsat ro'yxatiga ham
if (!b.yoqilgan) return false;
if (b.foydalanuvchilar.includes(kim.id)) return true;
if (kim.rol !== undefined && b.rollar.includes(kim.rol)) {
return true;
}
return chelak(b.kalit, kim.id) < b.foiz;
}
export class BayroqlarXizmati {
#kesh: { vaqt: number; bayroqlar: Map<string, Bayroq> } | undefined;
constructor(
private readonly pool: Pool, private readonly ttlMs = 30_000,
) {}
async hammasi(): Promise<Map<string, Bayroq>> {
if (this.#kesh !== undefined
&& Date.now() - this.#kesh.vaqt < this.ttlMs) {
return this.#kesh.bayroqlar;
}
const { rows } = await this.pool.query<{
kalit: string; yoqilgan: boolean; foiz: number;
rollar: string[]; foydalanuvchilar: string[];
}>("SELECT kalit, yoqilgan, foiz, rollar, "
+ "foydalanuvchilar::text[] AS foydalanuvchilar "
+ "FROM bayroqlar");
const bayroqlar = new Map(rows.map((r) => [r.kalit, {
...r, foydalanuvchilar: r.foydalanuvchilar.map(Number),
}]));
this.#kesh = { vaqt: Date.now(), bayroqlar };
return bayroqlar;
}
async yoqilganmi(kalit: string, kim: Kim): Promise<boolean> {
return baholash((await this.hammasi()).get(kalit), kim);
}
tozala(): void {
this.#kesh = undefined;
}
}
/** so'rovdan baholash uchun kim (sessiya bo'lsa rol bilan; API
* kalit — rolsiz: faqat foiz va ruxsat ro'yxati) */
export function kimOl(req: Request): Kim | undefined {
return req.egasi === undefined
? undefined
: { id: req.egasi, rol: req.sessiya?.rol };
}
/** xususiyat yoqilmagan bo'lsa — 404 (xususiyat mavjudligini
* oshkor qilmaymiz) */
export async function bayroqKerak(
b: BayroqlarXizmati | null, kalit: string, req: Request,
): Promise<void> {
const kim = kimOl(req);
if (b === null || kim === undefined) return;
if (!(await b.yoqilganmi(kalit, kim))) {
throw new TopilmadiXatosi(
`${req.method} ${req.path} — bunday manzil yo'q`,
"xususiyat-yoq", "Topilmadi");
}
}
/** GET /v1/bayroqlar — joriy foydalanuvchi uchun baholangan
* bayroqlar (frontend UI'ni shunga qarab ko'rsatadi; qoidalar
* oshkor qilinmaydi) */
@Controller("bayroqlar")
export class BayroqlarController {
constructor(
@Inject(BAYROQLAR) private readonly b: BayroqlarXizmati,
) {}
@Get()
async royxat(
@Req() req: Request,
): Promise<Record<string, boolean>> {
const kim = kimOl(req);
const hammasi = await this.b.hammasi();
return Object.fromEntries([...hammasi.values()].map((x) =>
[x.kalit, kim === undefined ? false : baholash(x, kim)]));
}
}
const BayroqSxemasi = z.strictObject({
yoqilgan: z.boolean(),
foiz: z.int().min(0).max(100),
rollar: z.array(z.enum(["foydalanuvchi", "admin"])).max(2)
.default([]),
foydalanuvchilar: z.array(z.int().positive()).max(1000)
.default([]),
tavsif: z.string().max(200).optional(),
});
const Kalit = z.strictObject({
kalit: z.string().regex(/^[a-z][a-z0-9-]{1,49}$/),
});
@Controller("admin/bayroqlar")
@Rollar("admin")
export class AdminBayroqlarController {
constructor(
@Inject(BAYROQLAR) private readonly b: BayroqlarXizmati,
@Inject(AUTH) private readonly auth: AuthBogliqliklari,
) {}
@Get()
async royxat(): Promise<Bayroq[]> {
return [...(await this.b.hammasi()).values()];
}
/** yaratish yoki o'zgartirish (upsert) — auditga yoziladi, shu
* nusxaning keshi darhol tozalanadi */
@Put(":kalit")
@HttpCode(200)
@JsonTana()
@Audit("bayroq.ozgartirildi", "bayroq")
async yoz(
@Param({ schema: Kalit }) { kalit }: { kalit: string },
@Body({ schema: BayroqSxemasi })
x: z.output<typeof BayroqSxemasi>,
): Promise<Bayroq> {
await this.auth.pool.query(
`INSERT INTO bayroqlar (kalit, yoqilgan, foiz, rollar,
foydalanuvchilar, tavsif, yangilangan_vaqt)
VALUES ($1, $2, $3, $4, $5, $6, now())
ON CONFLICT (kalit) DO UPDATE SET yoqilgan = $2, foiz = $3,
rollar = $4, foydalanuvchilar = $5,
tavsif = coalesce($6, bayroqlar.tavsif),
yangilangan_vaqt = now()`,
[kalit, x.yoqilgan, x.foiz, x.rollar, x.foydalanuvchilar,
x.tavsif ?? null]);
this.b.tozala();
return {
kalit, yoqilgan: x.yoqilgan, foiz: x.foiz, rollar: x.rollar,
foydalanuvchilar: x.foydalanuvchilar,
};
}
}
@Global()
@Module({})
export class BayroqlarModule {
/** auth yoqilgan bo'lsa (postgres/prisma) — bayroqlar auth
* pool'idan o'qiladi; aks holda null (cheklovsiz) */
static forRoot(yoqilgan: boolean): DynamicModule {
const controllers: Type[] = yoqilgan
? [BayroqlarController, AdminBayroqlarController]
: [];
const providers: Provider[] = [{
provide: BAYROQLAR, inject: [AUTH],
useFactory: (
a: AuthBogliqliklari | null,
): BayroqlarXizmati | null =>
(yoqilgan && a !== null
? new BayroqlarXizmati(a.pool)
: null),
}];
return {
module: BayroqlarModule, controllers, providers,
exports: [BAYROQLAR],
};
}
}Fayldagi qismlar:
chelak()vabaholash()— brauzerda sinagan funksiyalarimiz, Node versiyasi. Toza funksiyalar — test qilish oson.BayroqlarXizmati— jadvalni o'qib, jarayon xotirasida 30 soniya saqlaydi. Har so'rovda bazaga bormaydi.tozala()keshni darhol bo'shatadi.bayroqKerak()— AI yo'llari chaqiradigan yordamchi. O'chiq bo'lsa — 404xususiyat-yoq, 403 emas. Sabab: 403 "bu yo'l bor, lekin sizga mumkin emas" deydi — yashirin xususiyat mavjudligini oshkor qiladi. 404 — "bunday manzil yo'q".title— "Topilmadi" (31-qismdan Problemtitlelari o'zbekcha).BayroqlarController—GET /v1/bayroqlar: joriy foydalanuvchi uchun natijalar ({ "ai-qadamlar": true }), qoidalar emas. Frontend foizni va ruxsat ro'yxatini ko'rmaydi.AdminBayroqlarController—PUT /v1/admin/bayroqlar/:kalit(upsert).@Audit("bayroq.ozgartirildi", "bayroq")— oldingi darsdagi dekorator: kim, qachon, qaysi bayroqni o'zgartirgani jurnalga tushadi.BayroqlarModule.forRoot()— dinamik modul: auth bo'lmasa (xotira rejimi) xizmatnull— hamma narsa cheklovsiz.
API kalit bilan kelgan so'rovda rol yo'q (kimOl — sessiya bo'lmasa rol undefined): bunday mijozga faqat foiz va ruxsat ro'yxati amal qiladi.
Kanon fayl izohida OpenFeature bilan tajriba qilingan deb yozilgan edi — aslida bunday tajriba qilinmagan. Izohni "sinalmagan" deb tuzatdik.
5.4 Ulash
src/tokenlar.ts oxiriga:
/** BayroqlarXizmati yoki null (auth yo'q — cheklovsiz) */
export const BAYROQLAR = Symbol("BAYROQLAR");src/app.module.ts da, importlar orasiga va imports ro'yxatiga (AiModule dan keyin):
import { BayroqlarModule } from "./bayroq/bayroqlar.ts";
// imports: [ ...
// feature flag'lar (auth bilan)
BayroqlarModule.forRoot(authYoqilgan),
// ... ]src/ai/ai.controller.ts — uchala AI controller'iga bayroq xizmati beriladi va handler boshida tekshiriladi:
import { AI, BAYROQLAR } from "../tokenlar.ts";
import { bayroqKerak } from "../bayroq/bayroqlar.ts";
import type { BayroqlarXizmati } from "../bayroq/bayroqlar.ts";
// AiVazifaController, QidiruvController, YordamchiController:
constructor(
@Inject(AI) private readonly ai: AiXizmatlari,
@Inject(BAYROQLAR)
private readonly bayroqlar: BayroqlarXizmati | null,
) {}
// POST /v1/vazifalar/:id/qadamlar — birinchi qator:
await bayroqKerak(this.bayroqlar, "ai-qadamlar", req);
// GET /v1/qidiruv va POST /v1/yordamchi/savol — birinchi qator:
await bayroqKerak(this.bayroqlar, "ai-yordamchi", req);Tekshiruv handler'ning birinchi qatori — kvota va kesh undan keyin. O'chiq xususiyat kvota sarflamaydi va LLM'ni chaqirmaydi.
5.5 /v1/auth/men da rol
30-qismdan ochiq qolgan kichik xato ham shu commit'da yopiladi. Frontend admin menyusini ko'rsatish uchun foydalanuvchi rolini bilishi kerak edi — avval /v1/admin/* ga urinib 403 olishdan boshqa yo'l yo'q edi. auth.controller.ts dagi men() qaytaradigan obyekt:
// 31: rol ham (30-qism ochiq xatosi — frontend admin menyusini
// ko'rsatishi uchun; avval /v1/admin/* ga urinib 403 olish
// kerak edi)
return {
foydalanuvchi: { ...s.foydalanuvchi, rol: s.rol },
sessiya: {
id: s.id, eslabQol: s.eslabQol,
yaratilgan: s.yaratilgan.toISOString(),
tugash: s.tugash.toISOString(),
},
};Javobga maydon qo'shildi — shuning uchun test/oidc.test.ts dagi aniq solishtirish ham yangilanadi:
- { id: 1, email: "aziz@example.com", toliqIsm: "Aziz Google" }
+ { id: 1, email: "aziz@example.com", toliqIsm: "Aziz Google",
+ rol: "foydalanuvchi" }5.6 Testlar
test/prodakshn.test.ts ga qo'shiladigan qismlar. Importlar:
import { afterAll, beforeEach, describe, test } from "vitest";
import { baholash, chelak } from "../src/bayroq/bayroqlar.ts";Bazasiz describe — fayl boshida, authBaza dan oldin:
describe("#25 bayroqni baholash (toza funksiya)", () => {
const b = {
kalit: "yangi-xususiyat", yoqilgan: true, foiz: 30,
rollar: ["admin"], foydalanuvchilar: [7],
};
test("kill switch > ruxsat ro'yxati > rol > foiz; jadvalda yo'q — "
+ "cheklanmagan; deterministik", () => {
assert.equal(
baholash({ ...b, yoqilgan: false }, { id: 7, rol: "admin" }),
false);
assert.equal(baholash({ ...b, foiz: 0 }, { id: 7 }), true);
assert.equal(
baholash({ ...b, foiz: 0 }, { id: 8, rol: "admin" }), true);
assert.equal(baholash(undefined, { id: 1 }), true);
assert.equal(chelak("a", 42), chelak("a", 42));
});
test("foizli rollout: 10 000 da 30 % → 28–32 %; 30 → 50 % da "
+ "avvalgi yoqilganlarning HAMMASI yoqiq qoladi", () => {
const ids = Array.from({ length: 10_000 }, (_, i) => i + 1);
const oddiy = { ...b, rollar: [], foydalanuvchilar: [] };
const y30 = ids.filter((id) => baholash(oddiy, { id }));
const y50 = new Set(ids.filter((id) =>
baholash({ ...oddiy, foiz: 50 }, { id })));
assert.ok(y30.length >= 2800 && y30.length <= 3200,
`30 %: ${String(y30.length)}`);
assert.ok(y30.every((id) => y50.has(id)));
});
});tayyorla() ichida, admin rolini berishdan keyin:
// tozala() public jadvallarni TRUNCATE qiladi — 306 dagi
// boshlang'ich bayroqlar ham ketadi; qayta qo'yamiz
await pool.query("INSERT INTO bayroqlar (kalit, yoqilgan, foiz) "
+ "VALUES ('ai-qadamlar', true, 100), "
+ "('ai-yordamchi', true, 100)");Bu qator kanon test yozishda topgan tuzoq: testlarni tozalovchi tozala() hamma public jadvallarni bo'shatadi — migratsiya qo'ygan boshlang'ich bayroqlarni ham. Ular yo'q bo'lsa, "jadvalda yo'q — yoqiq" qoidasi ishlaydi va test noto'g'ri sababdan o'tib ketishi mumkin edi.
Audit testlaridan keyin — ikki yangi test:
test("#25 bayroqlar: o'chirilsa AI yo'li 404, ruxsat ro'yxati "
+ "bilan ochiq; kill switch ustun; PUT 403/422; audit",
async () => {
const { app, admin, aziz, azizId } = await tayyorla();
const B = "/v1/admin/bayroqlar/ai-qadamlar";
const bayroqlar = async () => (await request(app)
.get("/v1/bayroqlar").set("Cookie", aziz).expect(200))
.body as Record<string, boolean>;
assert.deepEqual(await bayroqlar(),
{ "ai-qadamlar": true, "ai-yordamchi": true });
const v = (await request(app).post("/v1/vazifalar")
.set("Cookie", aziz).send({ matn: "Bayroq sinovi" })
.expect(201)).body as { id: number };
const Q = `/v1/vazifalar/${String(v.id)}/qadamlar`;
await request(app).post(Q).set("Cookie", aziz).expect(200);
await request(app).put(B).set("Cookie", aziz)
.send({ yoqilgan: false, foiz: 0 }).expect(403);
await request(app).put(B).set("Cookie", admin)
.send({ yoqilgan: true, foiz: 101 }).expect(422);
await request(app).put(B).set("Cookie", admin)
.send({ yoqilgan: true, foiz: 0 }).expect(200);
const r = await request(app).post(Q).set("Cookie", aziz)
.expect(404);
assert.match((r.body as { type: string }).type, /xususiyat-yoq$/);
assert.equal((await bayroqlar())["ai-qadamlar"], false);
await request(app).put(B).set("Cookie", admin)
.send({ yoqilgan: true, foiz: 0, foydalanuvchilar: [azizId] })
.expect(200);
await request(app).post(Q).set("Cookie", aziz).expect(200);
await request(app).put(B).set("Cookie", admin)
.send({
yoqilgan: false, foiz: 100, foydalanuvchilar: [azizId],
})
.expect(200);
// kill switch ro'yxatdan ustun
await request(app).post(Q).set("Cookie", aziz).expect(404);
const j = (await jurnal()).filter((x) =>
x.amal === "bayroq.ozgartirildi"
&& x.obyekt_id === "ai-qadamlar");
assert.equal(j.length, 3);
});
test("ochiq xato (30-qism): GET /v1/auth/men javobida rol bor",
async () => {
const { app, admin, aziz } = await tayyorla();
const rol = async (cookie: string) => ((await request(app)
.get("/v1/auth/men").set("Cookie", cookie)
.expect(200)).body as {
foydalanuvchi: { rol: string };
}).foydalanuvchi.rol;
assert.equal(await rol(admin), "admin");
assert.equal(await rol(aziz), "foydalanuvchi");
});Birinchi test bayroqning butun hayotini o'tadi. 100 % da AI yo'li 200. Oddiy foydalanuvchi bayroqni o'zgartira olmaydi (403), foiz 101 — 422. Foiz 0 — Aziz uchun 404 va GET /v1/bayroqlar da false. Ruxsat ro'yxatiga qo'shildi — yana 200. Kill switch — ro'yxat bo'lsa ham 404. Uchala muvaffaqiyatli o'zgarish audit jurnalida.
Chiqishda bazasiz describe ning ikki testidan bittasi ko'rinadi — Vitest standart hisobotida tez tugagan testlarni qisqartiradi; (2) belgisi ikkalasi ham o'tganini bildiradi. Pastdagi jami — shu faylga qo'shib yurgizilgan oidc.test.ts va ai.test.ts bilan.
Biz loyihaning shu bosqichini PostgreSQL 17.6 da ishga tushirdik (test bazasi — sql/ fayllari, namunasiz; vector kengaytmasi o'rnatilmagani uchun embeddinglar jadvali yo'q — bu testlarga ta'sir qilmaydi):
✓ |asosiy| test/prodakshn.test.ts (6 tests) 3679ms
✓ #25 bayroqni baholash (toza funksiya) (2)
✓ foizli rollout: 10 000 da 30 % → 28–32 %; 30 → 50 % da avvalgi yoqilganlarning HAMMASI yoqiq qoladi 340ms
✓ #23 audit: muvaffaqiyatli amal yoziladi, 403 — yo'q; zanjir butun → buzilsa topiladi 1503ms
✓ #23 audit: hisob o'chirish — aktyor o'zi (amaldan keyin sessiya yo'q bo'lsa ham) 546ms
✓ #25 bayroqlar: o'chirilsa AI yo'li 404, ruxsat ro'yxati bilan ochiq; kill switch ustun; PUT 403/422; audit 551ms
✓ ochiq xato (30-qism): GET /v1/auth/men javobida rol bor 447ms
...
Test Files 3 passed (3)
Tests 21 passed (21)Nima bo'ldi? Oltita testning hammasi yashil: ikkitasi bazasiz (toza funksiyalar), to'rttasi haqiqiy Postgres bilan. Ikki eski audit testi ham o'tdi — demak bayroqlar oldingi darsning ishini buzmadi.
Kanon o'lchovi (bitta mashina, PostgreSQL 18.6) — prodakshn.test.ts 6 test, butun to'plam: Tests 327 passed | 1 skipped (328); tip, lint, build — 0 xato; migratsiyalar 26, drift yo'q.
5.7 Versiya, hujjat va commit
package.json da versiya 4.0.0-alfa.3. API.md jadvaliga to'rtta yangi yo'l:
| `GET /v1/bayroqlar` | joriy foydalanuvchi uchun baholangan feature flag'lar `{ "ai-qadamlar": true, … }` (qoidalar oshkor qilinmaydi) | 200 |
| `GET/PUT /v1/admin/bayroqlar[/:kalit]` (admin) | bayroqlar: `{ yoqilgan, foiz 0–100, rollar, foydalanuvchilar, tavsif }` — o'chiq xususiyat yo'li 404 `xususiyat-yoq` | 200 / 403 / 422 |
| `GET /v1/admin/audit?keyin=&limit=` (admin) | audit jurnali (faqat qo'shiladi, hash zanjiri) | 200 `{ yozuvlar, keyingi }` |
| `POST /v1/admin/audit/tekshir` (admin) | zanjir butunligi | 200 `{ butun, yozuvlar, buzilganlar: [{ id, sabab }] }` |O'zgarishlar tarixidagi v4.0.0-alfa qatoriga: "GET /v1/auth/men da rol; audit jurnali, feature flag'lar".
Tekshiruv (npm run check — tip, lint, test, build) yashil bo'lsa, ikki darsning ishini bitta commit qilamiz:
git add -A
M='feat: audit jurnali (faqat qo'"'"'shiladi, hash zanjiri, @Audit '
M+='interceptor), feature flag'"'"'lar (foizli rollout, segment, '
M+='kill switch); /auth/men da rol'
git commit -m "$M"Xabar uzun bo'lgani uchun uch qismga bo'lindi; apostrofli so'zlar '"'"' bilan qochirildi. Natija — kanon commit xabari: feat: audit jurnali (faqat qo'shiladi, hash zanjiri, @Audit interceptor), feature flag'lar (foizli rollout, segment, kill switch); /auth/men da rol.
6. Prodakshnda: kesh, ko'p nusxa va tozalash
6.1 30 soniyalik kesh
Kesh har so'rovda bazaga bormaslik uchun kerak. Lekin ilova ikki nusxada (instance) ishlasa nima bo'ladi? Admin birinchi nusxa orqali kill switch'ni bosdi — shu nusxa tozala() qildi, darhol o'chiq. Ikkinchi nusxa esa 30 soniyagacha eski keshdan "yoqiq" deydi. Avariyada 30 soniya — qabul qilsa bo'ladigan narx. Kanon bu kechikishni fayl izohida ochiq yozgan.
Darhol kerak bo'lsa — nusxalarga xabar yuborish: Redis Pub/Sub yoki Postgres'ning LISTEN/NOTIFY i. PUT tugagach NOTIFY bayroqlar, har nusxa LISTEN bayroqlar qilib, xabar kelganda keshini tozalaydi. Kanon buni qilmagan (sinalmagan).
Tekshirib ko'ring: Ilova uch nusxada ishlaydi. Admin 2-nusxa orqali foizni 30 dan 50 ga oshirdi. Keyingi 30 soniyada kim nima ko'radi?
Javob
2-nusxa keshini darhol tozaladi — u 50 % bo'yicha baholaydi. 1- va 3-nusxa eski keshdan hali 30 % deb o'ylaydi. Savati 30–49 bo'lgan foydalanuvchi so'rovi qaysi nusxaga tushishiga qarab goh xususiyatni ko'radi, goh yo'q — 30 soniya ichida "miltillash". Foizni oshirishda bu deyarli zararsiz; kill switch'da esa shu 30 soniya avariya davom etadi.
6.2 Tashqi vositalar
Bayroqlar uchun tayyor yechimlar bor — kanon ularni sinamagan, o'z yupqa qatlamini (0 paket) tanlagan:
- OpenFeature — bayroqlar uchun ochiq standart API (CNCF loyihasi): kod bitta interfeysni chaqiradi, orqasida istalgan provayder.
- Unleash, GrowthBook — o'z serveringizda yoki bulutda ishlaydigan bayroq xizmatlari: veb-panel, segmentlar, tajriba statistikasi.
Qachon o'tish kerak? Bayroqlar o'nlab bo'lsa, ularni dasturchi bo'lmagan odamlar (mahsulot menejeri) boshqarsa, A/B tajribalar statistikasi kerak bo'lsa. Ikki bayroq uchun — o'z jadvalingiz yetarli.
6.3 Eski bayroqlarni tozalash
Har release bayrog'i — vaqtinchalik if. 100 % ga yetib, bir hafta barqaror ishlagandan keyin u qarzga aylanadi: kod ikki yo'lli, testlar ikki holatli, hech kim eski yo'lni sinamaydi. Qoidalar:
- Har bayroqqa egasi va o'chirish sanasi (
tavsifda yoki alohida ustunda). - 100 % ga yetgan release bayrog'i — keyingi sprintda koddan olib tashlanadi, keyin jadvaldan.
- Kill switch'lar (
ai-qadamlar) — uzoq yashaydi, bu normal.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Foydalanuvchi xususiyatni goh ko'radi, goh yo'q | har so'rovda tasodifiy son | deterministik savat |
| Hamma tajribalarga bir xil odamlar tushadi | hash'ga bayroq kaliti kirmagan | kalit:id ni hash'lash |
Tugma yashirin, lekin curl bilan ishlaydi |
bayroq faqat frontend'da | backend'da bayroqKerak() |
| Kill switch bosildi, beta-testerlar baribir ko'rmoqda | ruxsat ro'yxati kill switch'dan ustun | yoqilgan — birinchi tekshiruv |
| Testlarda bayroqli yo'l doim ochiq | tozalash boshlang'ich qatorlarni o'chirgan | testda qatorlarni qayta qo'yish |
Koddagi if lar yillar davomida qoladi |
eski bayroqlar tozalanmaydi | egasi va muddat |
8. Mashqlar
Mashqlar kurs/mashqlar/31/25-bayroqlar/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
- Kodni serverga chiqarish — deploy; xususiyatni foydalanuvchiga ochish —
- Hammaga darhol o'chiradigan bayroq maydoni —
chelak()qaytaradigan eng katta qiymat —- O'chiq xususiyat yo'li qaysi status kodini qaytaradi? —
- Bayroq keshi jarayon xotirasida necha soniya saqlanadi? —
Yechim
- release — bayroq ikkalasini ajratadi.
- yoqilgan —
falsebo'lsa ruxsat ro'yxati ham o'chadi. - 99 —
% 100natijasi 0…99. - 404 — xususiyat mavjudligini oshkor qilmaslik uchun.
- 30 — boshqa nusxalarda o'zgarish 30 soniyagacha kechikadi.
2-mashq (o'rta): nega yoqiq?
Qo'llab-quvvatlash xodimi "nega Aziz yangi xususiyatni ko'ryapti?" deb so'raydi. sabab(b, kim, ch) funksiyasini yozing: baholash bilan bir xil tartibda tekshirib, natijani va sababini qaytarsin: "yoqiq: ruxsat ro'yxati", "yoqiq: rol admin", "yoqiq: chelak 8 < 30", "o'chiq: chelak 44 ≥ 30", "o'chiq: kill switch", "yoqiq: bayroq yo'q". ch — oldindan hisoblangan savat. Ishora: shablon satrlar, includes.
Yechim
function sabab(b, kim, ch) {
if (b === undefined) return "yoqiq: bayroq yo'q";
if (!b.yoqilgan) return "o'chiq: kill switch";
if (b.foydalanuvchilar.includes(kim.id)) {
return "yoqiq: ruxsat ro'yxati";
}
if (kim.rol !== undefined && b.rollar.includes(kim.rol)) {
return `yoqiq: rol ${kim.rol}`;
}
return ch < b.foiz
? `yoqiq: chelak ${ch} < ${b.foiz}`
: `o'chiq: chelak ${ch} ≥ ${b.foiz}`;
}
const b = { yoqilgan: true, foiz: 30, rollar: ["admin"],
foydalanuvchilar: [7] };
console.log(sabab(b, { id: 3 }, 8));
console.log(sabab(b, { id: 1 }, 44));
console.log(sabab(b, { id: 1, rol: "admin" }, 44));
console.log(sabab(b, { id: 7 }, 87));
console.log(sabab({ ...b, yoqilgan: false }, { id: 7 }, 87));
console.log(sabab(undefined, { id: 1 }, 44));Konsolda:
yoqiq: chelak 8 < 30
o'chiq: chelak 44 ≥ 30
yoqiq: rol admin
yoqiq: ruxsat ro'yxati
o'chiq: kill switch
yoqiq: bayroq yo'qSavatlar darsdagi haqiqiy qiymatlar (id 1 → 44, id 3 → 8, id 7 → 87). Bunday "tushuntiruvchi" funksiya admin panel uchun foydali — lekin oddiy foydalanuvchiga qaytarilmaydi: GET /v1/bayroqlar faqat true/false beradi.
3-mashq (qiyin): xavfsiz rollout rejasi
Rollout bosqichlari: 1 → 10 → 30 → 50 → 100 %. Har bosqichdan keyin xato ulushi o'lchanadi. keyingiQadam(joriy, xatoUlushi, chegara) funksiyasini yozing: xato ulushi chegaradan katta bo'lsa — { foiz: 0, holat: "orqaga" } (kill switch emas, foizni nolga); aks holda bosqichlar ro'yxatidagi keyingi foiz va "oldinga"; 100 da — "tugadi". Ro'yxatda yo'q joriy foiz — xato. Keyin soxta o'lchovlar ketma-ketligi bilan simulyatsiya qiling. Ishora: indexOf (massivda qidirish).
Yechim
const BOSQICHLAR = [1, 10, 30, 50, 100];
function keyingiQadam(joriy, xatoUlushi, chegara) {
const i = BOSQICHLAR.indexOf(joriy);
if (i === -1) throw new Error(`noma'lum bosqich: ${joriy}`);
if (xatoUlushi > chegara) return { foiz: 0, holat: "orqaga" };
if (joriy === 100) return { foiz: 100, holat: "tugadi" };
return { foiz: BOSQICHLAR[i + 1], holat: "oldinga" };
}
function simulyatsiya(olchovlar, chegara) {
let foiz = 1;
for (const x of olchovlar) {
const q = keyingiQadam(foiz, x, chegara);
console.log(`${foiz} % da xato ${x} → ${q.holat} ${q.foiz} %`);
if (q.holat !== "oldinga") return;
foiz = q.foiz;
}
}
simulyatsiya([0.001, 0.002, 0.001, 0.003, 0.002], 0.01);
console.log("---");
simulyatsiya([0.001, 0.002, 0.04], 0.01);Konsolda:
1 % da xato 0.001 → oldinga 10 %
10 % da xato 0.002 → oldinga 30 %
30 % da xato 0.001 → oldinga 50 %
50 % da xato 0.003 → oldinga 100 %
100 % da xato 0.002 → tugadi 100 %
---
1 % da xato 0.001 → oldinga 10 %
10 % da xato 0.002 → oldinga 30 %
30 % da xato 0.04 → orqaga 0 %Ikkinchi ssenariyda 30 % bosqichda xato 4 % ga chiqdi — rollout nolga qaytdi va faqat 30 % foydalanuvchi zarar ko'rdi, hamma emas. Bosqichli rollout'ning butun ma'nosi shu. Haqiqiy tizimda xato ulushi Observability darsidagi metrikalardan olinadi.
9. Real ishda
- Qayerda uchraydi: deyarli har katta mahsulotda — yangi xususiyatlar 1 % dan boshlab ochiladi; avariyada kill switch bilan o'chiriladi. "Trunk-based development" — tugallanmagan kod ham asosiy tarmoqda, bayroq ortida (Jamoa workflowlari).
- Intervyu savollari: "Feature flag va konfiguratsiya farqi?", "Foizli rollout'ni qanday deterministik qilasiz?", "Bayroqlar qarzini qanday boshqarasiz?"
- Kod review'da: "bayroq tekshiruvi kvota va LLM chaqiruvidan oldinmi?", "hash'ga bayroq kaliti kiradimi?", "bu release bayrog'ining o'chirish sanasi bormi?"
Xulosa
- Feature flag deploy va release'ni ajratadi: kod serverda, xususiyat bitta katak bilan ochiladi yoki yopiladi.
- Baholash tartibi: kill switch → ruxsat ro'yxati → rol segmenti → foiz; jadvalda yo'q bayroq — cheklanmagan.
- Savat
sha256("kalit:id") % 100— deterministik, monoton (30 → 50 da avvalgilar qoladi), bayroqlararo mustaqil (10 % × 10 % ≈ 101 kishi). - O'chiq xususiyat — 404, tekshiruv handler boshida: kvota va LLM'ga yetib bormaydi; bayroq o'zgarishi auditga tushadi.
- 30 soniyalik kesh — ko'p nusxada kechikish; darhol kerak bo'lsa LISTEN/NOTIFY yoki Pub/Sub. Eski bayroqlarni muddat bilan tozalang.
Keyingi dars: Graceful shutdown to'liq — server o'chayotganda boshlangan so'rovlarni yo'qotmasdan yopishni o'rganamiz.
Manbalar
- OpenFeature — specification: https://openfeature.dev/specification/
- Unleash hujjati — Gradual rollout: https://docs.getunleash.io/concepts/activation-strategies
- GrowthBook hujjati — Feature flags: https://docs.growthbook.io/features/basics
- Martin Fowler — Feature Toggles (aka Feature Flags): https://martinfowler.com/articles/feature-toggles.html
- PostgreSQL 18 hujjati — LISTEN / NOTIFY: https://www.postgresql.org/docs/18/sql-notify.html
- Kanon (IlmHamroh, 2026-11-03): NestJS 12.2.0, PostgreSQL 18.6, Node 24.21.0,
tekshiruv: 327 + 1 skip. SQL bloklari, formatlangan fayllar (tsc, ESLint), testlar, savat modeli va mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!