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()