File size: 1,696 Bytes
9b906ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * A FIFO promise semaphore: at most `limit` wrapped tasks run at once; the
 * rest wait in arrival order. Wrap a queryFn's work in `run` to bound how many
 * of a query family's requests are in flight simultaneously.
 */
export class ConcurrencyLimiter {
  private active = 0;

  private readonly queue: Array<() => void> = [];

  constructor(private readonly limit: number) {}

  async run<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
    await this.acquire();
    try {
      // A query cancelled while queued (e.g. its last observer unmounted)
      // frees its slot without ever hitting the network.
      if (signal?.aborted) {
        throw signal.reason ?? new DOMException("Aborted", "AbortError");
      }
      return await task();
    } finally {
      this.release();
    }
  }

  private acquire(): Promise<void> {
    if (this.active < this.limit) {
      this.active += 1;
      return Promise.resolve();
    }
    return new Promise((resolve) => {
      this.queue.push(resolve);
    });
  }

  private release(): void {
    const next = this.queue.shift();
    if (next) {
      // The waiter inherits the released slot directly, so `active` stays put.
      next();
    } else {
      this.active -= 1;
    }
  }
}

/**
 * Shared by every per-automation runs query: the dashboard and home page each
 * fan out one GET .../runs request per automation, which can exhaust the
 * automation service's DB connection pool (~15 connections) and stall every
 * request for the pool's 30s acquisition timeout. Three in flight keeps the
 * surfaces responsive without saturating the pool.
 */
export const automationRunRequestsLimiter = new ConcurrencyLimiter(3);