Cloud Functions and Cat Facts

A few weeks back, Myles Borins and I did a talk at Google I/O about choosing which cloud services to use in different situations. We both feel that the best technical talks have a solid storyline and so we had a hypothetical business built around “Cat Facts as a Service” motivating our talk.

Building out the demos for this talk was the first time I got to explore Google Cloud Functions, and I learned several interesting things while writing our Cat Fact function.

First, some caveats. My JavaScript isn’t great. I did Node.js professionally for 2 years, but I used CoffeeScript and focused more on the DevOps side of building a reproducible Node.js app instead of on the language and framework. Second, this isn’t a real app. It is demo code so I skip much error and input handling that I would typically include.

The goal of our app was to provide a cat picture (stored in Cloud Storage with a publicly readable URL) and a cat fact when the function was called. I’m lazy and didn’t want to deal with schemas for a project this small, so we stored our facts and links to the pictures in Cloud Datastore. Facts and pictures were our two entities. Each had a content field (the URL for the picture and a text “fact” for the fact) and a timestamp for the last time it was used. The timestamp was there to make sure that I didn’t reuse facts or pictures too frequently.

Uploading the Facts to Datastore

I created a file of cat facts of questionable veracity and then set about uploading them to Cloud Datastore. Since I was on a deadline, I used my primary language, Ruby.

datastore = Google::Cloud::Datastore.new(project: [project-id], credentials: [path-to-key])

facts = File.read(ARGV[0]).split("\n")

facts.each do |fact|
  temp = datastore.entity "Fact" do |f|
    f["fact"] = fact
    f["LastUsed"] = Time.now
  end

  datastore.save temp
end

The code is pretty straightforward for Rubyists. Create a datastore library object. Open up the file of facts and split. Then iterate through the facts creating a new Datastore entity for each with the timestamp set to now. Then save. It worked on the first try and I got through my file of facts in under a minute. I did something similar for the photos.

Cloud Function Code

Our cloud function is pretty simple. It queries Datastore for a picture, and a fact, updates the timestamp on both entities, and then sends the result back as JSON.

exports.getCatFact = (req, res) => {
  const datastore = Datastore();

  const factQuery = datastore.createQuery('Fact').order('LastUsed').limit(1);
  const picQuery  = datastore.createQuery('Picture').order('LastUsed').limit(1);

  Promise.all([datastore.runQuery(factQuery), datastore.runQuery(picQuery)])
  .then((results) => {
    const fact = results[0][0][0].fact;
    const pic = results[1][0][0].URL;

    updateTimestamp(results[0][0][0]);
    updateTimestamp(results[1][0][0]);

    var payload = {"fact": fact, "pic": pic};
    res.status(200).send(payload);
  });
}

I use the Datastore library for Node.js to handle database interactions. Using that library, creating a query is pretty straightforward. You specify the type of entity you want and then some parameters. For this query, I want only the least recently used entry, so I specify an order and a limit.

Myles taught me about promises which cleaned up the code considerably and weren’t something I’d used much in my previous JavaScript journey. The only complicated bit of this turned out to be the updateTimestamp function. Reading from Datastore is somewhat easier than writing to Datastore.

function updateTimestamp(entity) {
    const datastore = Datastore();
    const key = entity[datastore.KEY];

    entity['LastUsed'] = new Date();

    datastore.save({key: key, data: entity}, err => { console.log(err); });
}

In retrospect, it doesn’t seem that complicated, but a couple of things tripped me up. The primary issue was that the JavaScript Datastore library doesn’t return the Datastore key as an obvious part of the entity “object.” This behavior is different than the libraries in some other languages and stumped me for a good three days before I finally sorted it out by reading this bug. It turns out the key is there, but I couldn’t see it when I used console.log on the object. To access the key, you have to use the “magic” property datastore.KEY. The key is necessary to update the timestamps when I use a fact or picture. I also had to figure out the syntax datastore.save({key: key, data: entity}). You can use datastore.save(entity) , but that doesn’t appear to work unless it is a new record or the record has the key encoded in the right way. Finally, it took me a while to figure out how to get an actual timestamp instead of an integer number of seconds since the epoch. new Date() was key. Date.now() gave me an integer and a couple other commonly recommended ways of getting the current date/time in JavaScript either also returned integers or returned strings. That resulted in a column in Datastore with mixed types, which made the ordering work in ways I didn’t expect.