@onlineapps/service-wrapper 2.3.0 → 2.3.2

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": "@onlineapps/service-wrapper",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -30,10 +30,10 @@
30
30
  "@onlineapps/conn-infra-error-handler": "1.0.9",
31
31
  "@onlineapps/conn-infra-mq": "1.1.69",
32
32
  "@onlineapps/conn-orch-api-mapper": "1.0.30",
33
- "@onlineapps/conn-orch-cookbook": "2.0.37",
34
- "@onlineapps/conn-orch-orchestrator": "1.0.105",
35
- "@onlineapps/conn-orch-registry": "1.1.54",
36
- "@onlineapps/conn-orch-validator": "2.0.31",
33
+ "@onlineapps/conn-orch-cookbook": "2.0.38",
34
+ "@onlineapps/conn-orch-orchestrator": "1.0.106",
35
+ "@onlineapps/conn-orch-registry": "1.1.55",
36
+ "@onlineapps/conn-orch-validator": "2.0.32",
37
37
  "@onlineapps/monitoring-core": "1.0.21",
38
38
  "@onlineapps/service-common": "1.0.18",
39
39
  "@onlineapps/runtime-config": "1.0.2"
@@ -44,7 +44,7 @@ const CONFIG_REGISTRY = {
44
44
  description: 'Operations specification (API contract)'
45
45
  },
46
46
  VALIDATION_PROOF: {
47
- paths: ['config/runtime/validation-proof.json', 'conn-runtime/validation-proof.json'],
47
+ paths: ['conn-runtime/validation-proof.json'],
48
48
  required: false,
49
49
  description: 'Pre-validation proof from cookbook tests'
50
50
  }
@@ -25,9 +25,20 @@ const ErrorHandlerConnector = require('@onlineapps/conn-infra-error-handler');
25
25
  const { ValidationOrchestrator } = require('@onlineapps/conn-orch-validator');
26
26
  const runtimeCfg = require('./config');
27
27
 
28
+ const RUNTIME_DIR = 'conn-runtime';
29
+ const PROOF_RELATIVE_PATH = `${RUNTIME_DIR}/validation-proof.json`;
30
+
28
31
  const REVALIDATION_FAST_BACKOFF_MS = [30000, 60000, 120000, 300000, 600000, 1800000];
29
- const REVALIDATION_DAILY_MS = 86400000;
30
- const REVALIDATION_STATE = { IDLE: 'idle', FAST_BACKOFF: 'fast_backoff', DAILY_BACKOFF: 'daily_backoff', SUCCEEDED: 'succeeded' };
32
+ const REVALIDATION_HOURLY_MS = 3600000;
33
+ const REVALIDATION_HOURLY_MAX = 24;
34
+ const REVALIDATION_SLOW_MS = 21600000;
35
+ const REVALIDATION_STATE = {
36
+ IDLE: 'idle',
37
+ FAST_BACKOFF: 'fast_backoff',
38
+ HOURLY_BACKOFF: 'hourly_backoff',
39
+ SLOW_BACKOFF: 'slow_backoff',
40
+ SUCCEEDED: 'succeeded'
41
+ };
31
42
 
