Ions Reference

This reference covers everything you need to develop ion applications.

Prerequisites

To develop and deploy an ion application, you will need a Datomic Cloud system running version 477-8741 or later, using a split stack. (If you plan to use HTTP Direct, your system must be running the Production Topology.)

In your local development environment, you need to install:

Developing Ions

You develop ions in a tools.deps project orgnanized as follows:

  • The project must be a git repository. (Ions use git SHAs to uniquely name a reproducible revision.)
  • There should be a single deps.edn file located at the project root. (Multiple deps.edn files are incompatible with the use of SHAs to uniquely name a revision.)
  • The :deps sections of deps.edn must include the client-cloud and ion libraries.
  • The deps.edn classpath must include a resource named datomic/ion-config.edn. This resource configures the entry points for an application.

For an example, see the sample project deps.edn and ion-config.edn.

Ion Clients

Ions support the synchronous client API only. Ions do not support the use of asynchronous client or the peer api.

When you create a client with :ion server-type, Datomic will create a client based on where your code is running:

  • If on a Cloud node, Datomic will ignore the rest of the argument map to client and create an in-memory ion client.
  • Otherwise,Datomic will use the rest of the argument map to create a client as for the :cloud server-type.

This allows you to write code that connects from your development environment to a Cloud system for dev and testing, and that can then be deployed to a running system with no code change.

The following example shows how to create a client that connects through a local proxy port to the inventory-dev system in us-east-1 during development. When deployed as an ion, the same code will create an in-memory client on the system it is deployed to.

