anbaric-cloud-hosting 1.12.0 → 1.14.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-cloud-hosting",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -18,17 +18,17 @@
18
18
  "@aws-sdk/client-s3": "^3.1110.0",
19
19
  "@aws-sdk/client-secrets-manager": "^3.700.0",
20
20
  "@aws-sdk/client-servicediscovery": "^3.1110.0",
21
- "anbaric-data-store": "^1.12.0",
22
- "anbaric-plugins": "^1.12.0",
23
- "anbaric-tsapi": "^1.12.0",
21
+ "anbaric-data-store": "^1.14.0",
22
+ "anbaric-plugins": "^1.14.0",
23
+ "anbaric-tsapi": "^1.14.0",
24
24
  "esbuild": "^0.28.2",
25
25
  "pg": "^8.16.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",
29
29
  "@types/pg": "^8.15.0",
30
- "anbaric-impl-cloud": "^1.12.0",
31
- "anbaric-state-machine": "^1.12.0",
30
+ "anbaric-impl-cloud": "^1.14.0",
31
+ "anbaric-state-machine": "^1.14.0",
32
32
  "tsx": "^4.20.0",
33
33
  "typescript": "^7.0.2"
34
34
  },
@@ -4,7 +4,7 @@ import {AuditFilter, AuditRecordStore} from "../auditing/AuditRecordStore";
4
4
 
5
5
  /* The writes worth keeping in the durable trail; reads (READ, LIST, QUERY) are
6
6
  dropped by default. Interactions are plain strings chosen by each store. */
7
- const DEFAULT_WRITE_MASK : Array<string> = ["CREATE", "SAVE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE", "EXECUTE"];
7
+ const DEFAULT_WRITE_MASK : Array<string> = ["CREATE", "SAVE", "UPDATE_PROPERTIES", "CHANGE_STATE", "DELETE", "EXECUTE", "KILL"];
8
8
 
