github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/fn/commands/lambda/node/bootstrap.js (about) 1 'use strict'; 2 3 var fs = require('fs'); 4 5 var oldlog = console.log 6 console.log = console.error 7 8 // Some notes on the semantics of the succeed(), fail() and done() methods. 9 // Tests are the source of truth! 10 // First call wins in terms of deciding the result of the function. BUT, 11 // subsequent calls also log. Further, code execution does not stop, even where 12 // for done(), the docs say that the "function terminates". It seems though 13 // that further cycles of the event loop do not run. For example: 14 // index.handler = function(event, context) { 15 // context.fail("FAIL") 16 // process.nextTick(function() { 17 // console.log("This does not get logged") 18 // }) 19 // console.log("This does get logged") 20 // } 21 // on the other hand: 22 // index.handler = function(event, context) { 23 // process.nextTick(function() { 24 // console.log("This also gets logged") 25 // context.fail("FAIL") 26 // }) 27 // console.log("This does get logged") 28 // } 29 // 30 // The same is true for context.succeed() and done() captures the semantics of 31 // both. It seems this is implemented simply by having process.nextTick() cause 32 // process.exit() or similar, because the following: 33 // exports.handler = function(event, context) { 34 // process.nextTick(function() {console.log("This gets logged")}) 35 // process.nextTick(function() {console.log("This also gets logged")}) 36 // context.succeed("END") 37 // process.nextTick(function() {console.log("This does not get logged")}) 38 // }; 39 // 40 // So the context object needs to have some sort of hidden boolean that is only 41 // flipped once, by the first call, and dictates the behavior on the next tick. 42 // 43 // In addition, the response behaviour depends on the invocation type. If we 44 // are to only support the async type, succeed() must return a 202 response 45 // code, not sure how to do this. 46 // 47 // Only the first 256kb, followed by a truncation message, should be logged. 48 // 49 // Also, the error log is always in a json literal 50 // { "errorMessage": "<message>" } 51 var Context = function() { 52 var concluded = false; 53 54 var contextSelf = this; 55 56 // The succeed, fail and done functions are public, but access a private 57 // member (concluded). Hence this ugly nested definition. 58 this.succeed = function(result) { 59 if (concluded) { 60 return 61 } 62 63 // We have to process the result before we can conclude, because otherwise 64 // we have to fail. This means NO EARLY RETURNS from this function without 65 // review! 66 if (result === undefined) { 67 result = null 68 } 69 70 var failed = false; 71 try { 72 // Output result to log 73 oldlog(JSON.stringify(result)); 74 } catch(e) { 75 // Set X-Amz-Function-Error: Unhandled header 76 console.log("Unable to stringify body as json: " + e); 77 failed = true; 78 } 79 80 // FIXME(nikhil): Return 202 or 200 based on invocation type and set response 81 // to result. Should probably be handled externally by the runner/swapi. 82 83 // OK, everything good. 84 concluded = true; 85 process.nextTick(function() { process.exit(failed ? 1 : 0) }) 86 } 87 88 this.fail = function(error) { 89 if (concluded) { 90 return 91 } 92 93 concluded = true 94 process.nextTick(function() { process.exit(1) }) 95 96 if (error === undefined) { 97 error = null 98 } 99 100 // FIXME(nikhil): Truncated log of error, plus non-truncated response body 101 var errstr = "fail() called with argument but a problem was encountered while converting it to a to string"; 102 103 // The semantics of fail() are weird. If the error is something that can be 104 // converted to a string, the log output wraps the string in a JSON literal 105 // with key "errorMessage". If toString() fails, then the output is only 106 // the error string. 107 try { 108 if (error === null) { 109 errstr = null 110 } else { 111 errstr = error.toString() 112 } 113 oldlog(JSON.stringify({"errorMessage": errstr })) 114 } catch(e) { 115 // Set X-Amz-Function-Error: Unhandled header 116 oldlog(errstr) 117 } 118 } 119 120 this.done = function() { 121 var error = arguments[0]; 122 var result = arguments[1]; 123 if (error) { 124 contextSelf.fail(error) 125 } else { 126 contextSelf.succeed(result) 127 } 128 } 129 130 var plannedEnd = Date.now() + (getTimeoutInSeconds() * 1000); 131 this.getRemainingTimeInMillis = function() { 132 return Math.max(plannedEnd - Date.now(), 0); 133 } 134 } 135 136 function getTimeoutInSeconds() { 137 var t = parseInt(getEnv("TASK_TIMEOUT")); 138 if (Number.isNaN(t)) { 139 return 3600; 140 } 141 142 return t; 143 } 144 145 var getEnv = function(name) { 146 return process.env[name] || ""; 147 } 148 149 var makeCtx = function() { 150 var fnname = getEnv("AWS_LAMBDA_FUNCTION_NAME"); 151 // FIXME(nikhil): Generate UUID. 152 var taskID = getEnv("TASK_ID"); 153 154 var mem = getEnv("TASK_MAXRAM").toLowerCase(); 155 var bytes = 300 * 1024 * 1024; 156 157 var scale = { 'b': 1, 'k': 1024, 'm': 1024*1024, 'g': 1024*1024*1024 }; 158 // We don't bother validating too much, if the last character is not a number 159 // and not in the scale table we just return a default value. 160 // We use slice instead of indexing so that we always get an empty string, 161 // instead of undefined. 162 if (mem.slice(-1).match(/[0-9]/)) { 163 var a = parseInt(mem); 164 if (!Number.isNaN(a)) { 165 bytes = a; 166 } 167 } else { 168 var rem = parseInt(mem.slice(0, -1)); 169 if (!Number.isNaN(rem)) { 170 var multiplier = scale[mem.slice(-1)]; 171 if (multiplier) { 172 bytes = rem * multiplier 173 } 174 } 175 } 176 177 var memoryMB = bytes / (1024 * 1024); 178 179 var ctx = new Context(); 180 Object.defineProperties(ctx, { 181 "functionName": { 182 value: fnname, 183 enumerable: true, 184 }, 185 "functionVersion": { 186 value: "$LATEST", 187 enumerable: true, 188 }, 189 "invokedFunctionArn": { 190 // FIXME(nikhil): Should be filled in. 191 value: "", 192 enumerable: true, 193 }, 194 "memoryLimitInMB": { 195 // Sigh, yes it is a string. 196 value: ""+memoryMB, 197 enumerable: true, 198 }, 199 "awsRequestId": { 200 value: taskID, 201 enumerable: true, 202 }, 203 "logGroupName": { 204 // FIXME(nikhil): Should be filled in. 205 value: "", 206 enumerable: true, 207 }, 208 "logStreamName": { 209 // FIXME(nikhil): Should be filled in. 210 value: "", 211 enumerable: true, 212 }, 213 "identity": { 214 // FIXME(nikhil): Should be filled in. 215 value: null, 216 enumerable: true, 217 }, 218 "clientContext": { 219 // FIXME(nikhil): Should be filled in. 220 value: null, 221 enumerable: true, 222 }, 223 }); 224 225 return ctx; 226 } 227 228 var setEnvFromHeader = function () { 229 var headerPrefix = "CONFIG_"; 230 var newEnvVars = {}; 231 for (var key in process.env) { 232 if (key.indexOf(headerPrefix) == 0) { 233 newEnvVars[key.slice(headerPrefix.length)] = process.env[key]; 234 } 235 } 236 237 for (var key in newEnvVars) { 238 process.env[key] = newEnvVars[key]; 239 } 240 } 241 242 243 function run() { 244 setEnvFromHeader(); 245 // FIXME(nikhil): Check for file existence and allow non-payload. 246 var path = process.env["PAYLOAD_FILE"]; 247 var stream = process.stdin; 248 if (path) { 249 try { 250 stream = fs.createReadStream(path); 251 } catch(e) { 252 console.error("bootstrap: Error opening payload file", e) 253 process.exit(1); 254 } 255 } 256 257 var input = ""; 258 stream.setEncoding('utf8'); 259 stream.on('data', function(chunk) { 260 input += chunk; 261 }); 262 263 stream.on('error', function(err) { 264 console.error("bootstrap: Error reading payload stream", err); 265 process.exit(1); 266 }); 267 268 stream.on('end', function() { 269 var payload = {} 270 try { 271 if (input.length > 0) { 272 payload = JSON.parse(input); 273 } 274 } catch(e) { 275 console.error("bootstrap: Error parsing JSON", e); 276 process.exit(1); 277 } 278 279 if (process.argv.length > 2) { 280 var handler = process.argv[2]; 281 var parts = handler.split('.'); 282 // FIXME(nikhil): Error checking. 283 var script = parts[0]; 284 var entry = parts[1]; 285 var started = false; 286 try { 287 var mod = require('./'+script); 288 var func = mod[entry]; 289 if (func === undefined) { 290 oldlog("Handler '" + entry + "' missing on module '" + script + "'"); 291 return; 292 } 293 294 if (typeof func !== 'function') { 295 throw "TypeError: " + (typeof func) + " is not a function"; 296 } 297 started = true; 298 var cback 299 // RUN THE FUNCTION: 300 mod[entry](payload, makeCtx(), functionCallback) 301 } catch(e) { 302 if (typeof e === 'string') { 303 oldlog(e) 304 } else { 305 oldlog(e.message) 306 } 307 if (!started) { 308 oldlog("Process exited before completing request\n") 309 } 310 } 311 } else { 312 console.error("bootstrap: No script specified") 313 process.exit(1); 314 } 315 }) 316 } 317 318 function functionCallback(err, result) { 319 if (err != null) { 320 // then user returned error and we should respond with error 321 // http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-mode-exceptions.html 322 oldlog(JSON.stringify({"errorMessage": errstr })) 323 return 324 } 325 if (result != null) { 326 oldlog(JSON.stringify(result)) 327 } 328 } 329 330 run()