Cached Results

This is an example of a caching mechanism that I introduced into a page I wrote for some project.

Problem

At a certain point, during the development of a visual calculator, I noticed that the computation of results corresponding to moving sliders on the page had become much slower than before.

Solution

To mitigate the problem I introduced a caching mechanism based on:

  1. selecting slow methods, whose results we’re going to cache;
  2. wrapping those methods into a check:
    • if there is a cached result, use it
    • otherwise, compute it now, save it, then use it;
  3. deleting cached results as soon as they become stale.

A nice thing about this cache mechanism is that it’s standard.

  • We don’t need to change the code of the app.
  • A new cached result is computed only when unavailable.
  • Cached results become unavailable after certain events.

A nice thing of my implementation is that it automatically computes which cached values to invalidate, based on their dependencies lists.

  • This means that we only have to define the dependencies lists, which is very easy. Just look at the method’s definition and list all of its inputs (params, function calls, and contextual values).
    monthlyLoanProfitMoney(): number {
        const result = (this.totalInstalmentMoney() - this.loanAmountMoney.value) / this.loanDurationMonths();
        return result;
    }
    

    Dependencies: totalInstalmentMoney, loanAmountMoney, loanDurationMonths.

Code

import * as _ from 'lodash';

export class CachedResults {

  private cache: any = {};

  // private definitions: any = {
  //   // computed: [input1, input2, ...],
  // };

  private invalidations: any;

  static getPathsOfTheTransitiveClosure(edges) {
    // TODO check for dependency loops and throw an error
    const result = [];
    _.forEach(edges, edge => {
      let [from,] = edge;
      let path = [from];
      let found = edge;
      while (found) {
        let [, to] = found;
        path.push(to);
        found = edges.find(([x,]) => x === to);
      }
      result.push(path);
    });
    return result;
  }

  constructor(
    private definitions: any,
  ) {
    this.invalidations = this.getInvalidations();
  }

  use(methodName, fn) {
    if (!this.cache[methodName]) {
      this.cache[methodName] = fn(...args);
    }
    return this.cache[methodName];
  }

  invalidate(changed) {
    const invalidCache = this.invalidations[changed];
    invalidCache.forEach(x => { delete this.cache[x] });
  }

  private getInvalidations() {
    const arcs = this.getArcsFromInputToComputed();
    const paths = CachedResults.getPathsOfTheTransitiveClosure(arcs);
    return this.getPathsFromRoots(paths);
  }

  private getComputedValues() {
    return Object.keys(this.definitions);
  }

  private getArcsFromInputToComputed() {
    const result = [];
    _.forEach(this.definitions, (inputs, computed) => {
      _.forEach(inputs, input => {
        result.push([input, computed]);
      });
    });
    return result;
  }

  private getRootValues() {
    const allInputs = [];
    _.forEach(this.definitions, inputs => {
      allInputs.push(...inputs);
    });
    const computed = this.getComputedValues();
    const origins = _.difference(allInputs, computed);
    return _.uniq(origins);
  }

  private getPathsFromRoots(paths) {
    const roots = this.getRootValues();
    const result = {};
    roots.forEach(origin => {
      const path = paths.find(([x,]) => x === origin);
      const [, ...rest] = path;
      result[origin] = rest;
    });
    return result;
  }

}

Usage

import { CachedResults } from '...';
//...
cache: CachedResults;
//...
constructor() {
    this.cache = new CachedResults({
        loanDurationMonths: 'loanDurationYears'.split(' '),
        totalInstalmentMoney: 'loanDurationMonths monthlyInstalmentMoney'.split(' '),
        monthlyInstalmentMoney: 'annualPercentageRate loanDurationYears loanAmountMoney'.split(' '),
    });
}
//...
monthlyLoanProfitMoney(): number {
    return this.cache.use('monthlyLoanProfitMoney', () => {
        const result = (this.totalInstalmentMoney() - this.loanAmountMoney.value) / this.loanDurationMonths();
        return result;
    });
}
//...
<input id="loanDurationYears" type="range" min="5" max="30" step="1" (change)="cache.invalidate('loanDurationYears')" />

Secreta: a little suite for managing configuration secrets

After learning AWS and Lambda, in the past few weeks I wrote Secreta, which is a set of three tools to manage configuration secrets in AWS Lambda functions.

