@mountainpass/addressr 2.4.4 → 2.5.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/lib/service/address-service.js +13 -1
- package/lib/src/read-shadow.js +192 -0
- package/lib/src/waycharter-server.js +2 -0
- package/lib/swagger.js +3 -0
- package/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ var _streamDown = _interopRequireDefault(require("../utils/stream-down"));
|
|
|
32
32
|
var _setLinkOptions = require("./set-link-options");
|
|
33
33
|
var _rangeExpansion = require("./range-expansion");
|
|
34
34
|
var _gnafPackageFetch = require("./gnaf-package-fetch");
|
|
35
|
+
var _readShadow = require("../src/read-shadow");
|
|
35
36
|
var _nodeCrypto = _interopRequireDefault(require("node:crypto"));
|
|
36
37
|
var _glob = require("glob");
|
|
37
38
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -806,7 +807,9 @@ async function loadAddressDetails(file, expectedCount, context, {
|
|
|
806
807
|
}
|
|
807
808
|
async function searchForAddress(searchString, p, pageSize = PAGE_SIZE) {
|
|
808
809
|
// const searchString = '657 The Entrance Road'; //'2/25 TOTTERDE'; // 'UNT 2, BELCONNEN';
|
|
809
|
-
|
|
810
|
+
// ADR 031: hoist params so the same body object is shared by the primary
|
|
811
|
+
// and the fire-and-forget shadow mirror — no double-build cost.
|
|
812
|
+
const searchParameters = {
|
|
810
813
|
index: ES_INDEX_NAME,
|
|
811
814
|
body: {
|
|
812
815
|
from: (p - 1 || 0) * pageSize,
|
|
@@ -871,6 +874,15 @@ async function searchForAddress(searchString, p, pageSize = PAGE_SIZE) {
|
|
|
871
874
|
}
|
|
872
875
|
}
|
|
873
876
|
}
|
|
877
|
+
};
|
|
878
|
+
const searchResp = await globalThis.esClient.search(searchParameters);
|
|
879
|
+
// ADR 031 read-shadow: fire-and-forget mirror to a configurable secondary
|
|
880
|
+
// OpenSearch backend so v2 caches warm with realistic production query
|
|
881
|
+
// distribution before cutover. Returns synchronously; failures swallowed.
|
|
882
|
+
// No-op when ADDRESSR_SHADOW_HOST is unset (default).
|
|
883
|
+
(0, _readShadow.mirrorRequest)({
|
|
884
|
+
method: 'search',
|
|
885
|
+
params: searchParameters
|
|
874
886
|
});
|
|
875
887
|
logger('hits', JSON.stringify(searchResp.body.hits, undefined, 2));
|
|
876
888
|
return searchResp;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports._resetShadowClientForTesting = _resetShadowClientForTesting;
|
|
7
|
+
exports.mirrorRequest = mirrorRequest;
|
|
8
|
+
exports.validateReadShadowConfig = validateReadShadowConfig;
|
|
9
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
10
|
+
var _opensearch = require("@opensearch-project/opensearch");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
/* eslint-disable @eslint-community/eslint-comments/disable-enable-pair */
|
|
13
|
+
/* eslint-disable security/detect-object-injection -- env var names are compile-time constants, not user input */
|
|
14
|
+
|
|
15
|
+
// @jtbd JTBD-201 (Validate a New Search Backend With Realistic Production Traffic Before Cutover)
|
|
16
|
+
//
|
|
17
|
+
// ADR 031: Read-shadow for search-backend migrations.
|
|
18
|
+
//
|
|
19
|
+
// Mirrors `/addresses?q=...` and `/addresses/{id}` requests to a configurable
|
|
20
|
+
// secondary OpenSearch backend in fire-and-forget mode. Goal: warm the
|
|
21
|
+
// candidate cluster's filesystem and field-data caches with realistic
|
|
22
|
+
// production query distribution before cutover.
|
|
23
|
+
//
|
|
24
|
+
// Default off: when `ADDRESSR_SHADOW_HOST` is unset, `mirrorRequest` is a
|
|
25
|
+
// no-op. Self-hosted users (npm/Docker) are unaffected.
|
|
26
|
+
//
|
|
27
|
+
// Failure isolation: every code path through `mirrorRequest` is wrapped to
|
|
28
|
+
// guarantee the primary `/addresses` response cannot be impacted. Synchronous
|
|
29
|
+
// throws from client construction or method invocation are caught; async
|
|
30
|
+
// rejections are swallowed via `.catch(swallowError)`; per-request
|
|
31
|
+
// `AbortController` timeout (default 3000ms) bounds in-flight resources under
|
|
32
|
+
// shadow target outage.
|
|
33
|
+
//
|
|
34
|
+
// `ADDRESSR_SHADOW_*` is the application-level shadow target (read by the
|
|
35
|
+
// running addressr server). It is distinct from `TF_VAR_ELASTIC_V2_*`, which
|
|
36
|
+
// is the Terraform input that populates EB env at deploy time. They may carry
|
|
37
|
+
// the same values during a migration window, but their lifecycles differ
|
|
38
|
+
// (shadow vars persist across migrations; V2 vars are clobbered into
|
|
39
|
+
// canonical names at decommission per ADR 029 step 9).
|
|
40
|
+
|
|
41
|
+
const logger = (0, _debug.default)('api');
|
|
42
|
+
const error = (0, _debug.default)('error');
|
|
43
|
+
error.log = console.error.bind(console);
|
|
44
|
+
const HOST_VAR = 'ADDRESSR_SHADOW_HOST';
|
|
45
|
+
const PORT_VAR = 'ADDRESSR_SHADOW_PORT';
|
|
46
|
+
const USERNAME_VAR = 'ADDRESSR_SHADOW_USERNAME';
|
|
47
|
+
const PASSWORD_VAR = 'ADDRESSR_SHADOW_PASSWORD';
|
|
48
|
+
const PROTOCOL_VAR = 'ADDRESSR_SHADOW_PROTOCOL';
|
|
49
|
+
const TIMEOUT_VAR = 'ADDRESSR_SHADOW_TIMEOUT_MS';
|
|
50
|
+
const SUPPORTED_METHODS = new Set(['search', 'get']);
|
|
51
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
52
|
+
|
|
53
|
+
// Module-scoped lazy singleton. The shadow client is built on first call
|
|
54
|
+
// and reused for the process lifetime so connection setup amortises across
|
|
55
|
+
// requests (the @opensearch-project/opensearch client uses keepalive HTTP
|
|
56
|
+
// agents internally).
|
|
57
|
+
let cachedClient;
|
|
58
|
+
let cachedClientFingerprint;
|
|
59
|
+
function isNonEmpty(value) {
|
|
60
|
+
return typeof value === 'string' && value.length > 0;
|
|
61
|
+
}
|
|
62
|
+
function validateReadShadowConfig(environment = process.env) {
|
|
63
|
+
const hostSet = isNonEmpty(environment[HOST_VAR]);
|
|
64
|
+
const usernameSet = isNonEmpty(environment[USERNAME_VAR]);
|
|
65
|
+
const passwordSet = isNonEmpty(environment[PASSWORD_VAR]);
|
|
66
|
+
if (!hostSet) {
|
|
67
|
+
return; // feature disabled, nothing to validate
|
|
68
|
+
}
|
|
69
|
+
if (usernameSet && !passwordSet) {
|
|
70
|
+
throw new Error(`Read-shadow misconfigured: ${USERNAME_VAR} is set but ${PASSWORD_VAR} is missing. Set both to enable basic auth, or unset both to use the shadow target without auth.`);
|
|
71
|
+
}
|
|
72
|
+
if (passwordSet && !usernameSet) {
|
|
73
|
+
throw new Error(`Read-shadow misconfigured: ${PASSWORD_VAR} is set but ${USERNAME_VAR} is missing. Set both to enable basic auth, or unset both to use the shadow target without auth.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function buildClientOptions(environment) {
|
|
77
|
+
const host = environment[HOST_VAR];
|
|
78
|
+
const port = environment[PORT_VAR] || '443';
|
|
79
|
+
const protocol = environment[PROTOCOL_VAR] || 'https';
|
|
80
|
+
const username = environment[USERNAME_VAR];
|
|
81
|
+
const password = environment[PASSWORD_VAR];
|
|
82
|
+
const node = isNonEmpty(username) ? `${protocol}://${username}:${password}@${host}:${port}` : `${protocol}://${host}:${port}`;
|
|
83
|
+
return {
|
|
84
|
+
node
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stable fingerprint so the cache resets when env changes between calls
|
|
89
|
+
// (e.g. between unit tests that snapshot/restore env). Excludes credentials.
|
|
90
|
+
function clientFingerprint(environment) {
|
|
91
|
+
return [environment[HOST_VAR] || '', environment[PORT_VAR] || '', environment[PROTOCOL_VAR] || '', isNonEmpty(environment[USERNAME_VAR]) ? '+auth' : '-auth'].join('|');
|
|
92
|
+
}
|
|
93
|
+
function getShadowClient({
|
|
94
|
+
environment = process.env,
|
|
95
|
+
ClientClass = _opensearch.Client
|
|
96
|
+
} = {}) {
|
|
97
|
+
if (!isNonEmpty(environment[HOST_VAR])) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const fingerprint = clientFingerprint(environment);
|
|
101
|
+
if (cachedClient && cachedClientFingerprint === fingerprint) {
|
|
102
|
+
return cachedClient;
|
|
103
|
+
}
|
|
104
|
+
const options = buildClientOptions(environment);
|
|
105
|
+
cachedClient = new ClientClass(options);
|
|
106
|
+
cachedClientFingerprint = fingerprint;
|
|
107
|
+
return cachedClient;
|
|
108
|
+
}
|
|
109
|
+
function getTimeoutMs(environment) {
|
|
110
|
+
const raw = environment[TIMEOUT_VAR];
|
|
111
|
+
if (!isNonEmpty(raw)) {
|
|
112
|
+
return DEFAULT_TIMEOUT_MS;
|
|
113
|
+
}
|
|
114
|
+
const parsed = Number.parseInt(raw, 10);
|
|
115
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
|
|
116
|
+
}
|
|
117
|
+
function swallowError(reason) {
|
|
118
|
+
if (reason && reason.name === 'AbortError') {
|
|
119
|
+
error('read-shadow: request aborted by timeout');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (reason instanceof Error) {
|
|
123
|
+
error('read-shadow: %s', reason.message);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
error('read-shadow: non-error rejection %o', reason);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fire-and-forget mirror to a configurable shadow OpenSearch backend.
|
|
130
|
+
// Returns synchronously; the kicked-off promise is detached.
|
|
131
|
+
//
|
|
132
|
+
// Programmer-error throws (unsupported method) are synchronous so callers
|
|
133
|
+
// notice during dev. All operational errors (network, target down, abort,
|
|
134
|
+
// synchronous client throws) are swallowed via swallowError.
|
|
135
|
+
function mirrorRequest({
|
|
136
|
+
method,
|
|
137
|
+
params,
|
|
138
|
+
environment = process.env,
|
|
139
|
+
ClientClass = _opensearch.Client
|
|
140
|
+
} = {}) {
|
|
141
|
+
if (!SUPPORTED_METHODS.has(method)) {
|
|
142
|
+
throw new Error(`read-shadow: unsupported method ${JSON.stringify(method)}; expected one of ${[...SUPPORTED_METHODS].join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
let client;
|
|
145
|
+
try {
|
|
146
|
+
client = getShadowClient({
|
|
147
|
+
environment,
|
|
148
|
+
ClientClass
|
|
149
|
+
});
|
|
150
|
+
} catch (constructError) {
|
|
151
|
+
swallowError(constructError);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!client) {
|
|
155
|
+
return; // feature disabled (HOST unset)
|
|
156
|
+
}
|
|
157
|
+
const timeoutMs = getTimeoutMs(environment);
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
160
|
+
if (typeof timer.unref === 'function') {
|
|
161
|
+
timer.unref();
|
|
162
|
+
}
|
|
163
|
+
let promise;
|
|
164
|
+
try {
|
|
165
|
+
promise = client[method](params, {
|
|
166
|
+
signal: controller.signal
|
|
167
|
+
});
|
|
168
|
+
} catch (syncError) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
swallowError(syncError);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!promise || typeof promise.then !== 'function') {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
promise.then(() => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
logger('read-shadow: %s ok', method);
|
|
180
|
+
return;
|
|
181
|
+
}).catch(error_ => {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
swallowError(error_);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Internal: lets unit tests reset the singleton between cases. Not part of
|
|
188
|
+
// the public contract; flagged with a leading underscore by convention.
|
|
189
|
+
function _resetShadowClientForTesting() {
|
|
190
|
+
cachedClient = undefined;
|
|
191
|
+
cachedClientFingerprint = undefined;
|
|
192
|
+
}
|
|
@@ -13,6 +13,7 @@ var _addressService = require("../service/address-service");
|
|
|
13
13
|
var _version = require("../version");
|
|
14
14
|
var _nodeCrypto = _interopRequireDefault(require("node:crypto"));
|
|
15
15
|
var _proxyAuth = require("./proxy-auth");
|
|
16
|
+
var _readShadow = require("./read-shadow");
|
|
16
17
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
18
|
//import connect from 'connect';
|
|
18
19
|
|
|
@@ -659,6 +660,7 @@ let server;
|
|
|
659
660
|
const PAGE_SIZE = process.env.PAGE_SIZE || 8;
|
|
660
661
|
function startRest2Server() {
|
|
661
662
|
(0, _proxyAuth.validateProxyAuthConfig)();
|
|
663
|
+
(0, _readShadow.validateReadShadowConfig)();
|
|
662
664
|
app.use((_request, response, next) => {
|
|
663
665
|
if (process.env.ADDRESSR_ACCESS_CONTROL_ALLOW_ORIGIN !== undefined) {
|
|
664
666
|
response.append('Access-Control-Allow-Origin', process.env.ADDRESSR_ACCESS_CONTROL_ALLOW_ORIGIN);
|
package/lib/swagger.js
CHANGED
|
@@ -14,6 +14,7 @@ var _nodeHttp = require("node:http");
|
|
|
14
14
|
var _jsYaml = require("js-yaml");
|
|
15
15
|
var _nodePath = _interopRequireDefault(require("node:path"));
|
|
16
16
|
var _swaggerTools = require("swagger-tools");
|
|
17
|
+
var _readShadow = require("./src/read-shadow");
|
|
17
18
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
19
|
//import connect from 'connect';
|
|
19
20
|
|
|
@@ -86,6 +87,8 @@ function swaggerInit() {
|
|
|
86
87
|
}
|
|
87
88
|
let server;
|
|
88
89
|
function startServer() {
|
|
90
|
+
// ADR 031: fail loudly at startup if read-shadow env vars are misconfigured.
|
|
91
|
+
(0, _readShadow.validateReadShadowConfig)();
|
|
89
92
|
app.use((request, response, next) => {
|
|
90
93
|
if (process.env.ADDRESSR_ACCESS_CONTROL_ALLOW_ORIGIN !== undefined) {
|
|
91
94
|
response.append('Access-Control-Allow-Origin', process.env.ADDRESSR_ACCESS_CONTROL_ALLOW_ORIGIN);
|
package/lib/version.js
CHANGED