@lde/distribution-monitor 0.1.13 → 0.1.14

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/dist/store.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
1
2
  import type { ObservationStore, Observation } from './types.js';
2
3
  /**
3
4
  * PostgreSQL implementation of the ObservationStore interface.
@@ -24,4 +25,26 @@ export declare class PostgresObservationStore implements ObservationStore {
24
25
  store(observation: Omit<Observation, 'id'>): Promise<Observation>;
25
26
  refreshLatestObservationsView(): Promise<void>;
26
27
  }
28
+ /**
29
+ * Create the unique index that `REFRESH ... CONCURRENTLY` requires on the
30
+ * `latest_observations` materialized view. drizzle's `pgMaterializedView` does
31
+ * not model it, so `pushSchema` never emits it; create it idempotently here —
32
+ * `IF NOT EXISTS` matches by name and skips (no rebuild) when it already exists.
33
+ *
34
+ * The statement takes a SHARE lock on the view, which conflicts with the
35
+ * EXCLUSIVE lock held by a `REFRESH ... CONCURRENTLY` running in another
36
+ * instance. During a rolling deploy the old and new pods overlap, so that wait
37
+ * can exceed `lock_timeout` and raise `55P03` (lock_not_available). Tolerate it:
38
+ * whenever the view is being refreshed the index already exists, so a failed
39
+ * re-check is harmless — far better than aborting startup and crash-looping the
40
+ * monitor. Re-throw anything else.
41
+ */
42
+ export declare function ensureLatestObservationsIndex(db: Pick<PostgresJsDatabase, 'execute'>): Promise<void>;
43
+ /**
44
+ * Whether an error is (or wraps) the PostgreSQL `lock_not_available` (`55P03`)
45
+ * SQLSTATE, raised when a statement exceeds `lock_timeout` waiting for a
46
+ * contended lock. drizzle wraps the driver error, so the SQLSTATE lives on a
47
+ * nested `cause` rather than the top-level error.
48
+ */
49
+ export declare function isLockNotAvailable(error: unknown): boolean;
27
50
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgBhE;;GAEG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO;IAYP;;;;;;;;;;;OAWG;WACU,MAAM,CACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC;IA6C9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAK9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAS5C,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrD"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgBhE;;GAEG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO;IAYP;;;;;;;;;;;OAWG;WACU,MAAM,CACjB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC;IAsC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAK9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAS5C,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjE,6BAA6B,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAU1D"}
package/dist/store.js CHANGED
@@ -62,12 +62,7 @@ export class PostgresObservationStore {
62
62
  for (const statement of sqlStatements) {
63
63
  await store.db.execute(sql.raw(statement));
64
64
  }
65
- // The unique index on the materialized view is required for
66
- // REFRESH ... CONCURRENTLY but is not modelled by drizzle's
67
- // `pgMaterializedView`, so `pushSchema` never emits it. Create it
68
- // idempotently — `IF NOT EXISTS` matches by name and skips (no rebuild) when
69
- // it already exists.
70
- await store.db.execute(sql `CREATE UNIQUE INDEX IF NOT EXISTS latest_observations_monitor_idx ON latest_observations (monitor)`);
65
+ await ensureLatestObservationsIndex(store.db);
71
66
  return store;
72
67
  }
73
68
  async close() {
@@ -97,3 +92,42 @@ export class PostgresObservationStore {
97
92
  await this.db.execute(refreshLatestObservationsViewSql);
98
93
  }
99
94
  }
95
+ /**
96
+ * Create the unique index that `REFRESH ... CONCURRENTLY` requires on the
97
+ * `latest_observations` materialized view. drizzle's `pgMaterializedView` does
98
+ * not model it, so `pushSchema` never emits it; create it idempotently here —
99
+ * `IF NOT EXISTS` matches by name and skips (no rebuild) when it already exists.
100
+ *
101
+ * The statement takes a SHARE lock on the view, which conflicts with the
102
+ * EXCLUSIVE lock held by a `REFRESH ... CONCURRENTLY` running in another
103
+ * instance. During a rolling deploy the old and new pods overlap, so that wait
104
+ * can exceed `lock_timeout` and raise `55P03` (lock_not_available). Tolerate it:
105
+ * whenever the view is being refreshed the index already exists, so a failed
106
+ * re-check is harmless — far better than aborting startup and crash-looping the
107
+ * monitor. Re-throw anything else.
108
+ */
109
+ export async function ensureLatestObservationsIndex(db) {
110
+ try {
111
+ await db.execute(sql `CREATE UNIQUE INDEX IF NOT EXISTS latest_observations_monitor_idx ON latest_observations (monitor)`);
112
+ }
113
+ catch (error) {
114
+ if (!isLockNotAvailable(error)) {
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Whether an error is (or wraps) the PostgreSQL `lock_not_available` (`55P03`)
121
+ * SQLSTATE, raised when a statement exceeds `lock_timeout` waiting for a
122
+ * contended lock. drizzle wraps the driver error, so the SQLSTATE lives on a
123
+ * nested `cause` rather than the top-level error.
124
+ */
125
+ export function isLockNotAvailable(error) {
126
+ if (typeof error !== 'object' || error === null) {
127
+ return false;
128
+ }
129
+ if ('code' in error && error.code === '55P03') {
130
+ return true;
131
+ }
132
+ return ('cause' in error && isLockNotAvailable(error.cause));
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/distribution-monitor",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Monitor DCAT distributions (SPARQL endpoints and data dumps) with periodic probes",
5
5
  "repository": {
6
6
  "url": "git+https://github.com/ldelements/lde.git",