Using JVM Clojure for Google Cloud Functions
I recently ran into a requirement where a Google Cloud Function (GCF) seemed a good fit.
I wanted to use Clojure for this, at least as a first cut, but the examples I found
were centered around ClojureScript/NodeJS, which wound up being a non-starter
for various boring reasons. Googling about turned up essentially nothing about
how to use JVM Clojure for this purpose. The GCF story for Java is about as
easy as it can get, just implement a known interface, uberjar,
and deploy with the gcloud
tool.
My first attempt in Clojure used gen-class
, but deployment resulted in this
exception:
"Exception in thread "main" java.lang.ExceptionInInitializerError
at clojure.lang.Namespace.<init>(Namespace.java:34)
at clojure.lang.Namespace.findOrCreate(Namespace.java:176)
at clojure.lang.Var.internPrivate(Var.java:156)
at ck.proxysql_notifier.<clinit>(Unknown Source)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
at com.google.cloud.functions.invoker.NewHttpFunctionExecutor.forClass(NewHttpFunctionExecutor.java:51)
at com.google.cloud.functions.invoker.runner.Invoker.startServer(Invoker.java:243)
at com.google.cloud.functions.invoker.runner.Invoker.main(Invoker.java:129)
Caused by: java.io.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath.
at clojure.lang.RT.load(RT.java:462)
at clojure.lang.RT.load(RT.java:424)
at clojure.lang.RT.<clinit>(RT.java:338)
... 11 more"
First step was to verify that clojure/core__int.class
made it in the jar. It did.
Next guess was some kind of class loader issue. I’ve actually run into this scenario
several
times, where some service requires a Java class implementation, said service uses its
own class loader, and when the class is loaded you run afoul of the way Clojure uses
class loaders to support its dynamic features. It can be confusing, because it’s clear
that the Clojure classes are available in the uberjar, but you get an error that
they’re not found. Given that I’ve hit this problem
more than once, it’s surprising how little is written about it.
This post reminded me
of how I had solved this problem in the past, using setContextClassLoader
. And
this Slack thread
further reminded me that my past solutions used a simple “shim” class, because
the class loader needs to be set before the Clojure runtime is loaded.
I thought I should document the solution here,
since I
encounter this problem infrequently enough to forget the solution between instances.
This solution is specific to implementing a GCF in Clojure, but the general pattern holds in other cases, which is to implement whatever interfaces the service or framework requires as a Java class, which in turn calls into Clojure code implementing the actual logic. Here’s the Clojure:
(ns sparkofreason.cloud-function
(:require [cheshire.core :as json]
(:import (com.google.cloud.functions HttpRequest HttpResponse))
(defn service
[^HttpRequest request ^HttpResponse response]
(let [body (json/parse-stream (.getReader request))
response-writer (.getWriter response)]
(println "Received " body)
(.write response-writer "ok")))
Pretty boring stuff, deserialize the request body JSON, print it out (which will appear in the Stackdriver logs), and return “ok” for the response. The Java wrapper looks like this:
package com.sparkofreason.cloud_function;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.cloud.functions.HttpFunction;
import clojure.java.api.Clojure;
import clojure.lang.IFn;
public class MyCloudFn implements HttpFunction {
static {
Thread.currentThread().setContextClassLoader(MyCloudFn.class.getClassLoader());
IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("sparkofreason.cloud-function");
}
private static final service_impl = Clojure.var("sparkofreason.cloud-function", "service");
@Override
public void service(HttpRequest request, HttpResonse response)
throws Exception {
service_impl.invoke(request, response);
}
}
For the interop, I’m just following Alex Miller’s example of calling Clojure from Java.
The key part is the call to setContextClassLoader
. We put this in the static
constructor before any Clojure stuff happens, to ensure the class loader is
set so that our service or framework will have visibility to the classes which
get loaded when you start poking at clojure.java.api.Clojure
.
You’ll then need to build and deploy this to your service/framework, which for GCF looks like this:
- Call
java -c
on the java file, output the class to some folder like “classes”. - Build an uberjar using some Clojure tool (I used depstar)
including the “classes” (or whatever) directory in the classpath (e.g.
:extra-paths ["classes"]
in a:build
alias if usingdeps.edn
). - In your CLI, go to the folder containing the uberjar.
- Use the
gcloud functions deploy
command to deploy your cloud function. - Test with
curl
or whatever,POST
ing some JSON for the body. You should seeok
returned as the result, and whatever you sent for the body printed in the Stackdriver logs.
The Could not locate clojure/core__init.class
error can be mysterious in this type of interop
scenario, so hopefully if you run into that case, this post will save you some time. Note that I just through
this together quickly from memory, at some point I’ll make a little example repo and update this
post. If you try the code before and find errors before I get that done, please DM @sparkofreason on
the clojurians slack.