Skip to content

Commit c22e91b

Browse files
authored
Add option for wait for governance on cold start (#43)
* add waiting for governance option * bump version
1 parent 4865cb1 commit c22e91b

File tree

7 files changed

+198
-50
lines changed

7 files changed

+198
-50
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,38 @@ On cold starts (when a new execution environment is initialized), there can be a
945945

946946
In typical production APIs, regular traffic keeps functions warm, so the likelihood of requests slipping through this initial window is small.
947947

948-
In isolated tests or very low-traffic scenarios, the execution environment may be recycled and the cache cleared, recreating the short window until configuration is reloaded. When testing, please send few normal request to keep the system warm first.
948+
In isolated tests or very low-traffic scenarios, the execution environment may be recycled and the cache cleared, recreating the short window until configuration is reloaded. When testing, please send few normal request to keep the system warm first.
949+
950+
### Cold Start Governance
951+
952+
You can enforce that initial requests during a Lambda cold start wait until Moesif Config and Governance Rules are loaded.
953+
954+
- `waitForGovernanceOnColdStart` (Boolean): When `true`, the middleware blocks the first invocations until governance rules are fetched and the app config is available. Default: `false`.
955+
- `governanceLoadTimeoutMs` (Number): Maximum time in milliseconds to wait for config before proceeding even if it hasn’t loaded, to avoid indefinite blocking. Default: `5000`.
956+
957+
Example:
958+
959+
```javascript
960+
const moesif = require('moesif-aws-lambda');
961+
962+
const options = {
963+
applicationId: process.env.MOESIF_APPLICATION_ID,
964+
waitForGovernanceOnColdStart: true,
965+
governanceLoadTimeoutMs: 8000,
966+
};
967+
968+
exports.handler = moesif(options, async (event, context) => {
969+
return {
970+
statusCode: 200,
971+
headers: { 'Content-Type': 'application/json' },
972+
body: JSON.stringify({ ok: true })
973+
};
974+
});
975+
```
976+
977+
Notes:
978+
- Requests during cold start may take longer but ensure governance is applied when rules exist.
979+
- If the timeout elapses without config, the request proceeds to avoid indefinite delays.
949980

950981
## How to Get Help
951982
If you face any issues using this middleware, try the [troubheshooting guidelines](#troubleshoot). For further assistance, reach out to our [support team](mailto:support@moesif.com).

lib/index.js

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ module.exports = function (options, handler) {
7070
// config moesifapi
7171
var config = moesifapi.configuration;
7272
config.ApplicationId = options.applicationId || options.ApplicationId || process.env.MOESIF_APPLICATION_ID;
73-
config.UserAgent = 'moesif-aws-lambda-nodejs/' + '2.0.7';
73+
config.UserAgent = 'moesif-aws-lambda-nodejs/' + '2.1.0';
7474
config.BaseUri = options.baseUri || options.BaseUri || config.BaseUri;
7575
var moesifController = moesifapi.ApiController;
7676

@@ -125,7 +125,46 @@ module.exports = function (options, handler) {
125125
details);
126126
};
127127
governanceRulesManager.setLogger(logGovernance);
128-
governanceRulesManager.tryGetRules();
128+
var getRulesPromise = governanceRulesManager.tryGetRules();
129+
130+
// Option: wait for governance rules + config on cold start
131+
options.waitForGovernanceOnColdStart = options.waitForGovernanceOnColdStart || false;
132+
options.governanceLoadTimeoutMs = typeof options.governanceLoadTimeoutMs === 'number'
133+
? options.governanceLoadTimeoutMs
134+
: 2000;
135+
136+
var initialLoadPromise = null;
137+
if (options.waitForGovernanceOnColdStart) {
138+
// Kick off config load immediately
139+
moesifConfigManager.tryGetConfig();
140+
141+
function waitForConfig(timeoutMs) {
142+
return new Promise(function(resolve) {
143+
var start = Date.now();
144+
(function check() {
145+
if (moesifConfigManager.hasConfig()) {
146+
return resolve();
147+
}
148+
if (timeoutMs && Date.now() - start >= timeoutMs) {
149+
// Timeout reached; proceed even if config isn't available
150+
return resolve();
151+
}
152+
// Keep nudging config fetch and poll until available or timeout
153+
moesifConfigManager.tryGetConfig();
154+
setTimeout(check, 100);
155+
})();
156+
});
157+
}
158+
159+
initialLoadPromise = Promise.all([
160+
// Use the existing rules fetch promise kicked off above
161+
getRulesPromise,
162+
waitForConfig(options.governanceLoadTimeoutMs)
163+
]).catch(function() {
164+
// Swallow errors; proceed regardless to avoid indefinite blocking
165+
return;
166+
});
167+
}
129168

130169
var trySaveEventLocal = function(eventData) {
131170
moesifConfigManager.tryGetConfig();
@@ -179,60 +218,69 @@ module.exports = function (options, handler) {
179218
callback(err, modifiedResultObject)
180219
};
181220

182-
const isV1 = determineIsEventVersionV1(event);
183-
184-
let returnPromise;
185-
if (governanceRulesManager.hasRules()) {
186-
var requestDataForGovernance = prepareRequestDataForGovernance(event, context, isV1);
187-
188-
var governedResponseHolder = governanceRulesManager.governRequest(
189-
moesifConfigManager._config,
190-
// this may cause identifyUser and identifyCompany to be called twice,
191-
// but this should be ok, but in order to block for governance rule
192-
// we have to trigger this earlier in the stream before response might be ready
193-
ensureToString(options.identifyUser(event, context)),
194-
ensureToString(options.identifyCompany(event, context)),
195-
requestDataForGovernance.requestFields,
196-
requestDataForGovernance.requestHeaders,
197-
requestDataForGovernance.requestBody
198-
);
221+
function proceed() {
222+
const isV1 = determineIsEventVersionV1(event);
223+
224+
let returnPromise;
225+
if (governanceRulesManager.hasRules()) {
226+
var requestDataForGovernance = prepareRequestDataForGovernance(event, context, isV1);
227+
228+
var governedResponseHolder = governanceRulesManager.governRequest(
229+
moesifConfigManager._config,
230+
// this may cause identifyUser and identifyCompany to be called twice,
231+
// but this should be ok, but in order to block for governance rule
232+
// we have to trigger this earlier in the stream before response might be ready
233+
ensureToString(options.identifyUser(event, context)),
234+
ensureToString(options.identifyCompany(event, context)),
235+
requestDataForGovernance.requestFields,
236+
requestDataForGovernance.requestHeaders,
237+
requestDataForGovernance.requestBody
238+
);
199239

200-
if (governedResponseHolder.headers) {
201-
extraHeaders = { ...extraHeaders, ...governedResponseHolder.headers };
240+
if (governedResponseHolder.headers) {
241+
extraHeaders = { ...extraHeaders, ...governedResponseHolder.headers };
242+
}
243+
244+
if (governedResponseHolder.blocked_by) {
245+
returnPromise = Promise.resolve({
246+
isBase64Encoded: false,
247+
statusCode: governedResponseHolder.status,
248+
headers: {
249+
...extraHeaders,
250+
'X-Moesif-Blocked-By': governedResponseHolder.blocked_by
251+
},
252+
body: JSON.stringify(governedResponseHolder.body)
253+
});
254+
}
255+
}
256+
if (!returnPromise) {
257+
returnPromise = handler(event, context, next);
202258
}
203259

204-
if (governedResponseHolder.blocked_by) {
205-
returnPromise = Promise.resolve({
206-
isBase64Encoded: false,
207-
statusCode: governedResponseHolder.status,
208-
headers: {
260+
if (returnPromise instanceof Promise) {
261+
return returnPromise.then((result) => {
262+
result.headers = {
263+
...result.headers,
209264
...extraHeaders,
210-
'X-Moesif-Blocked-By': governedResponseHolder.blocked_by
211-
},
212-
body: JSON.stringify(governedResponseHolder.body)
265+
};
266+
267+
return logEvent(event, context, null, result, options, moesifController).then(() => {
268+
return result;
269+
});
270+
}).catch((err) => {
271+
logEvent(event, context, err, {}, options, moesifController);
272+
throw err;
213273
});
214274
}
215-
}
216-
if (!returnPromise) {
217-
returnPromise = handler(event, context, next);
275+
return returnPromise;
218276
}
219277

220-
if (returnPromise instanceof Promise) {
221-
return returnPromise.then((result) => {
222-
result.headers = {
223-
...result.headers,
224-
...extraHeaders,
225-
};
226-
227-
return logEvent(event, context, null, result, options, moesifController).then(() => {
228-
return result;
229-
});
230-
}).catch((err) => {
231-
logEvent(event, context, err, {}, options, moesifController);
232-
throw err;
278+
if (initialLoadPromise) {
279+
return initialLoadPromise.then(function () {
280+
return proceed();
233281
});
234282
}
235-
return returnPromise;
283+
return proceed();
236284
};
237285

238286
moesifMiddleware.updateUser = function(userModel, cb) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "moesif-aws-lambda",
3-
"version": "2.0.7",
3+
"version": "2.1.0",
44
"description": "API Monitoring Middleware for AWS Lambda",
55
"main": "lib/index.js",
66
"keywords": [

scripts/run-tests.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env zsh
2+
set -euo pipefail
3+
4+
# Use Node from .nvmrc if available
5+
if command -v nvm >/dev/null 2>&1; then
6+
nvm use >/dev/null
7+
fi
8+
9+
# Set a dummy MOESIF_APPLICATION_ID if not provided
10+
export MOESIF_APPLICATION_ID=application_id_placeholder
11+
12+
node --test tests/*.js
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const path = require("path");
2+
require("dotenv").config({ path: path.resolve(__dirname, ".env") });
3+
4+
// Use basic assertions to avoid Node version dependency on node:test
5+
const assert = require("assert");
6+
7+
const moesif = require("../lib");
8+
const governanceRulesManager = require("../lib/governanceRulesManager");
9+
const moesifConfigManager = require("../lib/moesifConfigManager");
10+
11+
// Stub config and rules to avoid network calls
12+
moesifConfigManager._config = { sample_rate: 100 };
13+
14+
// Make rules available and return a blocking response
15+
const BLOCK_RULE_ID = "test-rule-id";
16+
governanceRulesManager.hasRules = function () { return true; };
17+
governanceRulesManager.governRequest = function () {
18+
return {
19+
status: 451,
20+
headers: { "X-Blocked": "true" },
21+
body: { reason: "blocked by test" },
22+
blocked_by: BLOCK_RULE_ID,
23+
};
24+
};
25+
26+
const handler = async (event, context) => {
27+
return {
28+
statusCode: 200,
29+
headers: { "Content-Type": "application/json" },
30+
body: JSON.stringify({ ok: true }),
31+
};
32+
};
33+
34+
const context = { getRemainingTimeInMillis: () => 1000 };
35+
36+
async function run() {
37+
const moesifWrappedHandler = moesif({
38+
applicationId: process.env.MOESIF_APPLICATION_ID || "dummy",
39+
waitForGovernanceOnColdStart: true,
40+
governanceLoadTimeoutMs: 1000,
41+
}, handler);
42+
43+
const event = { headers: {}, httpMethod: "GET", path: "/", requestContext: {} };
44+
45+
const response = await moesifWrappedHandler(event, { ...context });
46+
47+
assert.deepStrictEqual(response.statusCode, 451);
48+
assert.ok(response.headers["X-Moesif-Transaction-Id"]);
49+
assert.deepStrictEqual(response.headers["X-Moesif-Blocked-By"], BLOCK_RULE_ID);
50+
assert.ok(response.body);
51+
}
52+
53+
run().catch((err) => {
54+
console.error(err);
55+
process.exit(1);
56+
});

0 commit comments

Comments
 (0)