├── cranker-websockets.png ├── cranker-http-request.png ├── .gitignore ├── src └── cranker │ ├── utils.clj │ ├── testrig.clj │ └── core.clj ├── project.clj └── README.md /cranker-websockets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cranker/master/cranker-websockets.png -------------------------------------------------------------------------------- /cranker-http-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicferrier/cranker/master/cranker-http-request.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /src/cranker/utils.clj: -------------------------------------------------------------------------------- 1 | (ns cranker.utils 2 | "Utils for cranker - reversing the polairty of your HTTP and Websockets." 3 | (:require [taoensso.timbre :as timbre :refer (debug log info warn error fatal)])) 4 | 5 | (defn cranker-formatter 6 | "A log formatter." 7 | [{ :keys [level throwable message timestamp hostname ns] }] 8 | (format "[%s] %s%s" 9 | timestamp 10 | (or message "") 11 | (or (timbre/stacktrace throwable "\n" ) ""))) 12 | 13 | (defn trunc-str [s c] 14 | (if (> (count s) c) 15 | (str (subs s 0 c) "...") 16 | s)) 17 | 18 | ;;; utils.clj ends here 19 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject cranker "0.1.0-SNAPSHOT" 2 | :description "Connect HTTP in reverse to scale." 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.6.0"] 7 | [org.clojure/core.async "0.1.346.0-17112a-alpha"] 8 | [com.taoensso/timbre "3.3.1"] 9 | ;;[org.clojure/tools.logging "0.2.6"] 10 | [stylefruits/gniazdo "0.3.0"] 11 | [org.clojure/data.json "0.2.5"] 12 | [http-kit "2.1.19"]] 13 | ;;:main ^:skip-aot cranker.core 14 | :main cranker.core 15 | :target-path "target/%s" 16 | :aot :all 17 | :profiles {:uberjar {:aot :all} }) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cranker 2 | 3 | Avoid orchestration by reversing the polairty of your infrastructure. 4 | 5 | ## todo 6 | 7 | * websockets over websockets 8 | * seems like we need some framing 9 | * but the client websocket could be tied to a cranker websocket 10 | * ability to use one cranker-lb for multiple services 11 | * when approx connects to lb have it send some service identification 12 | * and then cranker lb should keep a websocket pool per-service id 13 | * load balancers could send traffic to that with http headers or paths 14 | * if it was a header then the approx could use the same header 15 | * when approx makes the websocket send X-Service-Id: my-service 16 | * configure the loadbalancer to send to cranker-ln with X-Service-Id: my-service 17 | * weights 18 | * have cranker approx connect to multiple cranker lbs 19 | * but associate them with a weight 20 | * so if cranker approx knows lb xxx is in another datacenter it can cost it more 21 | * the cost is sent with the approx websocket creation 22 | * so now cranker lb knows what cost to apply 23 | * need to test sending a bunch of data 24 | * the load balancer side and the app server side need to start independently 25 | * we need to be able to turn the test mode on 26 | * and select the mode: test, loadbal or appserver 27 | * the app server side needs to wait 28 | * implement HTTP proxying better 29 | * adding proxy header in correct circumstances 30 | 31 | ## where we are right now 32 | 33 | We have both ends of cranker coded 34 | 35 | * lb-server and cranker-server implement the load balancer end 36 | * start-lb starts both the servers 37 | * both servers need an address but default to 38 | * 8000 - cranker-server (websockets) 39 | * 8001 - load balancer proxy 40 | * cranker-connector implements the app server side 41 | * it needs the app-server address ... 42 | * ... and the address of the cranker server 43 | * we have a test mode that: 44 | * sets up a fake appserv on 8003 45 | * sets up cranker-connector from the appserv to a load balancer 46 | * fires a request at the load balancer 47 | * shows that the request comes back via the fake appserv 48 | 49 | ## how it works 50 | 51 | HTTP requests with cranker: 52 | 53 | ![cranker for http](cranker-http-request.png) 54 | 55 | * a single cranker/a runs near your load balancer, where it can have a fixed address 56 | * it listens to 2 sockets, x and y 57 | * receives connections from the load balancer on x 58 | * receives websocket connections from cranker/b on y 59 | * these will arrive in lumps 60 | * when a cranker-connector starts it tries to connect a lot of sockets 61 | * cranker/b runs near your app server 62 | * makes websockets to cranker/a 63 | * what comes over the websocket is encoded HTTP requests from the load balancer 64 | * proxy the request to the app server 65 | * return the response encoded over the websocket 66 | 67 | 68 | Websockets with cranker: 69 | 70 | ![cranker for websockets](cranker-websockets.png) 71 | 72 | ## Installation 73 | 74 | Download from http://example.com/FIXME. 75 | 76 | 77 | ## Usage 78 | 79 | FIXME: explanation 80 | 81 | $ java -jar cranker-0.1.0-standalone.jar [args] 82 | 83 | 84 | ## Examples 85 | 86 | ... 87 | 88 | ### Bugs 89 | 90 | ... 91 | 92 | 93 | ## License 94 | 95 | Copyright © 2015 FIXME 96 | 97 | Distributed under the Eclipse Public License either version 1.0 or (at 98 | your option) any later version. 99 | -------------------------------------------------------------------------------- /src/cranker/testrig.clj: -------------------------------------------------------------------------------- 1 | (ns cranker.testrig 2 | "Test rig for cranker - reversing the polairty of your HTTP and Websockets." 3 | (:require [cranker.utils :refer :all]) 4 | (:require [clojure.string :as str]) 5 | (:require [clojure.core.async :refer [>!! thread]]) 6 | (:require [clojure.pprint :as pp]) 7 | (:require [taoensso.timbre :as timbre :refer (debug log info warn error fatal)]) 8 | (:require [org.httpkit.client :as http-client]) 9 | (:require [gniazdo.core :as ws]) ; will need for socket connections 10 | (:require [clojure.walk]) 11 | (:require [org.httpkit.server :as http-server]) 12 | (:import [java.io File FileOutputStream])) 13 | 14 | (defn lb-http-request 15 | "Make a request to the fake load balancer. 16 | 17 | This is test code to allow us to run all our tests internally. 18 | 19 | `address' is the http address to talk to. 20 | 21 | `query-params' is a query string. 22 | 23 | `form-data' is POST form data to pass, mutually exclusive with 24 | `multipart'??? 25 | 26 | `multipart' is a file to pass. 27 | 28 | `status', `headers' and `body-regex' are values to use to do 29 | assertions on. If present the assertions are performed." 30 | [address &{ :keys [query-params 31 | form-params 32 | multipart 33 | id 34 | method 35 | status-assert 36 | headers-assert 37 | body-regex-assert]}] 38 | (let [response @(if (= method :post) 39 | (http-client/request 40 | {:url address 41 | :method :post 42 | :query-params query-params 43 | :form-params form-params 44 | :multipart multipart } nil) 45 | ;; Else get 46 | (http-client/get address { :query-params query-params })) 47 | { :keys [status headers body] } response] 48 | (when form-params (info id "lb-http-request the form-params are: " form-params)) 49 | (when multipart (info id (format "lb-http-request the multipart is: %s" multipart))) 50 | (info id (format "lb-http-request[%s][status]: %s" address status)) 51 | (info id (format "lb-http-request[%s][headers]: %s" address headers)) 52 | (info id (format "lb-http-request[%s][body]: %s" address (trunc-str body 40))) 53 | (do 54 | (when status-assert 55 | (assert (== status-assert status))) 56 | (when headers-assert 57 | (doall 58 | (map (fn [[k v]] 59 | (info id "headers [" k "] == " v " -> " (pp/cl-format nil "~s" (headers k))) 60 | (when (headers k) (assert (= (headers k) v)))) 61 | headers-assert))) 62 | (when body-regex-assert 63 | (debug (pp/cl-format nil "~s" body-regex-assert)) 64 | (assert (re-matches body-regex-assert body)))))) 65 | 66 | (defn appserv-handler 67 | "Handle requests from cranker as if we are an app server. 68 | 69 | This is test code. It fakes a real app server so we can do all our 70 | tests of cranker flow internally." [req] 71 | (http-server/with-channel req channel 72 | (http-server/on-close channel (fn [status] (debug "appserv close - " status))) 73 | (http-server/on-receive 74 | channel (fn [data] (warn "ooer! data from an appserv con: " data))) 75 | (info "appserv-handler request -> " req) 76 | (let [response { :status 200 77 | :headers { "content-type" "text/html" 78 | "server" "fake-appserver-0.0.1" } 79 | :body (str "