(require '[datomic.client.api :as d])

(def cfg {:server-type :ion
          :region "us-east-1"
          :system "inventory-dev"
          :endpoint "http://entry.inventory-dev.us-east-1.datomic.net:8182/"
          :proxy-port 8182})

(def client (d/client cfg))

Ion Entry Points

Ion applications can include arbitrary Clojure code, but to be useful they must expose one or more entry points for callers. Ions support six kinds of entry points for different callers, each with a different function signature.

entry point input output
lambda :input JSON, :context map String, InputStream, ByteBuffer, or File
web web request web response
web lambda proxy ionized web request ionized web response
transaction fn db + data transaction data
query fn data data
xform pulled attribute value data

Lambda Entry Point

A lambda entry point is a function that takes a map with two keys:

  • :input is a String containing the input JSON payload.
  • :context contains all the data fields of the AWS Lambda context as a Clojure map, with all keys (including nested and dynamic map keys) converted to keywords.

A lambda entry point can return any of String, ByteBuffer, InputStream, or File; or it can throw an exception to signal an error.

For example, the following echo function simply echos back its invocation:

(defn echo
  [{:keys [context input]}]
  input)

Datomic automatically creates AWS Lambdas that forward requests to your lambda entry points. Your entry points have access to all the resources of your Datomic system, and are not constrained by the execution context of AWS Lambdas. However, lambda entry point responses must still flow back through AWS Lambda to callers, so lambda entry points are bound by the AWS Lambda limits on response size (6MB) and response timeout (900 seconds).

Web Entry Point

A web entry point is a function that takes the following input map:

key req'd value example
:body no InputStream "hello"
:headers yes map string->string {"x-foo" "bar"}
:protocol yes HTTP protocol "HTTP/1.1"
:remote-addr yes caller host "example.com"
:request-method yes HTTP verb as keyword :get
:scheme no :http or :https :https
:server-name yes server host name "example.com"
:server-port no TCP port 443
:uri yes request URI  

and returns

key req'd value example
:body no InputStream or String "goodbye"
:headers yes map string->string {"x-foo" "bar"}
:status yes HTTP status code 200

The input and output maps are a subset of the Clojure Ring Spec, and many web applications should be able to run unmodified as web ions.

The ion-starter project includes an example web ion.

Web Lambda Proxy Entry Points

If you running the Solo Topology, web ions are not available, as they require a load balancer. You can instead create a web service using API Gateway Lambda Proxy integration.

To build a web lambda proxy, develop and test an ordinary web entry point as described above. Then, create a separate var that wraps the web entry point function using ionize, which converts a web ion to the signature required by a Lambda Proxy.

The Ions Tutorial includes an example.

The input map for web lambda proxies includes the following additional keys.

key req'd value example
:json no api-gateway/json {"resource":"{proxy+}","path":"datomic","httpMethod":"POST" … }
:data no api-gateway/data {"Path": "/datomic","Querystringparameters": null, "Pathparameters": {"Proxy": "datomic"}…

ion-config.edn

The datomic/ion-config.edn resource configures the application's entry points. ion-config is an edn map with the following keys:

  • :app-name is string name of a Datomic application
  • :allow is a vector of fully qualified symbols naming query or transaction functions. When you deploy an application, Datomic will automatically require all the namespaces mentioned under :allow.
  • :xforms is a vector of fully qualified symbols naming functions for use in xform.
  • :lambdas is a map configuring AWS Lambda entry points
  • :http-direct is a map configuring an HTTP Direct entry point.

:lambdas

:lambdas is a map from lambda names (keywords) to lambda configurations.

For each entry in the :lambdas map, ion deploy will create an AWS lambda named $(group)-$(name), where group is the :group key you used to invoke deploy, and name is the name is the lambda name. AWS Lambda names are limited to 64 characters, so make sure that you choose a lambda name that will not exceed this when appended to your group name.

A lambda configuration supports the following keys:

key required value example default
:fn yes symbol datomic.ion.starter/echo  
:description no string "echo" ""
:timeout-secs no positive int 60 60
:concurrency-limit no positive int or :none 20 20
:integration no :api-gateway/proxy :api-gateway/proxy  

:concurrency-limit sets AWS reserved concurrency. Set it :none if you want the AWS Lambda to use unreserved concurrency.

The :integration key customizes integration between the lambda and AWS. The only value currently supported is :api-gateway/proxy, which causes the AWS Lambda proxy to conform to the special conventions for AWS Lambda proxies.

HTTP Direct Configuration

The HTTP Direct configuration map supports the following keys:

key required description default
:handler-fn yes fully qualified handler function name  
:pending-ops-queue-length no number of operations to queue 100
:processing-concurrency no number of concurrent operations 16
:pending-ops-exceeded-message no return message when queue is full "Throttled"
:pending-ops-exceeded-code no HTTP status code returned when queue is full 429

Nodes will enqueue up to :pending-ops-queue-length HTTP direct requests, servicing them with :processing-concurrency threads. When the queue is full, nodes will reject requests with :pending-ops-exceeded-messsage and :pending-ops-exceeded-code.

You can monitor the HTTP direct queues with the HttpDirectOpsPending and HttpDirectThrottled metrics.

ion-config example

The example below is taken from the ion-starter project.

{:allow [;; transaction function
         datomic.ion.starter/create-item

         ;; query function
         datomic.ion.starter/feature-item?]            

 ;; AWS Lambda entry points
 :lambdas {:echo
           {:fn datomic.ion.starter/echo
            :description "Echos input"}
           :get-tutorial-schema
           {:fn datomic.ion.starter/get-tutorial-schema
            :description "returns the schema for the Datomic docs tutorial"}
           :get-items-by-type
           {:fn datomic.ion.starter/get-items-by-type
            :description "Lambda handler that returns items by type"}

           ;; AWS Lambda API Gateway proxy integration entry point
           :items-by-type-ionize
           {:fn datomic.ion.starter/items-by-type-ionize
            :integration :api-gateway/proxy
            :description "ionized version of items-by-type"}}

 ;; HTTP Direct entry point
 :http-direct {:handler-fn datomic.ion.starter/items-by-type}
 :app-name "<YOUR-APP-HERE>"}

JVM Settings

When developing and testing locally, it can be helpful to match the JVM settings that your code will run under in Datomic Cloud.

Datomic Cloud runs Java version 1.8.0.

instance type Heap (-Xmx) Stack (-Xss)
t3.small (solo) 1220m 512k
t3.medium 2582m 1m
m5.large 5198m 1m
i3.large 10520m 1m
i3.xlarge 21280m 1m

Additionally all instances use the following flags:

-XX:+UseG1GC -XX:MaxGCPauseMillis=50 -Dclojure.spec.skip-macros=true

Push

Ion push creates an application revision in S3 that can later be deployed to one or more compute groups. Ion push understands your dependencies at a granular level, so e.g. all revisions can share the same copy of a common library. This is more efficient than "uberjar" style deployment.

To push an application revision, call datomic.ion.dev with an :op of :push:

clojure -A:ion-dev '{:op :push (options)}'
Keyword required value example
:op yes :push :push
:uname no unreproducible name "janes-wip"
:creds-profile no AWS profile name "janes-profile"
:region no AWS region "us-east-1"

Push will ensure that git and maven libs exist in your ion code bucket:

s3://$(ion-code-bucket)/datomic/libs

and will create a CodeDeploy application revision located at

s3://$(ion-code-bucket)/datomic/apps/$(app-name)/$(git-sha|uname).zip

On success, push returns a map with the following keys:

keyword required value
:rev no git SHA for the commit that was pushed
:uname no unreproducible name for the push
:deploy-groups yes list of available groups for deploy
:deploy-command yes sample command for deploy
:doc no documentation
:dependency-conflicts no map describing conflicts

Dependency Conflicts

Because ions run on the same classpath as Datomic Cloud, it is possible for ion dependencies to conflict with Datomic's own dependencies. If this happens:

  • Datomic's dependencies will be used.
  • The return from push will warn you with a :dependency-conflicts map.
  • You can add the :deps from the conflicts map to your local project so that you can test against the libraries used by Datomic Cloud.

The Datomic team works to keep Datomic's dependencies up-to-date. If you are unable to resolve a dependency conflict, please contact support.

Unreproducible Push

By default, an ion push is reproducible, i.e. built entirely from artifacts in git or maven repositories. For a push to be reproducible, your git working tree must be clean, and your deps.edn project cannot include any local/root dependencies. A reproducible push is uniquely named by the SHA of its git commit.

In some situations, you may want to push code that Datomic does not know to be reproducible. For example:

  • you are testing work-in-progress that does not have a git commit
  • you are implementing your own approach to reproducibility

For these situations, ions permit an unreproducible push. Since an unreproducible push has no git SHA, you must specify an unrepro name (:uname).

You are responsible for making the uname unique within your org+region+app. If unames are not unique, ions will be unable to automatically roll back failed deploys.

Deploy

Ion deploy deploys a pushed revision to a compute group.

When you deploy, Datomic will use AWS Step Functions to

  • CodeDeploy the code for the application onto each instance in the compute group.
  • Automatically roll back to the previous deployment if the application does not deploy correctly (e.g. if loading a namespace throws an exception.)
  • Ensure that active databases are present in memory before routing requests to newly updated nodes.
  • Ensure that all the lambdas requested via the ion-config.edn map exist and are configured correctly.

Node deployment happens one node at a time, so a Datomic system will remain available with N-1 members active throughout a deployment.

To deploy an application, call datomic.ion.dev with an :op of :deploy:

clojure -A:ion-dev '{:op :deploy (options)}'
keyword required value example
:op yes :deploy :deploy
:group yes compute group name "my-datomic-compute"
:rev (or :uname) output from push "6468765f843d70e01a7a2e483405c5fcc9aa0883"
:uname (or :rev) input to push "janes-wip"
:creds-profile no AWS profile name "janes-profile"
:region no AWS region "us-east-1"

The time to complete an entire deployment is the sum of

  • the sum of times to deploy to each node
  • the time to (re)configure lambdas

The time to deploy to a single node is the sum of

  • the time to copy new code and deps from S3 to the node
  • the time to stop and restart the Datomic Cloud process
  • the time to load active databases into memory

Deployment to a single node can take from 20 seconds up to several minutes, depending on the number and size of active databases.

You can monitor a deployment with the deploy-status command or from the AWS Console.

deploy-status

To check the status of a deploy, call datomic.ion.dev with an :op of :deploy-status:

clojure -A:ion-dev '{:op :deploy-status (options)}'
keyword required value example
:op yes :deploy-status :deploy-status
:execution-arn yes output from deploy "arn:aws:states:us-east-1:123456789012:execution:datomic-compute:datomic-compute-1526506240469"
:creds-profile no AWS profile name "janes-profile"
:region no AWS region "us-east-1"

Deploy Status returns the the Step Functions reference of the overall ion deploy and of the code deploy as a map:

{:deploy-status "SUCCEEDED"
 :code-deploy-status "SUCCEEDED"}

On success, both the overall ion deploy and Code Deploy status will eventually return with "SUCCEEDED"

Console Status

You can monitor a deploy in near real-time from the Step Functions Console. Look for a state machine named datomic-$(group).

Invoking Ions

Ion entry points that extend the power of the Client API are described in the relevant documentation sections:

Web service endpoints can be invoked by any HTTP client, such as curl, wget, or a web browser. Lambda invocation is described below.

Invoking Lambdas

When you deploy an application with lambda ions, Datomic will create AWS Lambdas named:

$(group)-$(name)

where group is the :group key you used to invoke deploy, and name is the name under the :lambdas key in ion-config.edn.

The tutorial includes an example of invoking an ion Lambda via the AWS CLI.

Configuration

Ions typically have some amount of configuration, e.g. the name of a Datomic database that they use. If you are deploying your ion application to a single compute group, then you might choose to keep such configuration in Clojure vars in your application source code. This is easy, if not simple:

  • Everything is in a single place. (For small services, it may even fit in a single screen of Clojure code.)
  • Code and configuration are versioned together on every ion push.

If you have more complex ions, potentially deployed to multiple compute groups (or even multiple systems), there are several challenges to consider:

  • Configuration values may have a lifecycle that is independent of application source code. They should not be hard-coded in the application, and should be managed separately from source code.
  • Applications need a way to obtain their configuration values at runtime.
  • Configuration values may be sensitive, and should not be stored or conveyed as plaintext.
  • Configuration values may need to be secured at a granular level.

Datomic Cloud provides tools to help with these challenges.

configuration scope source IAM
app-info map compute group Datomic no
environment map compute group user no
parameters arbitrary user yes

app-info map

If you deploy the same ion to different compute groups, your code may want to be conditional based on which group it is running in. You can discover your compute group at runtime with get-app-info, which takes no arguments, and returns a map that will include at least these keys:

For example, the following excerpt from deploy-monitor gets the app-name:

(get (ion/get-app-info) :app-name)

When running outside Datomic Cloud, get-app-info returns the value of the DATOMIC_APP_INFO_MAP environment variable, read as edn.

If you want to add your own per-compute-group configuration, you can create an environment map.

Environment Map

You create an environment map when you first create a compute group. For example, the environment map below specifies that a compute group is for a :staging deployment environment:

environment-map.png

You can then retrieve the environment map at runtime with get-env, which takes no arguments, and returns the environment map.

For example, the following excerpt from deploy-monitor retrieves the deployment environment specified above:

(get (ion/get-env) :env)

You can set the environment map for local development via the DATOMIC_ENV_MAP environment variable. When running outside Datomic Cloud, get-env returns the value of DATOMIC_ENV_MAP, read as edn.

If you need to change the environment map for a running compute group, you can perform a CloudFormation parameter upgrade.

If you need more flexible lifecycle, or granular security, you can use parameters as described below.

Parameters

  • A parameter is a named slot known to application code that can be filled in with a specific parameter value at runtime.
  • The AWS Systems Manager Parameter Store provides an implementation of parameters that supports an independent lifecycle, encryption for sensitive data, and IAM security over a hierarchical naming scheme.
  • All Datomic cluster nodes have read permission on parameter store keys that begin with datomic-shared.
  • The ion library provides get-params as a convenience for reading parameter store parameters.

get-params

The get-params function is a convenience abstraction over GetParametersByPath. get-params take a map with a :path key, and it returns all the parameters under path as a map from parameter name string to parameter value string, decrypting if necessary.

For example, the call to get-params below returns all the parameters under the path /datomic-shared/prod/deploy-monitor.

(ion/get-params {:path "/datomic-shared/prod/deploy-monitor"})

Per-System Permissions

The datomic-shared parameter prefix is readable by any Datomic system. If you want more granular permissions, you can choose your own naming convention (under a different prefix!), and explicitly add permissions to the IAM policy for your Datomic nodes.

Configuration Example

The deploy-monitor sample application demonstrates using app-info, the environment map, and parameters together. First, the environment and app-info map are used to create a key prefix following the the naming convention:

/datomic-shared/(env)/(app-name)/
  • datomic-shared is a prefix readable by all Datomic nodes
  • env is taken from the environment map, and differentiates different environments for the same application, e.g. "ci" vs. "prod".
  • app-name is the ion app-name.

Then, this prefix is used to name configuration keys in the parameter store. deploy-monitor needs four parameters:

  • a Datomic database name
  • a Slack channel
  • two encrypted tokens for Slack

Given this convention, the following AWS CLI commands will create the parameters for the "prod" environment:

# actual parameter values not shown
aws ssm put-parameter --name /datomic-shared/prod/deploy-monitor/db-name --value $DB_NAME --type String
aws ssm put-parameter --name /datomic-shared/prod/deploy-monitor/channel --value $CHANNEL --type String
aws ssm put-parameter --name /datomic-shared/prod/deploy-monitor/bot-token --value $BOT_TOKEN --type SecureString
aws ssm put-parameter --name /datomic-shared/prod/deploy-monitor/verification-token --value $VERIFICATION_TOKEN --type SecureString

You could use similar commands to create parameters for additional environments.

At runtime, deploy-monitor uses get-app-info and get-env to load the information needed to create a Parameter Store path, and then reads all of its parameter values with get-params:

(def get-params
  "Returns the params under /datomic-shared/(env)/(app-name)/, where

env       value of get-env :env
app-name  value of get-app-info :app-name"
  (memoize
   #(let [app (or (get (ion/get-app-info) :app-name) (fail :app-name))
          env (or (get (ion/get-env) :env) (fail :env))]
      (ion/get-params {:path (str "/datomic-shared/" (name env) "/" app "/")}))))

Best Practices

  • Ions are designed to let you do most of your development and testing at a local REPL, with a much faster feedback look than push/deploy. Take advantage of this wherever possible.
  • You do not need ions, or a Datomic Cloud system, to get develop and test Datomic applications. You can start with dev-local, and when you are ready for ions, make your code portable with divert-system.
  • You should not store AWS credentials in the Parameter Store, as Datomic Cloud fully supports IAM roles.
  • The tools.deps classpath is the source of truth for an ion revision. If you need a file for local development that you do not want deployed, you should add that file's directory with a tools.deps alias. (You cannot e.g. "subtract" the file with a .gitignore. Ions use git only for calculating a SHA.)
  • Provision AWS Lambda concurrency to mitigate cold starts.
  • If you have dependencies that are needed only in dev and test, place them under a tools.deps alias so that you do not deploy them to production.