Serverless Pathom with GCF
In this tutorial, we are going to implement a Pathom server and deploy it as a Google Cloud Function.
We will start with the code created at the Pathom Tutorial, and make it available as a service.
Project Setup
To make the GCP integration I'll use the library google-cloud-functions-ring-adapter.
Example setup for deps.edn
:
{:paths
["src/main" "src/java"]
:deps
{com.wsscode/pathom3 {:mvn/version "2021.07.10-alpha"}
metosin/muuntaja {:mvn/version "0.6.8"}
nl.epij/google-cloud-functions-ring-adapter {:mvn/version "0.1.0"}
org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/core.async {:mvn/version "1.3.618"}
ring-cors/ring-cors {:mvn/version "0.1.13"}}
:aliases
{:assemble
{:extra-deps {nl.epij.gcf/deploy {:git/url "https://github.com/pepijn/google-cloud-functions-clojure"
:sha "e0f49db974a8f97e1459efd16c0edfb3030a6115"
:deps/root "deploy"}}
:exec-fn nl.epij.gcf.deploy/assemble-jar!
:exec-args {:nl.epij.gcf/entrypoint PathomServer
:nl.epij.gcf/java-paths ["src/java"]
:nl.epij.gcf/compile-path "target/classes"
:nl.epij.gcf/jar-path "target/artifacts/application.jar"}}
:run
{:extra-deps {nl.epij.gcf/deploy {:git/url "https://github.com/pepijn/google-cloud-functions-clojure"
:sha "e0f49db974a8f97e1459efd16c0edfb3030a6115"
:deps/root "deploy"}}
:exec-fn nl.epij.gcf.deploy/run-server!
:exec-args {:nl.epij.gcf/entrypoint PathomServer
:nl.epij.gcf/java-paths ["src/java"]
:nl.epij.gcf/compile-path "target/classes"
:nl.epij.gcf/jar-path "target/artifacts/application.jar"}}}}
Recommended Settings for Pathom
Here are some recommended settings to do when exposing a Pathom API.
Cache plan results
It's common for clients to make the same EQL requests. Pathom can leverage this and persist the planning part of the process across requests. This is how we set it up:
; create a var to store the cache
(defonce plan-cache* (atom {}))
(def env
; persistent plan cache
(-> {::pcp/plan-cache* plan-cache*}
(pci/register
[ip->lat-long
latlong->woeid
woeid->temperature])))
Check the Cache page for more details on how to control the cache.
Ring handler setup
Now let's set up a Ring handler. The two main things for this:
- Setup content negotiation to decode/encode data, I'll use muuntaja.
- Handle the request using the Pathom Boundary Interface.
(ns com.wsscode.pathom-server
(:require
[cognitect.transit :as t]
; to include the env setup from the Tutorial demo
[com.wsscode.pathom3.demos.ip-weather :refer [env]]
[com.wsscode.pathom3.connect.operation.transit :as pcot]
[com.wsscode.pathom3.interface.eql :as p.eql]
[muuntaja.core :as muuntaja]
[muuntaja.middleware :as middleware]))
; create a boundary interface
(def pathom (p.eql/boundary-interface env))
(defn handler [{:keys [body-params]}]
{:status 200
:body (pathom body-params)})
(def muuntaja-options
(update-in
muuntaja/default-options
[:formats "application/transit+json"]
; in this part we setup the read and write handlers for Pathom resolvers and mutations
merge {:decoder-opts {:handlers pcot/read-handlers}
:encoder-opts {:handlers pcot/write-handlers
; write-meta is required if you wanna see execution stats on Pathom Viz
:transform t/write-meta}}))
(def app
(-> handler
(middleware/wrap-format muuntaja-options)))
The :transform t/write-meta
will make transit encode also the meta-data. This means the
running status data will flow. It allows Pathom Viz to show debug information.
Keep in mind the "run status" data is usually much larger than the response itself.
You can mitigate this in two different levels at Pathom:
- Set
:com.wsscode.pathom3.connect.runner/omit-run-stats-resolver-io? true
to remove input/output details. This adds a great reduction in the size of the status. - Set
:com.wsscode.pathom3.connect.runner/omit-run-stats? true
to remove all status from meta
The boundary interface isn't required, but it gives the clients extra capabilities like allowing the user to provide root data.
Now to hook that, we need to create a Java file in our sources that will link our handler:
import nl.epij.gcf.RingHttpFunction;
public class PathomServer extends RingHttpFunction {
public String getHandler() {
return "com.wsscode.pathom-server/app";
}
}
After this part, we can test our server locally:
PORT=13337 clojure -X:run
Once it runs, we can test it by sending a request:
curl --location --request POST 'http://localhost:13337' \
--header 'Content-Type: application/edn' \
--header 'Accept: application/edn' \
--data-raw '{:pathom/eql [:temperature], :pathom/entity {:ip "198.29.213.3"}}'
# => {:temperature 26.064999999999998}
I used EDN fore readability, but you should use application/transit+json
(with transit data) for performance
and to support the custom handlers for resolvers and mutations.
GCF Deploy
For the next steps, you need to have GCP Account and install the Google Cloud SDK.
On Mac, you can install GCP with brew install google-cloud-sdk
To deploy the handler as Google Cloud Function; first we assemble the jar:
clojure -X:assemble
Deploy to GCP (tune as you see fit):
gcloud functions deploy --runtime java11 --source target/artifacts/ --max-instances 1 development-pathom-server-demo --trigger-http --allow-unauthenticated --entry-point PathomServer --memory 2GB --timeout 270
If you go through with no errors, the function should be online!
You can find out the URL to try it in the message output after deploy.
Look for httpsTrigger: ... url:
You can find the full sources of this demo at https://github.com/wilkerlucio/pathom3-demo-gcf.