my fake appserver!

" 80 | (if (req :body) 81 | (format "
%s
" (slurp (req :body))) 82 | "")) }] 83 | (http-server/send! channel response)))) 84 | 85 | (defn ^File gen-tempfile 86 | "Generate a tempfile, the file will be deleted before jvm shutdown." 87 | ([size extension] 88 | (let [string-80k 89 | (fn [] 90 | (apply 91 | str 92 | (map char 93 | (take (* 8 1024) 94 | (apply concat (repeat (range (int \a) (int \z)))))))) 95 | const-string (let [tmp (string-80k)] 96 | (apply str (repeat 1024 tmp))) 97 | tmp (doto (File/createTempFile "tmp_" extension) 98 | (.deleteOnExit))] 99 | (with-open [w (FileOutputStream. tmp)] 100 | (.write w ^bytes (.getBytes (subs const-string 0 size)))) 101 | tmp))) 102 | 103 | (defn test-lb [lb-ctrl ap-ctrl] 104 | (let [fake-appserv-stop (http-server/run-server appserv-handler { :port 8003 }) 105 | tempfile (gen-tempfile 5000 ".jpg")] 106 | (thread 107 | (Thread/sleep 1000) 108 | ;; Multipart request 109 | (lb-http-request 110 | "http://localhost:8003/blah" 111 | :id "straight#1" 112 | :form-params { :a 1 :b 2 } 113 | ;; Looks to me like multipart is not supported by http-kit/client 114 | ;; check the http-client code 115 | ;; FIXME - looks like it's just out version!!! 116 | :multipart [{ :name "image" :content tempfile}] 117 | :method :post 118 | :status-assert 200 119 | :headers-assert { :server "fake-appserver-0.0.1,http-kit" } 120 | :body-regex-assert #"

