anbaric-cloud-hosting 1.21.2 → 1.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -52,6 +52,6 @@ export * from "./hosting/handlers/StateMachinesHandler";
52
52
  export * from "./hosting/handlers/auth/AuthorizeCliHandler";
53
53
  export * from "./hosting/handlers/auth/KeysHandler";
54
54
  export * from "./hosting/handlers/auth/WhoamiHandler";
55
- export * from "./queuing/ConfirmableQueue";
55
+ export * from "./queuing/RemoteQueue";
56
56
  export * from "./queuing/ConsumerRegistry";
57
57
  export * from "./queuing/Dispatcher";
@@ -1,4 +1,5 @@
1
- import {Dequeue, QueueMessage} from "anbaric-tsapi";
1
+ import {QueueMessage} from "anbaric-tsapi";
2
+ import {RemoteQueue} from "./RemoteQueue";
2
3
  import {ConsumerRegistry} from "./ConsumerRegistry";
3
4
 
4
5
  class Dispatcher {
@@ -6,7 +7,7 @@ class Dispatcher {
6
7
  private ticker? : NodeJS.Timeout;
7
8
  private draining = false;
8
9
 
9
- constructor(private queue : Dequeue, private registry : ConsumerRegistry,
10
+ constructor(private queue : RemoteQueue, private registry : ConsumerRegistry,
10
11
  private dispatchIntervalMs : number = 1000) {}
11
12
 
12
13
  start() : void {
@@ -20,6 +21,13 @@ class Dispatcher {
20
21
  this.ticker = undefined;
21
22
  }
22
23
 
24
+ /* The queue leases each dequeued message and redelivers any it is not
25
+ `confirm`ed — the consumer confirms once it has processed one. So the
26
+ dispatcher never puts a message back: a failed push just leaves the
27
+ message to redeliver on its next lease. A message with no registered
28
+ listener yet is debounced (backed off), which lets a momentary
29
+ registration race resolve and cancels the message only if it never
30
+ finds a listener. */
23
31
  private async drain() : Promise<void> {
24
32
  if (this.draining) return;
25
33
  this.draining = true;
@@ -30,7 +38,7 @@ class Dispatcher {
30
38
  for (const message of messages) {
31
39
  const url = this.registry.lookup(message.appId, message.workflowId);
32
40
  if (!url) {
33
- await this.queue.enqueue(message.jobId, message.appId, message.workflowId);
41
+ await this.queue.debounce(message);
34
42
  continue;
35
43
  }
36
44
  byConsumerUrl.set(url, [...(byConsumerUrl.get(url) ?? []), message]);
@@ -53,9 +61,7 @@ class Dispatcher {
53
61
  });
54
62
  if (!response.ok) throw new Error(`Consumer at ${url} responded with status ${response.status}`);
55
63
  } catch {
56
- for (const message of batch) {
57
- await this.queue.enqueue(message.jobId, message.appId, message.workflowId);
58
- }
64
+ // Left in the queue: the unconfirmed lease redelivers it for retry.
59
65
  }
60
66
  }
61
67
 
@@ -1,11 +1,12 @@
1
1
  import {QueueMessage} from "anbaric-tsapi";
2
2
  import {Pool} from "pg";
3
- import {ConfirmableQueue} from "./ConfirmableQueue";
3
+ import {RemoteQueue} from "./RemoteQueue";
4
4
 
5
5
  const DEQUEUE_BATCH_SIZE = 100;
6
6
  const LEASE_SECONDS = 30;
7
+ const MAX_ATTEMPTS = 5;
7
8
 
8
- class PostgresQueue implements ConfirmableQueue {
9
+ class PostgresQueue implements RemoteQueue {
9
10
 
10
11
  constructor(private pool : Pool) {}
11
12
 
@@ -23,6 +24,7 @@ class PostgresQueue implements ConfirmableQueue {
23
24
  WHERE position IN (
24
25
  SELECT position FROM queue
25
26
  WHERE (due IS NULL OR due <= now())
27
+ AND (retry_at IS NULL OR retry_at <= now())
26
28
  AND (leased_until IS NULL OR leased_until < now())
27
29
  ORDER BY (due IS NOT NULL), due, position
28
30
  LIMIT $1
@@ -47,6 +49,30 @@ class PostgresQueue implements ConfirmableQueue {
47
49
  await this.pool.query("DELETE FROM queue WHERE position = $1", [message.position]);
48
50
  }
49
51
 
52
+ async debounce(message : QueueMessage) : Promise<void> {
53
+ if (message.position === undefined) return;
54
+ // First bounce backs off 10s (a consumer that is mid-registration is
55
+ // ready by then); later bounces back off a minute. Clearing the lease
56
+ // makes the row eligible again the moment retry_at passes. After enough
57
+ // bounces the message is a genuine orphan, so it is cancelled.
58
+ const result = await this.pool.query(
59
+ `UPDATE queue
60
+ SET attempts = attempts + 1,
61
+ retry_at = now() + (CASE WHEN attempts = 0 THEN interval '10 seconds' ELSE interval '1 minute' END),
62
+ leased_until = NULL
63
+ WHERE position = $1
64
+ RETURNING attempts`,
65
+ [message.position],
66
+ );
67
+ if (Number(result.rows[0]?.attempts) >= MAX_ATTEMPTS) await this.cancel(message);
68
+ }
69
+
70
+ async cancel(message : QueueMessage) : Promise<void> {
71
+ console.warn(`Cancelling undeliverable queue message for job "${message.jobId}" (workflow "${message.workflowId}")`);
72
+ if (message.position === undefined) return;
73
+ await this.pool.query("DELETE FROM queue WHERE position = $1", [message.position]);
74
+ }
75
+
50
76
  async size() : Promise<number> {
51
77
  const result = await this.pool.query("SELECT count(*)::int AS count FROM queue");
52
78
  return result.rows[0].count;
@@ -0,0 +1,17 @@
1
+ import {Dequeue, QueueMessage} from "anbaric-tsapi";
2
+
3
+ interface RemoteQueue extends Dequeue {
4
+
5
+ confirm(message : QueueMessage) : Promise<void>;
6
+ // Push a message that could not be delivered yet (e.g. its consumer has not
7
+ // registered) into the future with a growing back-off, so a momentary race
8
+ // resolves itself. After enough bounces the message is cancelled.
9
+ debounce(message : QueueMessage) : Promise<void>;
10
+ // Discard a message that can never be delivered. Removes it like confirm,
11
+ // but signals an error.
12
+ cancel(message : QueueMessage) : Promise<void>;
13
+ size() : Promise<number>;
14
+
15
+ }
16
+
17
+ export { RemoteQueue }
@@ -1,10 +0,0 @@
1
- import {Dequeue, QueueMessage} from "anbaric-tsapi";
2
-
3
- interface ConfirmableQueue extends Dequeue {
4
-
5
- confirm(message : QueueMessage) : Promise<void>;
6
- size() : Promise<number>;
7
-
8
- }
9
-
10
- export { ConfirmableQueue }