9
9
  /* Persists audit records, subject to a write mask: only the interactions in
10
10
  the mask are stored, so high-volume reads can be audited at the call site
@@ -9,9 +9,10 @@ type JobRow = {
9
9
  started_at : Date,
10
10
  started_by : string,
11
11
  last_updated : Date,
12
+ killed : boolean,
12
13
  };
13
14
 
14
- const JOB_COLUMNS = "id, state, properties, workflow_id, started_at, started_by, last_updated";
15
+ const JOB_COLUMNS = "id, state, properties, workflow_id, started_at, started_by, last_updated, killed";
15
16
 
16
17
  class PostgresJobPersistence extends JobPersistence {
17
18
 
@@ -21,12 +22,12 @@ class PostgresJobPersistence extends JobPersistence {
21
22
 
22
23
  protected async saveInternal(job : Job) : Promise<void> {
23
24
  await this.pool.query(
24
- `INSERT INTO jobs (id, state, properties, workflow_id, started_at, started_by, last_updated)
25
- VALUES ($1, $2, $3, $4, $5, $6, $7)
25
+ `INSERT INTO jobs (id, state, properties, workflow_id, started_at, started_by, last_updated, killed)
26
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
26
27
  ON CONFLICT (id) DO UPDATE SET state = EXCLUDED.state, properties = EXCLUDED.properties,
27
- workflow_id = EXCLUDED.workflow_id, last_updated = EXCLUDED.last_updated`,
28
+ workflow_id = EXCLUDED.workflow_id, last_updated = EXCLUDED.last_updated, killed = EXCLUDED.killed`,
28
29
  [job.id, job.state, Object.fromEntries(job.properties), job.workflowId,
29
- job.startedAt, job.startedBy, job.lastUpdated],
30
+ job.startedAt, job.startedBy, job.lastUpdated, job.killed],
30
31
  );
31
32
  }
32
33
 
@@ -53,6 +54,25 @@ class PostgresJobPersistence extends JobPersistence {
53
54
  return result.rows.map(row => this.deserializeRow(row));
54
55
  }
55
56
 
57
+ protected async killInternal(id : string) : Promise<void> {
58
+ await this.pool.query("UPDATE jobs SET killed = true, last_updated = now() WHERE id = $1", [id]);
59
+ }
60
+
61
+ protected async killOlderThanInternal(lastUpdatedBefore : Date) : Promise<number> {
62
+ const result = await this.pool.query(
63
+ "UPDATE jobs SET killed = true, last_updated = now() WHERE last_updated < $1 AND killed = false",
64
+ [lastUpdatedBefore],
65
+ );
66
+ return result.rowCount ?? 0;
67
+ }
68
+
69
+ protected async countByStateInternal() : Promise<Array<JobPersistence.StateCount>> {
70
+ const result = await this.pool.query(
71
+ "SELECT state, killed, count(*)::int AS count FROM jobs GROUP BY state, killed",
72
+ );
73
+ return result.rows.map(row => ({ state: row.state, killed: row.killed, count: row.count }));
74
+ }
75
+
56
76
  private deserializeRow(row : JobRow) : Job {
57
77
  return deserializeJob({
58
78
  ...row,
@@ -60,6 +80,7 @@ class PostgresJobPersistence extends JobPersistence {
60
80
  startedAt: row.started_at.toISOString(),
61
81
  startedBy: row.started_by,
62
82
  lastUpdated: row.last_updated.toISOString(),
83
+ killed: row.killed,
63
84
  });
64
85
  }
65
86
 
@@ -22,6 +22,7 @@ const ensureSchema = async (pool : Pool) : Promise<void> => {
22
22
  await pool.query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS started_by TEXT NOT NULL DEFAULT 'system'");
23
23
  await pool.query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS last_updated TIMESTAMPTZ NOT NULL DEFAULT now()");
24
24
  await pool.query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS transitions JSONB NOT NULL DEFAULT '[]'");
25
+ await pool.query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS killed BOOLEAN NOT NULL DEFAULT false");
25
26
  await pool.query(`
26
27
  CREATE TABLE IF NOT EXISTS documents (
27
28
  collection TEXT NOT NULL,
@@ -7,6 +7,21 @@ class JobsHandler implements RequestHandler {
7
7
  constructor(private persistence : JobPersistence) {}
8
8
 
9
9
  async handle(request : Request) : Promise<void> {
10
+ if (request.id === "stats" && !request.subresource) {
11
+ if (request.method !== "GET") return request.notFound();
12
+ return request.reply(200, { states: await this.persistence.countByState(SystemActor.actor) });
13
+ }
14
+ if (request.id === "kill-old" && !request.subresource) {
15
+ if (request.method !== "POST") return request.notFound();
16
+ const { before } = await request.body();
17
+ const killed = await this.persistence.killOlderThan(new Date(before), SystemActor.actor);
18
+ return request.reply(200, { killed });
19
+ }
20
+ if (request.id && request.subresource === "kill") {
21
+ if (request.method !== "POST") return request.notFound();
22
+ await this.persistence.kill(request.id, SystemActor.actor);
23
+ return request.reply(204);
24
+ }
10
25
  if (request.subresource) return request.notFound();
11
26
  if (request.id) return this.handleJob(request, request.id);
12
27
  return this.handleCollection(request);
@@ -7,7 +7,13 @@ class QueueHandler implements RequestHandler {
7
7
  constructor(private queue : ConfirmableQueue) {}
8
8
 
9
9
  async handle(request : Request) : Promise<void> {
10
- if (request.method !== "POST" || !request.id || request.subresource) return request.notFound();
10
+ if (request.subresource) return request.notFound();
11
+
12
+ if (request.method === "GET" && request.id === "size") {
13
+ return request.reply(200, { size: await this.queue.size() });
14
+ }
15
+
16
+ if (request.method !== "POST" || !request.id) return request.notFound();
11
17
 
12
18
  switch (request.id) {
13
19
  case "enqueue": {
@@ -3,6 +3,7 @@ import {Dequeue, QueueMessage} from "anbaric-tsapi";
3
3
  interface ConfirmableQueue extends Dequeue {
4
4
 
5
5
  confirm(message : QueueMessage) : Promise<void>;
6
+ size() : Promise<number>;
6
7
 
7
8
  }
8
9
 
@@ -47,6 +47,11 @@ class PostgresQueue implements ConfirmableQueue {
47
47
  await this.pool.query("DELETE FROM queue WHERE position = $1", [message.position]);
48
48
  }
49
49
 
50
+ async size() : Promise<number> {
51
+ const result = await this.pool.query("SELECT count(*)::int AS count FROM queue");
52
+ return result.rows[0].count;
53
+ }
54
+
50
55
  }
51
56
 
52
57
  export { PostgresQueue }