my fake.*
-+HttpKitFormBoundary.*\r 121 | Content-Disposition: form-data; name=\"image\"\r 122 | .*\r 123 | .*\r 124 | .*\r 125 | .*\r 126 |
") 127 | ;; Show that the direct request works 128 | (lb-http-request 129 | "http://localhost:8003/blah" 130 | :id "straight#2" 131 | :form-params { :a 1 :b 2 } 132 | :method :post 133 | :status-assert 200 134 | :headers-assert { :server "fake-appserver-0.0.1,http-kit" } 135 | :body-regex-assert #"

my fake.*
b=2&a=1
") 136 | ;; Show a cranker request works - we actually need a ton of 137 | ;; different requests here 138 | 139 | ;; - with and without 140 | ;; parameters 141 | ;; headers 142 | ;; uploaded files 143 | (lb-http-request 144 | "http://localhost:8001/blah" 145 | :id "cranker#1" 146 | :status-assert 200 147 | :headers-assert { :server "fake-appserver-0.0.1,http-kit,http-kit" } 148 | :body-regex-assert #"

my fake.*

$") 149 | 150 | ;; Show that a cranker request with data works 151 | (lb-http-request 152 | "http://localhost:8001/blah" 153 | :id "cranker#2" 154 | :form-params { "a" 1 "b" 2 } 155 | :method :post 156 | :status-assert 200 157 | :headers-assert { :server "fake-appserver-0.0.1,http-kit,http-kit" } 158 | :body-regex-assert #"

my fake.*

a=1&b=2
$") 159 | 160 | ;; And now cranker with a file... 161 | (lb-http-request 162 | "http://localhost:8001/blah" 163 | :id "cranker#3" 164 | :multipart [{ :name "image" :content tempfile}] 165 | :form-params { "a" 1 "b" 2 } 166 | :method :post 167 | :status-assert 200 168 | :headers-assert { :server "fake-appserver-0.0.1,http-kit,http-kit" } 169 | :body-regex-assert #"

my fake.*
-+HttpKitFormBoundary.*\r 170 | Content-Disposition: form-data; name=\"image\"\r 171 | .*\r 172 | .*\r 173 | .*\r 174 | .*\r 175 |
")) 176 | (Thread/sleep 2000) 177 | (fake-appserv-stop) 178 | (>!! lb-ctrl [:stop]) 179 | (>!! ap-ctrl [:stop]))) 180 | 181 | ;; Ends 182 | -------------------------------------------------------------------------------- /src/cranker/core.clj: -------------------------------------------------------------------------------- 1 | (ns cranker.core 2 | "Cranker - reverse the polairty of your HTTP and Websockets." 3 | (:gen-class) 4 | (:require [cranker.utils :refer :all]) 5 | (:require [cranker.testrig :refer :all]) 6 | (:require [clojure.test :refer :all]) 7 | (:require [clojure.string :as str]) 8 | (:require [clojure.pprint :as pp]) 9 | (:require 10 | [clojure.core.async 11 | :refer [>! !! !! chan [@socket data])))] 97 | (deliver socket (ws/connect endpoint :on-receive callback)) 98 | @socket)) 99 | 100 | (defn cranker-connector 101 | "The app server side of cranker. 102 | 103 | `cranker-lb' which is the ws uri of the cranker server 104 | 105 | `app-server-uri' which is the uri of the app server. 106 | 107 | `number' - the number of connections to open to the cranker server. 108 | 109 | Returns a promise which we might wait on." 110 | [ctrl app-server-uri cranker-lb number] 111 | (thread 112 | (let [ch (chan) 113 | sockets (doseq [n (range number)] 114 | (cranker-make-ws ch cranker-lb))] 115 | (debug "cranker-connector: started cranker") 116 | (go-loop 117 | [[socket data] (