32
43
  const INFRA_QUEUE_OWNERS = {
33
44
  'workflow.init': 'Gateway (api_gateway)',
@@ -82,7 +93,6 @@ class ServiceWrapper {
82
93
  this._revalidationState = REVALIDATION_STATE.IDLE;
83
94
  this._revalidationAttempt = 0;
84
95
  this._revalidationTimer = null;
85
- this._infraInvalidationConsumerTag = null;
86
96
  }
87
97
 
88
98
  /**
@@ -163,14 +173,8 @@ class ServiceWrapper {
163
173
  throw new Error(`Failed to start workflow listeners for ${serviceName}: ${listenerError.message}`);
164
174
  }
165
175
 
166
- // Phase 0.10: Subscribe to infrastructure invalidation events
167
- try {
168
- await this._subscribeToInfraInvalidation(serviceName);
169
- } catch (subErr) {
170
- this.logger?.warn('[ServiceWrapper][infra.invalidation] Failed to subscribe to invalidation events (non-critical)', {
171
- error: subErr.message
172
- });
173
- }
176
+ // Revalidation requests from Registry are handled via RegistryClient 'revalidate' event
177
+ // (subscribed in _initializeRegistry after successful registration)
174
178
  }
175
179
 
176
180
  /**
@@ -439,7 +443,6 @@ class ServiceWrapper {
439
443
  this.workflowInitConsumerTag = null;
440
444
  this.workflowControlConsumerTag = null;
441
445
  this.serviceWorkflowConsumerTag = null;
442
- this._infraInvalidationConsumerTag = null;
443
446
  if (this._revalidationTimer) {
444
447
  clearTimeout(this._revalidationTimer);
445
448
  this._revalidationTimer = null;
@@ -997,6 +1000,15 @@ class ServiceWrapper {
997
1000
  throw new Error(`Failed to register service ${serviceName}: ${error.message}`);
998
1001
  }
999
1002
 
1003
+ // Listen for revalidation requests from Registry (infra version changes)
1004
+ this.registryClient.on('revalidate', (payload) => {
1005
+ this.logger?.info('[ServiceWrapper][revalidate] Revalidation requested by Registry', {
1006
+ reason: payload.reason,
1007
+ infraFingerprint: payload.infraFingerprint?.substring(0, 16)
1008
+ });
1009
+ this._deleteProofAndRevalidate();
1010
+ });
1011
+
1000
1012
  // Start heartbeat with fail-fast on persistent MQ failure
1001
1013
  const heartbeatInterval = this.config.wrapper?.registry?.heartbeatInterval;
1002
1014
  if (typeof heartbeatInterval !== 'number' || Number.isNaN(heartbeatInterval) || heartbeatInterval <= 0) {
@@ -1271,7 +1283,7 @@ class ServiceWrapper {
1271
1283
  return { status: 'no_service_root' };
1272
1284
  }
1273
1285
 
1274
- const proofPath = path.join(this.serviceRoot, 'conn-runtime', 'validation-proof.json');
1286
+ const proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
1275
1287
 
1276
1288
  if (!fs.existsSync(proofPath)) {
1277
1289
  return { status: 'no_proof' };
@@ -1845,67 +1857,23 @@ class ServiceWrapper {
1845
1857
  }
1846
1858
 
1847
1859
  /**
1848
- * Subscribe to infra.invalidation events on the infrastructure.health.events fanout exchange.
1849
- * When received, deletes local validation proof and triggers re-validation with backoff.
1860
+ * Delete local validation proof and trigger re-validation.
1861
+ * Called when Registry sends a revalidation request (infra version change).
1850
1862
  * @private
1851
1863
  */
1852
- async _subscribeToInfraInvalidation(serviceName) {
1853
- if (!this.mqClient?._transport?.channel) {
1854
- this.logger?.debug('[ServiceWrapper][infra.invalidation] No MQ channel available — skipping subscription');
1855
- return;
1856
- }
1857
-
1858
- const channel = this.mqClient._transport.channel;
1859
- const exchangeName = 'infrastructure.health.events';
1860
-
1861
- await channel.assertExchange(exchangeName, 'fanout', { durable: true });
1862
-
1863
- const q = await channel.assertQueue('', { exclusive: true, autoDelete: true });
1864
-
1865
- await channel.bindQueue(q.queue, exchangeName, '');
1866
-
1867
- const { consumerTag } = await channel.consume(q.queue, async (msg) => {
1868
- if (!msg) return;
1869
-
1870
- try {
1871
- const event = JSON.parse(msg.content.toString());
1872
-
1873
- if (event.type !== 'infra.invalidation') {
1874
- channel.ack(msg);
1875
- return;
1876
- }
1877
-
1878
- this.logger?.info('[ServiceWrapper][infra.invalidation] Infrastructure change detected, scheduling re-validation', {
1879
- fingerprint: event.fingerprint?.substring(0, 16) + '...',
1880
- timestamp: event.timestamp
1881
- });
1882
-
1883
- // Delete local validation proof
1884
- const fs = require('fs');
1885
- const path = require('path');
1886
- if (this.serviceRoot) {
1887
- const proofPath = path.join(this.serviceRoot, 'conn-runtime', 'validation-proof.json');
1888
- if (fs.existsSync(proofPath)) {
1889
- fs.unlinkSync(proofPath);
1890
- this.logger?.info('[ServiceWrapper][infra.invalidation] Deleted local validation proof', { proofPath });
1891
- }
1892
- }
1893
-
1894
- this._revalidateWithBackoff();
1864
+ _deleteProofAndRevalidate() {
1865
+ const fs = require('fs');
1866
+ const path = require('path');
1895
1867
 
1896
- } catch (parseErr) {
1897
- this.logger?.warn('[ServiceWrapper][infra.invalidation] Failed to parse event', { error: parseErr.message });
1868
+ if (this.serviceRoot) {
1869
+ const proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
1870
+ if (fs.existsSync(proofPath)) {
1871
+ fs.unlinkSync(proofPath);
1872
+ this.logger?.info('[ServiceWrapper][revalidate] Deleted local validation proof', { proofPath });
1898
1873
  }
1874
+ }
1899
1875
 
1900
- channel.ack(msg);
1901
- });
1902
-
1903
- this._infraInvalidationConsumerTag = consumerTag;
1904
- this.logger?.info('[ServiceWrapper][infra.invalidation] Subscribed to infrastructure invalidation events', {
1905
- exchange: exchangeName,
1906
- queue: q.queue,
1907
- consumerTag
1908
- });
1876
+ this._revalidateWithBackoff();
1909
1877
  }
1910
1878
 
1911
1879
  /**
@@ -1915,7 +1883,8 @@ class ServiceWrapper {
1915
1883
  */
1916
1884
  _revalidateWithBackoff() {
1917
1885
  if (this._revalidationState === REVALIDATION_STATE.FAST_BACKOFF ||
1918
- this._revalidationState === REVALIDATION_STATE.DAILY_BACKOFF) {
1886
+ this._revalidationState === REVALIDATION_STATE.HOURLY_BACKOFF ||
1887
+ this._revalidationState === REVALIDATION_STATE.SLOW_BACKOFF) {
1919
1888
  this.logger?.info('[ServiceWrapper][revalidation] Ignoring duplicate invalidation — re-validation cycle already active', {
1920
1889
  state: this._revalidationState,
1921
1890
  attempt: this._revalidationAttempt
@@ -1925,27 +1894,44 @@ class ServiceWrapper {
1925
1894
 
1926
1895
  this._revalidationState = REVALIDATION_STATE.FAST_BACKOFF;
1927
1896
  this._revalidationAttempt = 0;
1897
+ this._hourlyAttempts = 0;
1928
1898
  this._scheduleRevalidationAttempt();
1929
1899
  }
1930
1900
 
1931
1901
  /**
1932
- * Schedule the next re-validation attempt based on current state and attempt count.
1902
+ * Schedule the next re-validation attempt.
1903
+ * Backoff phases: 6 fast (30s-30min) -> hourly (max 24h) -> every 6h (forever).
1904
+ * At transition from hourly to slow: deregister + shutdown.
1933
1905
  * @private
1934
1906
  */
1935
1907
  _scheduleRevalidationAttempt() {
1936
1908
  let delayMs;
1937
1909
 
1938
- if (this._revalidationState === REVALIDATION_STATE.DAILY_BACKOFF) {
1939
- delayMs = REVALIDATION_DAILY_MS;
1940
- } else if (this._revalidationAttempt < REVALIDATION_FAST_BACKOFF_MS.length) {
1941
- delayMs = REVALIDATION_FAST_BACKOFF_MS[this._revalidationAttempt];
1910
+ if (this._revalidationState === REVALIDATION_STATE.FAST_BACKOFF) {
1911
+ if (this._revalidationAttempt < REVALIDATION_FAST_BACKOFF_MS.length) {
1912
+ delayMs = REVALIDATION_FAST_BACKOFF_MS[this._revalidationAttempt];
1913
+ } else {
1914
+ this._revalidationState = REVALIDATION_STATE.HOURLY_BACKOFF;
1915
+ this._hourlyAttempts = 0;
1916
+ delayMs = REVALIDATION_HOURLY_MS;
1917
+ this.logger?.warn(`[ServiceWrapper][revalidation] Fast retries exhausted (${this._revalidationAttempt} attempts) — switching to hourly retry`);
1918
+ }
1919
+ } else if (this._revalidationState === REVALIDATION_STATE.HOURLY_BACKOFF) {
1920
+ if (this._hourlyAttempts < REVALIDATION_HOURLY_MAX) {
1921
+ delayMs = REVALIDATION_HOURLY_MS;
1922
+ } else {
1923
+ this.logger?.error(
1924
+ `[ServiceWrapper][revalidation] All revalidation attempts exhausted ` +
1925
+ `(${REVALIDATION_FAST_BACKOFF_MS.length} fast + ${REVALIDATION_HOURLY_MAX} hourly) — deregistering and shutting down`
1926
+ );
1927
+ this._deregisterAndShutdown();
1928
+ return;
1929
+ }
1942
1930
  } else {
1943
- this._revalidationState = REVALIDATION_STATE.DAILY_BACKOFF;
1944
- delayMs = REVALIDATION_DAILY_MS;
1945
- this.logger?.warn(`[ServiceWrapper][revalidation] Fast retries exhausted (${this._revalidationAttempt} attempts) - switching to daily retry`);
1931
+ delayMs = REVALIDATION_SLOW_MS;
1946
1932
  }
1947
1933
 
1948
- this.logger?.info(`[ServiceWrapper][revalidation] Next attempt in ${delayMs}ms`, {
1934
+ this.logger?.info(`[ServiceWrapper][revalidation] Next attempt in ${Math.round(delayMs / 1000)}s`, {
1949
1935
  attempt: this._revalidationAttempt + 1,
1950
1936
  state: this._revalidationState,
1951
1937
  delayMs
@@ -1954,16 +1940,37 @@ class ServiceWrapper {
1954
1940
  this._revalidationTimer = setTimeout(() => this._executeRevalidation(), delayMs);
1955
1941
  }
1956
1942
 
1943
+ /**
1944
+ * Deregister from Registry and exit. Container orchestrator will restart.
1945
+ * @private
1946
+ */
1947
+ async _deregisterAndShutdown() {
1948
+ try {
1949
+ if (this.registryClient) {
1950
+ await this.registryClient.deregister();
1951
+ this.logger?.info('[ServiceWrapper][revalidation] Deregistered from registry');
1952
+ }
1953
+ } catch (err) {
1954
+ this.logger?.error('[ServiceWrapper][revalidation] Failed to deregister before shutdown', { error: err.message });
1955
+ }
1956
+
1957
+ process.exit(1);
1958
+ }
1959
+
1957
1960
  /**
1958
1961
  * Execute a single re-validation attempt.
1959
1962
  * @private
1960
1963
  */
1961
1964
  async _executeRevalidation() {
1962
1965
  this._revalidationAttempt++;
1963
- const maxFast = REVALIDATION_FAST_BACKOFF_MS.length;
1966
+ if (this._revalidationState === REVALIDATION_STATE.HOURLY_BACKOFF) {
1967
+ this._hourlyAttempts++;
1968
+ }
1964
1969
  const startTime = Date.now();
1965
1970
 
1966
- this.logger?.info(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt}/${maxFast} starting`);
1971
+ this.logger?.info(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt} starting`, {
1972
+ state: this._revalidationState
1973
+ });
1967
1974
 
1968
1975
  try {
1969
1976
  const serviceName = this.config.service?.name;
@@ -1986,7 +1993,6 @@ class ServiceWrapper {
1986
1993
 
1987
1994
  this.validationProof = result.proof;
1988
1995
 
1989
- // Re-register with fresh proof
1990
1996
  if (this.registryClient) {
1991
1997
  const serviceInfo = {
1992
1998
  name: serviceName,
@@ -2005,26 +2011,15 @@ class ServiceWrapper {
2005
2011
  this._revalidationTimer = null;
2006
2012
  this.logger?.info(`[ServiceWrapper][revalidation] Success after ${this._revalidationAttempt} attempts (${elapsed}ms)`);
2007
2013
 
2008
- // Clean up failure tracking file
2009
2014
  this._clearValidationFailureFile();
2010
2015
 
2011
2016
  } catch (error) {
2012
2017
  const elapsed = Date.now() - startTime;
2013
- const nextDelay = this._revalidationState === REVALIDATION_STATE.DAILY_BACKOFF
2014
- ? REVALIDATION_DAILY_MS
2015
- : (this._revalidationAttempt < maxFast
2016
- ? REVALIDATION_FAST_BACKOFF_MS[this._revalidationAttempt]
2017
- : REVALIDATION_DAILY_MS);
2018
-
2019
- if (this._revalidationState === REVALIDATION_STATE.DAILY_BACKOFF) {
2020
- this.logger?.warn(`[ServiceWrapper][revalidation] Daily retry attempt ${this._revalidationAttempt} - ${error.message}`);
2021
- } else {
2022
- this.logger?.warn(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt} failed: ${error.message} - next retry in ${nextDelay}ms`, {
2023
- attempt: this._revalidationAttempt,
2024
- elapsed,
2025
- error: error.message
2026
- });
2027
- }
2018
+ this.logger?.warn(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt} failed (${elapsed}ms): ${error.message}`, {
2019
+ attempt: this._revalidationAttempt,
2020
+ state: this._revalidationState,
2021
+ error: error.message
2022
+ });
2028
2023
 
2029
2024
  this._scheduleRevalidationAttempt();
2030
2025
  }
@@ -2042,7 +2037,7 @@ class ServiceWrapper {
2042
2037
 
2043
2038
  if (!this.serviceRoot) return null;
2044
2039
 
2045
- const failurePath = path.join(this.serviceRoot, 'conn-runtime', 'validation-failure.json');
2040
+ const failurePath = path.join(this.serviceRoot, RUNTIME_DIR, 'validation-failure.json');
2046
2041
  try {
2047
2042
  if (!fs.existsSync(failurePath)) return null;
2048
2043
  return JSON.parse(fs.readFileSync(failurePath, 'utf8'));
@@ -2064,12 +2059,12 @@ class ServiceWrapper {
2064
2059
 
2065
2060
  if (!this.serviceRoot) return;
2066
2061
 
2067
- const runtimeDir = path.join(this.serviceRoot, 'conn-runtime');
2068
- if (!fs.existsSync(runtimeDir)) {
2069
- fs.mkdirSync(runtimeDir, { recursive: true });
2062
+ const rtDir = path.join(this.serviceRoot, RUNTIME_DIR);
2063
+ if (!fs.existsSync(rtDir)) {
2064
+ fs.mkdirSync(rtDir, { recursive: true });
2070
2065
  }
2071
2066
 
2072
- const failurePath = path.join(runtimeDir, 'validation-failure.json');
2067
+ const failurePath = path.join(rtDir, 'validation-failure.json');
2073
2068
  const data = {
2074
2069
  lastAttempt: new Date().toISOString(),
2075
2070
  attemptCount,
@@ -2090,7 +2085,7 @@ class ServiceWrapper {
2090
2085
 
2091
2086
  if (!this.serviceRoot) return;
2092
2087
 
2093
- const failurePath = path.join(this.serviceRoot, 'conn-runtime', 'validation-failure.json');
2088
+ const failurePath = path.join(this.serviceRoot, RUNTIME_DIR, 'validation-failure.json');
2094
2089
  try {
2095
2090
  if (fs.existsSync(failurePath)) {
2096
2091
  fs.unlinkSync(failurePath);
@@ -2214,15 +2209,15 @@ class ServiceWrapper {
2214
2209
  const lastAttempt = new Date(failureData.lastAttempt).getTime();
2215
2210
  const sinceLastAttempt = Date.now() - lastAttempt;
2216
2211
 
2217
- if (sinceLastAttempt < REVALIDATION_DAILY_MS) {
2218
- const remainingMs = REVALIDATION_DAILY_MS - sinceLastAttempt;
2212
+ if (sinceLastAttempt < REVALIDATION_SLOW_MS) {
2213
+ const remainingMs = REVALIDATION_SLOW_MS - sinceLastAttempt;
2219
2214
  const remainingMin = Math.round(remainingMs / 60000);
2220
2215
  this.logger?.info(
2221
2216
  `[ServiceWrapper][startup] Previous validation failures detected (${failureData.attemptCount} attempts since ${failureData.firstFailure}) - waiting ${remainingMin}min before retry`
2222
2217
  );
2223
2218
  await new Promise(resolve => setTimeout(resolve, Math.min(remainingMs, 300000)));
2224
2219
  } else {
2225
- this.logger?.info('[ServiceWrapper][startup] Previous failures detected, but 24h elapsed - retrying fresh');
2220
+ this.logger?.info('[ServiceWrapper][startup] Previous failures detected, but cooldown elapsed - retrying fresh');
2226
2221
  }
2227
2222
  }
2228
2223
 
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ // See: docs/standards/tenant-context-contract.md
3
4
  /**
4
5
  * Tenant context middleware — requires x-tenant-id + x-workspace-id headers.
5
6
  *