secreta-generate-aws is a command line utility that creates a pair of keys for asymmetric key encryption, using RSA. It does so by

  1. running the forge module directly in an AWS Lambda function
  2. saving the private key directly in an AWS Parameter, encrypted and protected by an access tag
  3. saving the public key to a local file

secreta-encrypt is a command line utility that encrypts to a local .secreta file the secrets referenced in your configuration files (using a public key).

secreta-decrypt-aws is a NodeJS module that you can install into your AWS Lambda function to decrypt .secreta files in memory (using the corresponding private key, retrieved from the AWS Parameter) and merge them into the rest of the configuration, as if they had never been encrypted.

You can share the public key in your project repository. This will allow any other trusted developer (like yourself) to always have a current public key to keep encrypting configuration secrets. These could be obtained by exchanging GPG email messages, for example.

You can share the .secreta files in your project repository. This will allow any other developer to always have current configuration secrets to keep deploying your AWS Lambda function.

Documentation here.

AWS Lambda Invoke Errors

Lately, I’ve been developing a Lambda function to create a pair of keys, store one in a parameter and return the other to the user. Today I got my first clean run, and I’m writing this to celebrate.

As part of learning lots of things along the way, because I hadn’t developed anything on AWS before, I found out that AWS reports errors in many different ways. Here are those that occurred to me in the last few hours.

See: AWS Lambda Invoke Errors (documentation)

Error — InvalidZipFileException

{ InvalidZipFileException: Lambda was not able to unzip the file
       <stack trace...>
     message: 'Lambda was not able to unzip the file',
     code: 'InvalidZipFileException',
     time: 2017-09-16T16:04:04.558Z,
     requestId: '<request id...>',
     statusCode: 502,
     retryable: true } }
  • This error occurred because the zip file I had uploaded was wrong: the files it contained were stored into a directory.
  • This is a Lambda creation error, and it’s quite complete.
  • Unhandled / Handled classification is limited to Lambda execution.

Error — Process exited

{ errorMessage: 'RequestId: <request id...> Process exited before completing request' }
  • This error occurred because I threw a validation error.
  • I could catch this error, but don’t, so it’s classified as Unhandled.
  • The logs show my error and its stack trace.
17:12:39 START RequestId: <request id...> Version: $LATEST
17:12:39 2017-09-16T17:12:39.442Z   <request id...>    Error: Expected a pair ID for your keys. at setPairId (/var/task/createPairOfKeys.js:9832:15) at handler (/var/task/createPairOfKeys.js:9805:9)
17:12:39 END RequestId: <request id...>
17:12:39 REPORT RequestId: <request id...> Duration: 203.72 ms Billed Duration: 300 ms Memory Size: 128 MB Max Memory Used: 78 MB
17:12:39 RequestId: <request id...> Process exited before completing request

Error — Timed out

{ errorMessage: '2017-09-16T17:32:08.746Z <request id...> Task timed out after 10.00 seconds' }
  • This error occurred because generating a pair of keys is not fast (bigint maths involved) and, in my few tests, it sometimes needed more than 40 seconds.
  • Surprisingly, my MacBook is faster than AWS Lambda. A few times it took less than 1 second, most of the times less than 6 seconds, and only once it timed out at 10 seconds.
  • I can’t catch this error, so it’s classified as Unhandled.

Error — AccessDeniedException

{ errorMessage: 'User: <user arn...> is not authorized to perform: ssm:PutParameter on resource: <resource arn...>',
        errorType: 'AccessDeniedException',
        stackTrace: [Object] } } }
  • This error occurred because the role I had assigned to my Lambda function didn’t have the right to write SSM parameters.
  • I catch this error and swallow it, so it’s classified as Handled.

Error — ParameterAlreadyExists

{ errorMessage: 'The parameter already exists. To overwrite this value, set the overwrite option in the request to true.',
        errorType: 'ParameterAlreadyExists',
        stackTrace: [Object] } } }
  • This error occurred because I explicitly create my SSM parameter with overwrite set to false. Not gonna change it, this is by design.
  • Notice how this is an exception whose name doesn’t end with Exception.
  • I catch this error and swallow it, so it’s classified as Handled.