← Усі задачіNode.js · Senior

Circuit breaker

Коли залежний сервіс лежить, кожен запит до нього чекає таймаут і забирає ресурси. Напишіть createBreaker(fn, { failureThreshold, resetMs, now }) з методами call(...args) і state().

- closed: виклики йдуть у fn; після failureThreshold помилок поспіль автомат переходить в open (успіх скидає лічильник). - open: call одразу відхиляється з Circuit open, fn не викликається. - Через resetMs після відкриття стан - half-open: пропускається один пробний виклик (інші паралельні отримують Circuit open). Успіх закриває автомат, помилка знову відкриває його на resetMs.

Тести (6)

  1. let t = 0;
    const b = createBreaker(async (x) => x * 2, { failureThreshold: 3, resetMs: 1000, now: () => t });
    expect(await b.call(21)).toBe(42);
    expect(b.state()).toBe('closed');
  2. let t = 0;
    const f = fn(async () => { throw new Error('timeout'); });
    const b = createBreaker(f, { failureThreshold: 3, resetMs: 1000, now: () => t });
    for (let i = 0; i < 3; i++) await expect(b.call()).rejects.toThrow('timeout');
    expect(b.state()).toBe('open');
    await expect(b.call()).rejects.toThrow('Circuit open');
    expect(f).toHaveBeenCalledTimes(3);
  3. let t = 0;
    let fail = true;
    const b = createBreaker(async () => { if (fail) throw new Error('down'); return 'ok'; }, { failureThreshold: 2, resetMs: 1000, now: () => t });
    await expect(b.call()).rejects.toThrow('down');
    await expect(b.call()).rejects.toThrow('down');
    t = 999;
    expect(b.state()).toBe('open');
    t = 1000;
    expect(b.state()).toBe('half-open');
    fail = false;
    expect(await b.call()).toBe('ok');
    expect(b.state()).toBe('closed');
  4. let t = 0;
    const b = createBreaker(async () => { throw new Error('down'); }, { failureThreshold: 1, resetMs: 500, now: () => t });
    await expect(b.call()).rejects.toThrow('down');
    t = 500;
    await expect(b.call()).rejects.toThrow('down');
    expect(b.state()).toBe('open');
    t = 900;
    await expect(b.call()).rejects.toThrow('Circuit open');
  5. let t = 0;
    let ok = false;
    const b = createBreaker(async () => { if (!ok) throw new Error('down'); await wait(30); return 'ok'; }, { failureThreshold: 1, resetMs: 100, now: () => t });
    await expect(b.call()).rejects.toThrow('down');
    t = 100;
    ok = true;
    const first = b.call();
    await expect(b.call()).rejects.toThrow('Circuit open');
    expect(await first).toBe('ok');
  6. let t = 0;
    let n = 0;
    const b = createBreaker(async () => { n++; if (n % 2) throw new Error('flaky'); return n; }, { failureThreshold: 2, resetMs: 1000, now: () => t });
    await expect(b.call()).rejects.toThrow('flaky');
    expect(await b.call()).toBe(2);
    await expect(b.call()).rejects.toThrow('flaky');
    expect(b.state()).toBe('closed');