Skip to content

Commit e92c74d

Browse files
authored
Wait in async lambda handler (#910)
* fix: add wait() in the async lambda wrapper * test: add tests for async lambda wrapper
1 parent c278b6a commit e92c74d

File tree

2 files changed

+182
-3
lines changed

2 files changed

+182
-3
lines changed

src/server/rollbar.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ Rollbar.prototype.asyncLambdaHandler = function (handler, timeoutHandler) {
304304
self.error(message, custom);
305305
};
306306
var shouldReportTimeouts = self.options.captureLambdaTimeouts;
307-
return function (event, context) {
307+
return function rollbarAsyncLambdaHandler(event, context) {
308308
return new Promise(function (resolve, reject) {
309309
self.lambdaContext = context;
310310
if (shouldReportTimeouts) {
@@ -313,8 +313,10 @@ Rollbar.prototype.asyncLambdaHandler = function (handler, timeoutHandler) {
313313
}
314314
handler(event, context)
315315
.then(function (resp) {
316-
clearTimeout(self.lambdaTimeoutHandle);
317-
resolve(resp);
316+
self.wait(function () {
317+
clearTimeout(self.lambdaTimeoutHandle);
318+
resolve(resp);
319+
});
318320
})
319321
.catch(function (err) {
320322
self.error(err);

test/server.lambda.test.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var vows = require('vows');
5+
var sinon = require('sinon');
6+
7+
process.env.NODE_ENV = process.env.NODE_ENV || 'test-node-env';
8+
var Rollbar = require('../src/server/rollbar');
9+
10+
function promisePending(promise, callback) {
11+
var testValue = 'test-pending';
12+
13+
// Detect if a given promise is pending/unfulfilled using the
14+
// behavior of Promise.race(), which always returns the first
15+
// already resolved promise in the array.
16+
Promise.race([promise, Promise.resolve(testValue)])
17+
.then(function(value){
18+
if (value === testValue) {
19+
return callback(true);
20+
} else {
21+
return callback(false);
22+
}
23+
})
24+
.catch(function(_error){
25+
return callback(false);
26+
})
27+
}
28+
29+
function fakePostItem(_item, callback){
30+
// 1000ms simulates low API response, and allows testing the state before completion.
31+
setTimeout(function(){ callback(); }, 1000);
32+
}
33+
34+
var stubContext = {
35+
getRemainingTimeInMillis: function() { return 2000 }
36+
}
37+
38+
vows.describe('lambda')
39+
.addBatch({
40+
'async handler sends message': {
41+
topic: function() {
42+
var rollbar = new Rollbar({
43+
accessToken: 'abc123',
44+
captureUncaught: true,
45+
captureLambdaTimeouts: false
46+
});
47+
var api = rollbar.client.notifier.queue.api;
48+
rollbar.requestStub = sinon.stub(api, 'postItem').callsFake(fakePostItem);
49+
50+
rollbar.testHandlerInstance = rollbar.lambdaHandler(async (_event, _context) => {
51+
// Testing the condition where the client handler sends a message before exit.
52+
// The Rollbar wrapper should send, and should not resolve until the API request
53+
// has completed.
54+
rollbar.info('lambda test message');
55+
return 'done';
56+
});
57+
58+
return rollbar;
59+
},
60+
'invokes handler and receives promise': function(r) {
61+
var handler = r.testHandlerInstance;
62+
63+
// rollbar.lambdaHandler in the above topic should have returned a
64+
// handler with the correct signature. (The callback handler is length = 3)
65+
assert.equal(handler.length, 2);
66+
assert.equal(handler.name, 'rollbarAsyncLambdaHandler');
67+
68+
// The rollbar.wait() in the wrapper should prevent this from
69+
// resolving until the response is received.
70+
r.testPromise = handler({}, stubContext);
71+
},
72+
'after handler invoked': {
73+
topic: function(r) {
74+
var callback = this.callback;
75+
76+
// This timeout allows a few extra ticks so that without the wait in
77+
// the wrapper this test should fail.
78+
setTimeout(function () {
79+
promisePending(r.testPromise, function(pending){
80+
r.promiseIsPending = pending;
81+
callback(r);
82+
});
83+
}, 10);
84+
},
85+
'promise is pending': function(r) {
86+
assert.isTrue(r.promiseIsPending);
87+
},
88+
'after promise resolved': {
89+
topic: function(r) {
90+
var callback = this.callback;
91+
92+
r.testPromise.then(function(value){
93+
assert.equal(value, 'done');
94+
callback(r);
95+
});
96+
},
97+
'sends message before exit': function(r) {
98+
var requestStub = r.requestStub;
99+
100+
// If the handler is allowed to exit prematurely, this will fail.
101+
assert.isTrue(requestStub.called);
102+
var data = requestStub.getCall(0).args[0];
103+
assert.equal(data.body.message.body, 'lambda test message');
104+
105+
requestStub.reset();
106+
}
107+
}
108+
}
109+
}
110+
})
111+
.addBatch({
112+
'async handler catches exception': {
113+
topic: function() {
114+
var rollbar = new Rollbar({
115+
accessToken: 'abc123',
116+
captureUncaught: true,
117+
captureLambdaTimeouts: false
118+
});
119+
var api = rollbar.client.notifier.queue.api;
120+
rollbar.requestStub = sinon.stub(api, 'postItem').callsFake(fakePostItem);
121+
122+
rollbar.testHandlerInstance = rollbar.lambdaHandler(async (_event, _context) => {
123+
// Testing the condition where the client handler throws.
124+
// The Rollbar wrapper should catch and report, and should not reject
125+
// until the API request has completed.
126+
throw new Error('lambda test error');
127+
});
128+
129+
return rollbar;
130+
},
131+
'invokes handler and receives promise': function(r) {
132+
var handler = r.testHandlerInstance;
133+
134+
// rollbar.lambdaHandler in the above topic should have returned a
135+
// handler with the correct signature. (The callback handler is length = 3)
136+
assert.equal(handler.length, 2);
137+
assert.equal(handler.name, 'rollbarAsyncLambdaHandler');
138+
139+
// The rollbar.wait() in the wrapper should prevent this from
140+
// resolving until the response is received.
141+
r.testPromise = handler({}, stubContext);
142+
},
143+
'after handler invoked': {
144+
topic: function(r) {
145+
var callback = this.callback;
146+
147+
promisePending(r.testPromise, function(pending){
148+
r.promiseIsPending = pending;
149+
callback(r);
150+
});
151+
},
152+
'promise is pending': function(r) {
153+
assert.isTrue(r.promiseIsPending);
154+
},
155+
'after promise rejected': {
156+
topic: function(r) {
157+
var callback = this.callback;
158+
159+
r.testPromise.catch(function(error){
160+
assert.equal(error.message, 'lambda test error');
161+
callback(r);
162+
});
163+
},
164+
'sends message before exit': function(r) {
165+
var requestStub = r.requestStub;
166+
167+
// If the handler is allowed to exit prematurely, this will fail.
168+
assert.isTrue(requestStub.called);
169+
var data = requestStub.getCall(0).args[0];
170+
assert.equal(data.body.trace_chain[0].exception.message, 'lambda test error');
171+
172+
requestStub.reset();
173+
}
174+
}
175+
}
176+
}
177+
}).export(module, {error: false});

0 commit comments

Comments
 (0)