anbaric-cloud-hosting 1.16.0 → 1.16.1

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.16.0",
3
+ "version": "1.16.1",
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.16.0",
22
- "anbaric-plugins": "^1.16.0",
23
- "anbaric-tsapi": "^1.16.0",
21
+ "anbaric-data-store": "^1.16.1",
22
+ "anbaric-plugins": "^1.16.1",
23
+ "anbaric-tsapi": "^1.16.1",
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.16.0",
31
- "anbaric-state-machine": "^1.16.0",
30
+ "anbaric-impl-cloud": "^1.16.1",
31
+ "anbaric-state-machine": "^1.16.1",
32
32
  "tsx": "^4.20.0",
33
33
  "typescript": "^7.0.2"
34
34
  },
@@ -31,11 +31,24 @@ abstract class BaseBuildLayer implements BuildLayer {
31
31
 
32
32
  protected deployments = new Map<string, Deployment>();
33
33
  private nextAppIndex = 0;
34
+ private hydration? : Promise<void>;
34
35
 
35
36
  constructor(protected appsDir : string, private consumerPortBase : number = 8800,
36
37
  private probe : Probe = adminProbe,
37
38
  private livenessTimeoutMs : number = LIVENESS_TIMEOUT_MS) {}
38
39
 
40
+ /* The deployments map is rebuilt from the durable backend the first time the
41
+ app registry is read, so apps survive a platform restart. A failed
42
+ rehydration is not cached, so a later request retries. */
43
+ ensureHydrated() : Promise<void> {
44
+ return this.hydration ??= this.rehydrate().catch(error => {
45
+ this.hydration = undefined;
46
+ console.warn(`Could not rehydrate the app registry: ${error instanceof Error ? error.message : error}`);
47
+ });
48
+ }
49
+
50
+ protected async rehydrate() : Promise<void> {}
51
+
39
52
  deploy(appName : string, appPort : number, tarball : Buffer) : DeploymentSummary {
40
53
  const existing = this.deployments.get(appName);
41
54
 
@@ -9,6 +9,7 @@ type DeploymentSummary = {
9
9
 
10
10
  interface BuildLayer {
11
11
 
12
+ ensureHydrated() : Promise<void>;
12
13
  deploy(appName : string, appPort : number, tarball : Buffer) : DeploymentSummary;
13
14
  status(appName : string) : (DeploymentSummary & { log : Array<string> }) | undefined;
14
15
  list() : Array<DeploymentSummary>;
@@ -2,6 +2,7 @@ import {readFile, writeFile} from "node:fs/promises";
2
2
  import {join} from "node:path";
3
3
  import {CodeBuildClient, BatchGetBuildsCommand, StartBuildCommand} from "@aws-sdk/client-codebuild";
4
4
  import {ECSClient, CreateServiceCommand, DeleteServiceCommand, DescribeServicesCommand,
5
+ DescribeTaskDefinitionCommand, ListServicesCommand as ListEcsServicesCommand,
5
6
  RegisterTaskDefinitionCommand, UpdateServiceCommand} from "@aws-sdk/client-ecs";
6
7
  import {S3Client, PutObjectCommand} from "@aws-sdk/client-s3";
7
8
  import {ServiceDiscoveryClient, CreateServiceCommand as CreateDiscoveryServiceCommand,
@@ -255,6 +256,67 @@ class FargateBuildLayer extends BaseBuildLayer {
255
256
  }));
256
257
  }
257
258
 
259
+ /* ECS is the durable record of what is deployed, so on the first read after
260
+ a restart the registry is rebuilt from the running anbaric-app-* services
261
+ and their task definitions (ports come from the container environment the
262
+ platform set at deploy time). */
263
+ protected async rehydrate() : Promise<void> {
264
+ const serviceArns = await this.appServiceArns();
265
+
266
+ for (let batch = 0; batch < serviceArns.length; batch += 10) {
267
+ const {services} = await this.aws.ecs.send(new DescribeServicesCommand({
268
+ cluster: this.options.cluster,
269
+ services: serviceArns.slice(batch, batch + 10),
270
+ }));
271
+ for (const service of services ?? []) {
272
+ if (service.status !== "ACTIVE" || !service.serviceName?.startsWith("anbaric-app-")) continue;
273
+ const appName = service.serviceName.slice("anbaric-app-".length);
274
+ if (this.deployments.has(appName)) continue;
275
+ const deployment = await this.deploymentFromService(appName, service);
276
+ if (deployment) this.deployments.set(appName, deployment);
277
+ }
278
+ }
279
+ }
280
+
281
+ private async appServiceArns() : Promise<Array<string>> {
282
+ const arns : Array<string> = [];
283
+ let nextToken : string | undefined;
284
+ do {
285
+ const page = await this.aws.ecs.send(new ListEcsServicesCommand({ cluster: this.options.cluster, nextToken }));
286
+ for (const arn of page.serviceArns ?? []) {
287
+ if (arn.includes("/anbaric-app-")) arns.push(arn);
288
+ }
289
+ nextToken = page.nextToken;
290
+ } while (nextToken);
291
+ return arns;
292
+ }
293
+
294
+ private async deploymentFromService(appName : string, service : { taskDefinition : string, runningCount? : number }) : Promise<Deployment | undefined> {
295
+ const {taskDefinition} = await this.aws.ecs.send(new DescribeTaskDefinitionCommand({ taskDefinition: service.taskDefinition }));
296
+ const container = taskDefinition?.containerDefinitions?.[0];
297
+ if (!container) return undefined;
298
+
299
+ const environment = new Map<string, string>(
300
+ (container.environment ?? []).map((entry : { name : string, value : string }) => [entry.name, entry.value]),
301
+ );
302
+ const port = (name : string, fallback : number) => {
303
+ const value = Number(environment.get(name));
304
+ return Number.isInteger(value) ? value : fallback;
305
+ };
306
+ const appPort = Number(environment.get("PORT"));
307
+ if (!Number.isInteger(appPort)) return undefined;
308
+
309
+ return {
310
+ appName,
311
+ status: (service.runningCount ?? 0) > 0 ? "running" : "stopped",
312
+ appPort,
313
+ appHost: this.appHostFor(appName),
314
+ adminPort: port("ANBARIC_ADMIN_PORT", 8791),
315
+ consumerPort: port("ANBARIC_CONSUMER_PORT", 0),
316
+ log: [],
317
+ };
318
+ }
319
+
258
320
  private serviceNameFor(appName : string) : string {
259
321
  return `anbaric-app-${appName}`;
260
322
  }
@@ -7,6 +7,7 @@ class AppsHandler implements RequestHandler {
7
7
  constructor(private buildLayer : BuildLayer) {}
8
8
 
9
9
  async handle(request : Request) : Promise<void> {
10
+ await this.buildLayer.ensureHydrated();
10
11
  switch (request.subresource) {
11
12
  case "deploy":
12
13
  if (request.id) return this.handleDeploy(request, request.id);