@onlineapps/service-wrapper 2.2.9 → 2.3.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 +5 -5
- package/src/ConfigLoader.js +1 -1
- package/src/ServiceWrapper.js +124 -107
- package/src/createTenantContextMiddleware.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-wrapper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
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.
|
|
34
|
-
"@onlineapps/conn-orch-orchestrator": "1.0.
|
|
35
|
-
"@onlineapps/conn-orch-registry": "1.1.
|
|
36
|
-
"@onlineapps/conn-orch-validator": "2.0.
|
|
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"
|
package/src/ConfigLoader.js
CHANGED
|
@@ -44,7 +44,7 @@ const CONFIG_REGISTRY = {
|
|
|
44
44
|
description: 'Operations specification (API contract)'
|
|
45
45
|
},
|
|
46
46
|
VALIDATION_PROOF: {
|
|
47
|
-
paths: ['
|
|
47
|
+
paths: ['conn-runtime/validation-proof.json'],
|
|
48
48
|
required: false,
|
|
49
49
|
description: 'Pre-validation proof from cookbook tests'
|
|
50
50
|
}
|
package/src/ServiceWrapper.js
CHANGED
|
@@ -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
|
|
30
|
-
const
|
|
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
|
-
//
|
|
167
|
-
|
|
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,
|
|
1286
|
+
const proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
|
|
1275
1287
|
|
|
1276
1288
|
if (!fs.existsSync(proofPath)) {
|
|
1277
1289
|
return { status: 'no_proof' };
|
|
@@ -1814,6 +1826,28 @@ class ServiceWrapper {
|
|
|
1814
1826
|
}
|
|
1815
1827
|
}
|
|
1816
1828
|
|
|
1829
|
+
// Extension 1: specificationEndpoint must match an Express route
|
|
1830
|
+
// See: docs/standards/api-versioning-contract.md
|
|
1831
|
+
const specEndpoint = this.config.service?.specificationEndpoint;
|
|
1832
|
+
if (specEndpoint && !routeSet.has(`GET:${specEndpoint}`)) {
|
|
1833
|
+
errors.push(
|
|
1834
|
+
`specificationEndpoint '${specEndpoint}' not found in Express routes — ` +
|
|
1835
|
+
`config claims this path but no GET route is registered`
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Extension 3: operations.json endpoints under /api/ must be versioned
|
|
1840
|
+
// See: docs/standards/api-versioning-contract.md
|
|
1841
|
+
const versionPattern = /^\/api\/v\d+\//;
|
|
1842
|
+
for (const [opName, opDef] of Object.entries(ops)) {
|
|
1843
|
+
if (opDef.endpoint?.startsWith('/api') && !versionPattern.test(opDef.endpoint)) {
|
|
1844
|
+
errors.push(
|
|
1845
|
+
`Operation '${opName}' endpoint '${opDef.endpoint}' missing version — ` +
|
|
1846
|
+
`expected /api/v{N}/... (see docs/standards/api-versioning-contract.md)`
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1817
1851
|
if (errors.length > 0) {
|
|
1818
1852
|
throw new Error(
|
|
1819
1853
|
`[ServiceWrapper] Operations-Routes alignment failed (${errors.length} issue${errors.length > 1 ? 's' : ''}):\n` +
|
|
@@ -1823,67 +1857,23 @@ class ServiceWrapper {
|
|
|
1823
1857
|
}
|
|
1824
1858
|
|
|
1825
1859
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1860
|
+
* Delete local validation proof and trigger re-validation.
|
|
1861
|
+
* Called when Registry sends a revalidation request (infra version change).
|
|
1828
1862
|
* @private
|
|
1829
1863
|
*/
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
const channel = this.mqClient._transport.channel;
|
|
1837
|
-
const exchangeName = 'infrastructure.health.events';
|
|
1838
|
-
|
|
1839
|
-
await channel.assertExchange(exchangeName, 'fanout', { durable: true });
|
|
1840
|
-
|
|
1841
|
-
const q = await channel.assertQueue('', { exclusive: true, autoDelete: true });
|
|
1842
|
-
|
|
1843
|
-
await channel.bindQueue(q.queue, exchangeName, '');
|
|
1844
|
-
|
|
1845
|
-
const { consumerTag } = await channel.consume(q.queue, async (msg) => {
|
|
1846
|
-
if (!msg) return;
|
|
1847
|
-
|
|
1848
|
-
try {
|
|
1849
|
-
const event = JSON.parse(msg.content.toString());
|
|
1850
|
-
|
|
1851
|
-
if (event.type !== 'infra.invalidation') {
|
|
1852
|
-
channel.ack(msg);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
this.logger?.info('[ServiceWrapper][infra.invalidation] Infrastructure change detected, scheduling re-validation', {
|
|
1857
|
-
fingerprint: event.fingerprint?.substring(0, 16) + '...',
|
|
1858
|
-
timestamp: event.timestamp
|
|
1859
|
-
});
|
|
1860
|
-
|
|
1861
|
-
// Delete local validation proof
|
|
1862
|
-
const fs = require('fs');
|
|
1863
|
-
const path = require('path');
|
|
1864
|
-
if (this.serviceRoot) {
|
|
1865
|
-
const proofPath = path.join(this.serviceRoot, 'conn-runtime', 'validation-proof.json');
|
|
1866
|
-
if (fs.existsSync(proofPath)) {
|
|
1867
|
-
fs.unlinkSync(proofPath);
|
|
1868
|
-
this.logger?.info('[ServiceWrapper][infra.invalidation] Deleted local validation proof', { proofPath });
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
this._revalidateWithBackoff();
|
|
1864
|
+
_deleteProofAndRevalidate() {
|
|
1865
|
+
const fs = require('fs');
|
|
1866
|
+
const path = require('path');
|
|
1873
1867
|
|
|
1874
|
-
|
|
1875
|
-
|
|
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 });
|
|
1876
1873
|
}
|
|
1874
|
+
}
|
|
1877
1875
|
|
|
1878
|
-
|
|
1879
|
-
});
|
|
1880
|
-
|
|
1881
|
-
this._infraInvalidationConsumerTag = consumerTag;
|
|
1882
|
-
this.logger?.info('[ServiceWrapper][infra.invalidation] Subscribed to infrastructure invalidation events', {
|
|
1883
|
-
exchange: exchangeName,
|
|
1884
|
-
queue: q.queue,
|
|
1885
|
-
consumerTag
|
|
1886
|
-
});
|
|
1876
|
+
this._revalidateWithBackoff();
|
|
1887
1877
|
}
|
|
1888
1878
|
|
|
1889
1879
|
/**
|
|
@@ -1893,7 +1883,8 @@ class ServiceWrapper {
|
|
|
1893
1883
|
*/
|
|
1894
1884
|
_revalidateWithBackoff() {
|
|
1895
1885
|
if (this._revalidationState === REVALIDATION_STATE.FAST_BACKOFF ||
|
|
1896
|
-
this._revalidationState === REVALIDATION_STATE.
|
|
1886
|
+
this._revalidationState === REVALIDATION_STATE.HOURLY_BACKOFF ||
|
|
1887
|
+
this._revalidationState === REVALIDATION_STATE.SLOW_BACKOFF) {
|
|
1897
1888
|
this.logger?.info('[ServiceWrapper][revalidation] Ignoring duplicate invalidation — re-validation cycle already active', {
|
|
1898
1889
|
state: this._revalidationState,
|
|
1899
1890
|
attempt: this._revalidationAttempt
|
|
@@ -1903,27 +1894,44 @@ class ServiceWrapper {
|
|
|
1903
1894
|
|
|
1904
1895
|
this._revalidationState = REVALIDATION_STATE.FAST_BACKOFF;
|
|
1905
1896
|
this._revalidationAttempt = 0;
|
|
1897
|
+
this._hourlyAttempts = 0;
|
|
1906
1898
|
this._scheduleRevalidationAttempt();
|
|
1907
1899
|
}
|
|
1908
1900
|
|
|
1909
1901
|
/**
|
|
1910
|
-
* Schedule the next re-validation attempt
|
|
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.
|
|
1911
1905
|
* @private
|
|
1912
1906
|
*/
|
|
1913
1907
|
_scheduleRevalidationAttempt() {
|
|
1914
1908
|
let delayMs;
|
|
1915
1909
|
|
|
1916
|
-
if (this._revalidationState === REVALIDATION_STATE.
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
+
}
|
|
1920
1930
|
} else {
|
|
1921
|
-
|
|
1922
|
-
delayMs = REVALIDATION_DAILY_MS;
|
|
1923
|
-
this.logger?.warn(`[ServiceWrapper][revalidation] Fast retries exhausted (${this._revalidationAttempt} attempts) - switching to daily retry`);
|
|
1931
|
+
delayMs = REVALIDATION_SLOW_MS;
|
|
1924
1932
|
}
|
|
1925
1933
|
|
|
1926
|
-
this.logger?.info(`[ServiceWrapper][revalidation] Next attempt in ${delayMs}
|
|
1934
|
+
this.logger?.info(`[ServiceWrapper][revalidation] Next attempt in ${Math.round(delayMs / 1000)}s`, {
|
|
1927
1935
|
attempt: this._revalidationAttempt + 1,
|
|
1928
1936
|
state: this._revalidationState,
|
|
1929
1937
|
delayMs
|
|
@@ -1932,16 +1940,37 @@ class ServiceWrapper {
|
|
|
1932
1940
|
this._revalidationTimer = setTimeout(() => this._executeRevalidation(), delayMs);
|
|
1933
1941
|
}
|
|
1934
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
|
+
|
|
1935
1960
|
/**
|
|
1936
1961
|
* Execute a single re-validation attempt.
|
|
1937
1962
|
* @private
|
|
1938
1963
|
*/
|
|
1939
1964
|
async _executeRevalidation() {
|
|
1940
1965
|
this._revalidationAttempt++;
|
|
1941
|
-
|
|
1966
|
+
if (this._revalidationState === REVALIDATION_STATE.HOURLY_BACKOFF) {
|
|
1967
|
+
this._hourlyAttempts++;
|
|
1968
|
+
}
|
|
1942
1969
|
const startTime = Date.now();
|
|
1943
1970
|
|
|
1944
|
-
this.logger?.info(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt}
|
|
1971
|
+
this.logger?.info(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt} starting`, {
|
|
1972
|
+
state: this._revalidationState
|
|
1973
|
+
});
|
|
1945
1974
|
|
|
1946
1975
|
try {
|
|
1947
1976
|
const serviceName = this.config.service?.name;
|
|
@@ -1964,7 +1993,6 @@ class ServiceWrapper {
|
|
|
1964
1993
|
|
|
1965
1994
|
this.validationProof = result.proof;
|
|
1966
1995
|
|
|
1967
|
-
// Re-register with fresh proof
|
|
1968
1996
|
if (this.registryClient) {
|
|
1969
1997
|
const serviceInfo = {
|
|
1970
1998
|
name: serviceName,
|
|
@@ -1983,26 +2011,15 @@ class ServiceWrapper {
|
|
|
1983
2011
|
this._revalidationTimer = null;
|
|
1984
2012
|
this.logger?.info(`[ServiceWrapper][revalidation] Success after ${this._revalidationAttempt} attempts (${elapsed}ms)`);
|
|
1985
2013
|
|
|
1986
|
-
// Clean up failure tracking file
|
|
1987
2014
|
this._clearValidationFailureFile();
|
|
1988
2015
|
|
|
1989
2016
|
} catch (error) {
|
|
1990
2017
|
const elapsed = Date.now() - startTime;
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
:
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (this._revalidationState === REVALIDATION_STATE.DAILY_BACKOFF) {
|
|
1998
|
-
this.logger?.warn(`[ServiceWrapper][revalidation] Daily retry attempt ${this._revalidationAttempt} - ${error.message}`);
|
|
1999
|
-
} else {
|
|
2000
|
-
this.logger?.warn(`[ServiceWrapper][revalidation] Attempt ${this._revalidationAttempt} failed: ${error.message} - next retry in ${nextDelay}ms`, {
|
|
2001
|
-
attempt: this._revalidationAttempt,
|
|
2002
|
-
elapsed,
|
|
2003
|
-
error: error.message
|
|
2004
|
-
});
|
|
2005
|
-
}
|
|
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
|
+
});
|
|
2006
2023
|
|
|
2007
2024
|
this._scheduleRevalidationAttempt();
|
|
2008
2025
|
}
|
|
@@ -2020,7 +2037,7 @@ class ServiceWrapper {
|
|
|
2020
2037
|
|
|
2021
2038
|
if (!this.serviceRoot) return null;
|
|
2022
2039
|
|
|
2023
|
-
const failurePath = path.join(this.serviceRoot,
|
|
2040
|
+
const failurePath = path.join(this.serviceRoot, RUNTIME_DIR, 'validation-failure.json');
|
|
2024
2041
|
try {
|
|
2025
2042
|
if (!fs.existsSync(failurePath)) return null;
|
|
2026
2043
|
return JSON.parse(fs.readFileSync(failurePath, 'utf8'));
|
|
@@ -2042,12 +2059,12 @@ class ServiceWrapper {
|
|
|
2042
2059
|
|
|
2043
2060
|
if (!this.serviceRoot) return;
|
|
2044
2061
|
|
|
2045
|
-
const
|
|
2046
|
-
if (!fs.existsSync(
|
|
2047
|
-
fs.mkdirSync(
|
|
2062
|
+
const rtDir = path.join(this.serviceRoot, RUNTIME_DIR);
|
|
2063
|
+
if (!fs.existsSync(rtDir)) {
|
|
2064
|
+
fs.mkdirSync(rtDir, { recursive: true });
|
|
2048
2065
|
}
|
|
2049
2066
|
|
|
2050
|
-
const failurePath = path.join(
|
|
2067
|
+
const failurePath = path.join(rtDir, 'validation-failure.json');
|
|
2051
2068
|
const data = {
|
|
2052
2069
|
lastAttempt: new Date().toISOString(),
|
|
2053
2070
|
attemptCount,
|
|
@@ -2068,7 +2085,7 @@ class ServiceWrapper {
|
|
|
2068
2085
|
|
|
2069
2086
|
if (!this.serviceRoot) return;
|
|
2070
2087
|
|
|
2071
|
-
const failurePath = path.join(this.serviceRoot,
|
|
2088
|
+
const failurePath = path.join(this.serviceRoot, RUNTIME_DIR, 'validation-failure.json');
|
|
2072
2089
|
try {
|
|
2073
2090
|
if (fs.existsSync(failurePath)) {
|
|
2074
2091
|
fs.unlinkSync(failurePath);
|
|
@@ -2192,15 +2209,15 @@ class ServiceWrapper {
|
|
|
2192
2209
|
const lastAttempt = new Date(failureData.lastAttempt).getTime();
|
|
2193
2210
|
const sinceLastAttempt = Date.now() - lastAttempt;
|
|
2194
2211
|
|
|
2195
|
-
if (sinceLastAttempt <
|
|
2196
|
-
const remainingMs =
|
|
2212
|
+
if (sinceLastAttempt < REVALIDATION_SLOW_MS) {
|
|
2213
|
+
const remainingMs = REVALIDATION_SLOW_MS - sinceLastAttempt;
|
|
2197
2214
|
const remainingMin = Math.round(remainingMs / 60000);
|
|
2198
2215
|
this.logger?.info(
|
|
2199
2216
|
`[ServiceWrapper][startup] Previous validation failures detected (${failureData.attemptCount} attempts since ${failureData.firstFailure}) - waiting ${remainingMin}min before retry`
|
|
2200
2217
|
);
|
|
2201
2218
|
await new Promise(resolve => setTimeout(resolve, Math.min(remainingMs, 300000)));
|
|
2202
2219
|
} else {
|
|
2203
|
-
this.logger?.info('[ServiceWrapper][startup] Previous failures detected, but
|
|
2220
|
+
this.logger?.info('[ServiceWrapper][startup] Previous failures detected, but cooldown elapsed - retrying fresh');
|
|
2204
2221
|
}
|
|
2205
2222
|
}
|
|
2206
2223
|
|