IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya25/34-dars29 daqiqa
Mundarija (30)

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-api da 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-api ga 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:

  1. Kill switch hammadan ustun. Avariyada "hammaga o'chir" deganda, ruxsat ro'yxatidagi beta-testerlar ham o'chadi. Aks holda kill switch yarim ishlaydi.
  2. Ruxsat ro'yxati foizdan ustun. Foiz 0 bo'lsa ham, ro'yxatdagi foydalanuvchi xususiyatni ko'radi — ichki sinov uchun.
  3. 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 yoqilgan ni false qildi. 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:

js
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:

text
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: 101

getUint32(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:

  1. Deterministik. id 3 — savat 8, har safar 8. id 7 ning savati 87 (30 dan katta), lekin u ruxsat ro'yxatida — yoqiq.
  2. 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.
  3. 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 id hash'lansa, "eng omadli 10 %" hamma tajribaga tushardi va natijalar aralashib ketardi.

Taqsimot qanchalik tekis? 10 000 foydalanuvchida turli foizlar uchun yoqilganlar soni:

10 000 foydalanuvchidan nechtasi yoqiq (ai-qadamlar kaliti)
  • 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:

sql
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;
text
 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):

sql
-- 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';
text
CREATE TABLE
INSERT 0 2
GRANT
GRANT
COMMENT

CHECK cheklovlari kalit formatini va foiz oralig'ini baza darajasida ushlaydi — API'dagi Zod sxemasi bilan bir xil qoida, ikki qatlamda:

sql
\set VERBOSITY terse
UPDATE bayroqlar SET foiz = 101 WHERE kalit = 'ai-qadamlar';
SELECT kalit, yoqilgan, foiz FROM bayroqlar ORDER BY kalit;
text
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):

prisma
/// 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:

ts
// 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:

  1. chelak() va baholash() — brauzerda sinagan funksiyalarimiz, Node versiyasi. Toza funksiyalar — test qilish oson.
  2. BayroqlarXizmati — jadvalni o'qib, jarayon xotirasida 30 soniya saqlaydi. Har so'rovda bazaga bormaydi. tozala() keshni darhol bo'shatadi.
  3. bayroqKerak() — AI yo'llari chaqiradigan yordamchi. O'chiq bo'lsa — 404 xususiyat-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 Problem title lari o'zbekcha).
  4. BayroqlarController — GET /v1/bayroqlar: joriy foydalanuvchi uchun natijalar ({ "ai-qadamlar": true }), qoidalar emas. Frontend foizni va ruxsat ro'yxatini ko'rmaydi.
  5. AdminBayroqlarController — PUT /v1/admin/bayroqlar/:kalit (upsert). @Audit("bayroq.ozgartirildi", "bayroq") — oldingi darsdagi dekorator: kim, qachon, qaysi bayroqni o'zgartirgani jurnalga tushadi.
  6. BayroqlarModule.forRoot() — dinamik modul: auth bo'lmasa (xotira rejimi) xizmat null — 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:

ts
/** 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):

ts
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:

ts
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:

ts
    // 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:

diff
- { 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:

ts
import { afterAll, beforeEach, describe, test } from "vitest";
import { baholash, chelak } from "../src/bayroq/bayroqlar.ts";

Bazasiz describe — fayl boshida, authBaza dan oldin:

ts
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:

ts
    // 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:

ts
  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):

text
 ✓ |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:

text
| `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:

bash
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 (tavsif da 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 — false bo'lsa ruxsat ro'yxati ham o'chadi.
  • 99 — % 100 natijasi 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
js
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:

text
yoqiq: chelak 8 < 30
o'chiq: chelak 44 ≥ 30
yoqiq: rol admin
yoqiq: ruxsat ro'yxati
o'chiq: kill switch
yoqiq: bayroq yo'q

Savatlar 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
js
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:

text
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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Feature flag'lar backend'da: foizli rollout, segmentlar va kill switch — IlmHamroh