Donkey
Modern Clojure HTTP server and client built for ease of use and performance
Install / Use
/learn @AppsFlyer/DonkeyREADME
Donkey
<p align="center"><img src="https://user-images.githubusercontent.com/29622398/160290393-63e0ccba-d7ab-4853-89d1-7b524ac79178.png" width=350/></p>Modern Clojure, Ring compliant, HTTP server and client, designed for ease of use and performance
Note: this project is no longer maintained.
Table of Contents
- Usage
- Requirements
- Building
- Start up options
- Creating a Donkey
- Server
- Client
- Metrics
- Debug mode
- Troubleshooting
- License
TOC Created by gh-md-toc
Usage
Including the library in project.clj
[com.appsflyer/donkey "0.5.2"]
Including the library in deps.edn
com.appsflyer/donkey {:mvn/version "0.5.2"}
Including the library in pom.xml
<dependency>
<groupId>com.appsflyer</groupId>
<artifactId>donkey</artifactId>
<version>0.5.2</version>
</dependency>
Requirements
Building
The preferred way to build the project for local development is using Maven. It's also possible to generate an uberjar using Leiningen, but you must use Maven to install the library locally.
Creating a jar with Maven
mvn package
Creating an uberjar with Leiningen
lein uberjar
Installing to a local repository
mvn clean install
Start up options
JVM system properties that can be supplied when running the application
-Dvertx.threadChecks=false: Disable blocked thread checks. Used by Vert.x to warn the user if an event loop or worker thread is being occupied above a certain threshold which will indicate the code should be examined.-Dvertx.disableContextTimings=true: Disable timing context execution. These are used by the blocked thread checker. It does not disable execution metrics that are exposed via JMX.
Creating a Donkey
In Donkey, you create HTTP servers and clients using a - Donkey. Creating
a Donkey is simple:
(ns com.appsflyer.sample-app
(:require [com.appsflyer.donkey.core :refer [create-donkey]]))
(def ^Donkey donkey-core (create-donkey))
We can also configure our donkey instance:
(ns com.appsflyer.sample-app
(:require [com.appsflyer.donkey.core :refer [create-donkey]]))
(def donkey-core (create-donkey {:event-loops 4}))
There should only be a single Donkey instance per application. That's because
the client and server will share the same resources making them very efficient.
Donkey is a factory for creating server(s) and client(s) (you can create
multiple servers and clients with a Donkey, but in almost all cases you will
only want a single server and / or client per application).
Server
The following examples assume these required namespaces
(:require [com.appsflyer.donkey.core :refer [create-donkey create-server]]
[com.appsflyer.donkey.server :refer [start]]
[com.appsflyer.donkey.result :refer [on-success]])
Creating a Server
Creating a server is done using a Donkey instance. Let's start by creating a
server listening for requests on port 8080.
(->
(create-donkey)
(create-server {:port 8080})
start
(on-success (fn [_] (println "Server started listening on port 8080"))))
Note that the following example will not work yet - for it to work we need to add a route which we will do next.
After creating the server we start it, which is an asynchronous call that may
return before the server actually started listening for incoming connections.
It's possible to block the current thread execution until the server is running
by calling start-sync or by "derefing" the arrow macro.
The next thing we need to do is define a route. We talk about routes in depth later on, but a route is basically a definition of an endpoint. Let's define a route and create a basic "Hello world" endpoint.
(->
(create-donkey)
(create-server {:port 8080
:routes [{:handler (fn [_request respond _raise]
(respond {:body "Hello, world!"}))}]})
start
(on-success (fn [_] (println "Server started listening on port 8080"))))
As you can see we added a :routes key to the options map used to initialize
the server. A route is a map that describes what kind of requests are handled at
a specific resource address (or :path), and how to handle them. The only
required key is :handler, which will be called when a request matches a route.
In the example above we're saying that we would like any request to be handled
by our handler function.
Our handler is a Ring compliant asynchronous handler. If you are not familiar with the Ring async handler specification, here's an excerpt:
An asynchronous handler takes 3 arguments: a request map, a callback function for sending a response, and a callback function for raising an exception. The response callback takes a response map as its argument. The exception callback takes an exception as its argument.
In the handler we are calling the response callback respond with a response
map where the body of the response is "Hello, world!".
If you run the example and open a browser on http://localhost:8080 you will
see a page with "Hello, World!".
Routes
In Donkey HTTP requests are routed to handlers. When you initialize a server you
define a set of routes that it should handle. When a request arrives the server
checks if one of the routes can handle the request. If no matching route is
found, then a 404 Not Found response is returned to the client.
Let's see a route example:
{
:handler (fn [request respond raise] ...)
:handler-mode :non-blocking
:path "/api/v2"
:match-type :simple
:methods [:get :put :post :delete]
:consumes ["application/json"]
:produces ["application/json"]
:middleware [(fn [handler] (fn [request respond raise] (handler request respond raise)))]
}
:handler A function that accepts 1 or 3 arguments (depending on
:handler-mode). The function will be called if a request matches the route.
This is where you call your application code. The handler should return a
response map with the following optional fields:
:status: The response status code (defaults to 200):headers: Map of key -> valueStringpairs:body: The response body asbyte[],String, orInputStream
:handler-mode To better understand the use of the :handler-mode, we need to
first get some background about Donkey. Donkey is an abstraction built on top of
a web tool-kit called Vert.x, which in turn is built on a
very popular and performant networking library called
Netty. Netty's architecture is based on the concept of a
single threaded event loop that serves requests. An event loop is conceptually a
long-running task with a queue of events it needs to dispatch. As long as events
are dispatched "quickly" and don't occupy too much of the event loop's time, it
can dispatch events at a very high rate. Because it is single threaded, or in
other words serial, during the time it takes to dispatch one event no other
event can be dispatched. Therefore, it's extremely important not to block the
event loop.
The :handler-mode is a contract where you declare the type of handling your
route does - :blocking or :non-blocking (default).
:non-blocking means that the handler is performing very fast CPU-bound tasks,
or non-blocking IO bound tasks. In both cases the guarantee is that it will not
block the event loop. In this case the :handler must accept 3 arguments.
Sometimes reality has it that we have to deal with legacy code that is doing
some blocking operations that we just cannot change easily. For these occasions
we have :blocking handler mode. In this case, the handler will be called on a
separate worker thread pool without needing to worry about blocking the event
loop. The worker thread pool size can be configured when creating a
Donkey instance by setting the :worker-threads option.
:path is the first thing a route is matched on. It is the part after the
hostname in a URI that identifies a resource on the host the client is trying to
access.
