43 | Key | Value/Description |
44 | :max-idle-per-key | Max connections idle for a pool to a particular server, default is 2 |
45 | :min-idle-per-key | Minimum connections idle for a pool to a particular server, default is 0 |
46 | :max-total | The maximum number of connections to open for all servers, default is 100 |
47 | :max-total-per-key | Same as :max-total but per key, default is 100 |
48 | :min-idle-per-key | Maximum idle connections per key, default is 0 |
49 | :close-pool-jvm-shutdown | If set to true, the pool will be closed for all keys and connections on JVM shutdown |
50 |
51 |
--------------------------------------------------------------------------------
/doc/retry.md:
--------------------------------------------------------------------------------
1 | # Connection Retry Policy
2 |
3 | ## Overview
4 |
5 | A IO Function can at any point throw an exception, the retry policy determines how
6 | on Exception should the function be called, if any or if the exception should be
7 | re thrown
8 |
9 | ## Default Retry Policy
10 |
11 | A ```tcp-driver.routing.retry.DefaultRetryPolicy``` record is provided that implements
12 | ```tcp-driver.routing.retry.IRetry``` protocol, and calls the IO Functions if
13 | and exception is thrown, doing so N times, where N is provided as part of the creation
14 | of DefaultRetryPolicy.
15 |
16 |
17 | ## Usage
18 |
19 | ```clojure
20 | (require '[tcp-driver.routing.retry :as retry])
21 |
22 | (let [rpolicy (retry/retry-policy 3)
23 | f (fn [] (prn "try-function and throw ex") (throw (Exception. "test")))]
24 | (try
25 | (retry/with-retry rpolicy f)
26 | (catch Exception e
27 | (prn (= (get (ex-data e) :retries) 3)))))
28 |
29 | ```
--------------------------------------------------------------------------------
/doc/stream.md:
--------------------------------------------------------------------------------
1 | #IO Stream Support
2 |
3 | The namespace ```tcp-driver.io.stream``` provides all the functions required to open/write/read
4 | to and from an ```InputStream``` or ```OutputStream```.
5 |
6 | The java class ```tcpdriver.io.IOUtil``` implements the backend of the stream functions for efficiency.
7 |
8 | ## Blocking IO
9 |
10 | Note that for client communication blocking IO is the most convenient and also simplest to reason about,
11 | providing automatic back pressure. The only issue with blocking reads is that there are no timeouts implemented
12 | in the API, this library implements timeouts on blocking reads without any background threads.
13 |
14 | Have a look at the java class ```tcpdriver.io.IOUtil``` to see how its implemented using avialble and partial reads.
15 |
16 | ## Timeouts
17 |
18 | All stream read operations have a timeout in milliseconds argument, and will throw a ```TimeoutException``` if
19 | the required bytes could not be read from the connection's ```InputStream``` in that time.
20 |
21 |
22 |
23 | ## Example
24 |
25 | ```clojure
26 |
27 | (require '[tcp-driver.io.stream :as tcp-stream])
28 |
29 | ;;get a connection either directly or from a pool
30 | ;;write a short string
31 | (tcp-stream/write-short-str conn "hi")
32 |
33 | ;;read a short string
34 | (tcp-stream/read-short-str conn 1000)
35 |
36 | ```
37 |
38 |
39 |
--------------------------------------------------------------------------------
/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | lein eastwood "{:exclude-linters [:unused-ret-vals] :exclude-namespaces [tcp-driver.io.conn-test tcp-driver.io.pool-test tcp-driver.routing.policy-test tcp-driver.routing.retry-test tcp-driver.test.util tcp-driver.driver-test]}"
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject tcp-driver "0.1.2-SNAPSHOT"
2 | :description "Java/Clojure TCP Connections done right"
3 | :url "https://github.com/gerritjvv/tcp-driver"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 |
7 | :global-vars {*warn-on-reflection* true
8 | *assert* true}
9 |
10 | :javac-options ["-target" "1.8" "-source" "1.8" "-Xlint:-options"]
11 | :jvm-opts ["-Xmx1g"]
12 |
13 | :source-paths ["src/clojure"]
14 | :java-source-paths ["src/java"]
15 | :dependencies [
16 | [org.clojure/clojure "1.8.0"]
17 |
18 | [fun-utils "0.6.2"]
19 | [org.apache.commons/commons-pool2 "2.4.2"]
20 | [prismatic/schema "1.1.3"]]
21 |
22 | :plugins [[jonase/eastwood "0.2.3"]])
23 |
--------------------------------------------------------------------------------
/src/clojure/tcp_driver/driver.clj:
--------------------------------------------------------------------------------
1 | (ns
2 | ^{:doc "
3 |
4 | The idea is the access TCP client connections like any other product driver code would e.g the cassandra or mondodb driver.
5 | There are allot of situations where software in the past (my own experience) became unstable because the TCP connections
6 | were not written or treated with the equivalent importance as server connections.
7 | Writing the TCP connection as if it were a product driver sets a certain design mindset.
8 |
9 | This is the main entry point namespace for this project, the other namespaces are:
10 |
11 | tcp-driver.io.conn -> TCP connection abstractions
12 | tcp-driver.io-pool -> Connection pooling and creating object pools
13 | tcp-driver.io-stream -> reading and writing from TCP Connections
14 |
15 | The main idea is that a driver can point at 1,2 or more servers, for each server a Pool of Connections are maintained
16 | using a KeyedObjectPool from the commons pool2 library.
17 |
18 | Pooling connections is done not only for performance but also make connection error handling easier, the connection
19 | is tested and retried before given to the application user, and if you have a connection at least at the moment
20 | of handoff you know that it is connection and ready to go.
21 |
22 | "}
23 | tcp-driver.driver
24 |
25 | (:require
26 | [schema.core :as s]
27 | [clojure.tools.logging :refer [error]]
28 | [tcp-driver.io.pool :as tcp-pool]
29 | [tcp-driver.io.conn :as tcp-conn]
30 | [tcp-driver.routing.policy :as routing]
31 | [tcp-driver.routing.retry :as retry]) (:import (java.io IOException)))
32 |
33 |
34 | ;;;;;;;;;;;;;;
35 | ;;;;;; Schemas and Protocols
36 |
37 | (def IRouteSchema (s/pred #(satisfies? routing/IRoute %)))
38 |
39 | (def IRetrySchema (s/pred #(satisfies? retry/IRetry %)))
40 |
41 | (def IPoolSchema (s/pred #(satisfies? tcp-pool/IPool %)))
42 |
43 | (def DriverRetSchema {:pool tcp-pool/IPoolSchema
44 | :routing-policy IRouteSchema
45 | :retry-policy IRetrySchema})
46 |
47 |
48 | ;;;;;;;;;;;;;;
49 | ;;;;;; Private functions
50 |
51 | (defn throw-no-connection! []
52 | (throw (RuntimeException. "No connection is available to perform the send")))
53 |
54 |
55 | (defn select-send!
56 | "ctx - DriverRetSchema
57 | host-address if specified this host is used, otherwise the routing policy is asked for a host
58 | io-f - function that takes a connection and on error throws an exception
59 | timeout-ms - connection timeout"
60 | ([ctx io-f timeout-ms]
61 | (select-send! ctx nil io-f timeout-ms))
62 | ([ctx host-address io-f timeout-ms]
63 | {:pre [ctx io-f timeout-ms]}
64 | (loop [i 0]
65 | (if-let [host (if host-address host-address (routing/-select-host (:routing-policy ctx)))]
66 |
67 | (let [pool (:pool ctx)]
68 |
69 | ;;;try the io-f, if an exception then only if we haven't tried (count hosts) already
70 | ;;;loop and retry, its expected that the routing policy blacklist or remove the host on error
71 |
72 | (let [host-key (select-keys host [:host :port])
73 | res (try
74 |
75 | (if-let [
76 | conn (tcp-pool/borrow pool host-key timeout-ms)]
77 |
78 | (try
79 | (io-f conn)
80 | (catch Throwable e
81 | ;;any exception will cause invalidation of the connection.
82 | (tcp-pool/invalidate pool host-key conn)
83 | (throw e))
84 | (finally
85 | (try
86 | (tcp-pool/return pool host-key conn)
87 | (catch Exception e nil))))
88 |
89 | (throw-no-connection!))
90 |
91 | (catch Exception t
92 |
93 | ;;blacklist host
94 | (routing/-blacklist! (:routing-policy ctx) host-key)
95 |
96 | (routing/-on-error! (:routing-policy ctx) host-key t)
97 | (ex-info (str "Error while connecting to " host-key) {:throwable t :host host-key :retries i :hosts (routing/-hosts (:routing-policy ctx))})))]
98 |
99 | (if (instance? Throwable res)
100 | (do
101 | (error res)
102 | (if (< i (count (routing/-hosts (:routing-policy ctx))))
103 | (recur (inc i))
104 | (throw res)))
105 | res)))
106 |
107 | (throw-no-connection!)))))
108 |
109 | (defn retry-select-send!
110 | "Send with the retry-policy, select-send! will be retried depending on the retry policy"
111 | ([{:keys [retry-policy] :as ctx} host-address io-f timeout-ms]
112 | {:pre [retry-policy]}
113 | (retry/with-retry retry-policy #(select-send! ctx host-address io-f timeout-ms)))
114 | ([{:keys [retry-policy] :as ctx} io-f timeout-ms]
115 | {:pre [retry-policy]}
116 | (retry/with-retry retry-policy #(select-send! ctx io-f timeout-ms))))
117 |
118 |
119 | ;; routing-policy is a function to which we pass the routing-env atom, which contains {:hosts (set [tcp-conn/HostAddressSchema]) } by default
120 | (s/defn create [pool :- IPoolSchema
121 | routing-policy :- IRouteSchema
122 | retry-policy :- IRetrySchema
123 | ] :- DriverRetSchema
124 | {:pool pool
125 | :routing-policy routing-policy
126 | :retry-policy retry-policy})
127 |
128 | ;;;;;;;;;;;;;;;;
129 | ;;;;;; Public API
130 |
131 | (defn send-f
132 | "
133 | Apply the io-f with a connection from the connection pool selected based on
134 | the retry policy, and retried if exceptions in the io-f based on the retry policy
135 | ctx - returned from create
136 | io-f - function that should accept the tcp-driver.io.conn/ITCPConn
137 | timeout-ms - the timeout for connection borrow"
138 | ([ctx host-address io-f timeout-ms]
139 | (retry-select-send! ctx host-address io-f timeout-ms))
140 | ([ctx io-f timeout-ms]
141 | (retry-select-send! ctx io-f timeout-ms)))
142 |
143 |
144 | (defn create-default
145 | "Create a driver with the default settings for tcp-pool, routing and retry-policy
146 | hosts: a vector or seq of {:host :port} maps
147 | return: DriverRetSchema
148 |
149 | pool-conf : tcp-driver.io.pool/PoolConfSchema
150 |
151 | Routing policy: The default routing policy will select hosts at random and on any exception blacklist a particular host.
152 | To add/remove/blacklist a node use the public functions add-host, remove-host and blacklist-host in this namespace.
153 | "
154 | ^{:arg-lists [routing-conf pool-conf retry-limit]}
155 | [hosts & {:keys [routing-conf pool-conf retry-limit] :or {retry-limit 10 routing-conf {} pool-conf {}}}]
156 | {:pre [
157 | (s/validate tcp-pool/PoolConfSchema pool-conf)
158 | (s/validate [tcp-conn/HostAddressSchema] hosts)
159 | (number? retry-limit)
160 | ]}
161 | (create
162 | (tcp-pool/create-tcp-pool pool-conf)
163 | (apply routing/create-default-routing-policy hosts (mapcat identity routing-conf))
164 | (retry/retry-policy retry-limit)))
165 |
166 | (defn close
167 | "Close the driver connection pool"
168 | ^{:arg-lists [pool]}
169 | [{:keys [pool]}]
170 | (tcp-pool/close pool))
171 |
172 | (defn add-host [{:keys [routing-policy]} host]
173 | {:pre [(s/validate tcp-conn/HostAddressSchema host)]}
174 | (routing/-add-host! routing-policy host))
175 |
176 | (defn remove-host [{:keys [routing-policy]} host]
177 | {:pre [(s/validate tcp-conn/HostAddressSchema host)]}
178 | (routing/-remove-host! routing-policy host))
179 |
180 | (defn blacklist-host [{:keys [routing-policy]} host]
181 | {:pre [(s/validate tcp-conn/HostAddressSchema host)]}
182 | (routing/-blacklist! routing-policy host))
183 |
184 |
185 | (defn blacklisted? [{:keys [routing-policy]} host]
186 | {:pre [(s/validate tcp-conn/HostAddressSchema host)]}
187 | (routing/-blacklisted? routing-policy host))
188 |
--------------------------------------------------------------------------------
/src/clojure/tcp_driver/io/conn.clj:
--------------------------------------------------------------------------------
1 | (ns
2 | ^{:doc "TCP Connection abstractions and implementations
3 | see host-address and tcp-conn-factory"}
4 | tcp-driver.io.conn
5 | (:require [schema.core :as s])
6 | (:import
7 | (java.net InetAddress Socket SocketAddress InetSocketAddress)
8 | (org.apache.commons.pool2 BaseKeyedPooledObjectFactory PooledObject KeyedPooledObjectFactory)
9 | (org.apache.commons.pool2.impl DefaultPooledObject)
10 | (java.io InputStream OutputStream)))
11 |
12 |
13 | ;;;;;;;;;;;;;;;;;;;;;;;;;
14 | ;;;;;;;;;;;;Protocol & Data
15 |
16 | (def HostAddressSchema {:host s/Str :port s/Int s/Any s/Any})
17 |
18 | (defrecord HostAddress [^String host ^int port])
19 |
20 | (defprotocol ITCPConn
21 | (-input-stream [this])
22 | (-output-stream [this])
23 | (-close [this])
24 | (-valid? [this]))
25 |
26 | (def ITCPConnSchema (s/pred (partial satisfies? ITCPConn)))
27 |
28 | ;;;;;;;;;;;;;;;;;;;;;;;;;
29 | ;;;;;;;;;;;;Private
30 |
31 | (defrecord SocketConn [^Socket socket]
32 | ITCPConn
33 | (-input-stream [_] (.getInputStream socket))
34 | (-output-stream [_] (.getOutputStream socket))
35 | (-close [_] (.close socket))
36 | (-valid? [_] (and
37 | (.isConnected socket)
38 | (not (.isClosed socket)))))
39 |
40 |
41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
42 | ;;;;;;;;;;;;;;;; Public API
43 |
44 | (defn wrap-tcp-conn
45 | "Wrap the Socket in a ITCPConn"
46 | [^Socket socket]
47 | (->SocketConn socket))
48 |
49 | (defn create-tcp-conn [{:keys [host port]}]
50 | {:pre [(string? host) (number? port)]}
51 | (->SocketConn
52 | (doto (Socket.)
53 | (.connect (InetSocketAddress. (str host) (int port)))
54 | (.setKeepAlive true))))
55 |
56 |
57 | (defn
58 | ^InputStream
59 | input-stream [conn]
60 | (-input-stream conn))
61 |
62 | (defn
63 | ^OutputStream
64 | output-stream [conn]
65 | (-output-stream conn))
66 |
67 | (defn close! [conn]
68 | (-close conn))
69 |
70 | (defn valid? [conn]
71 | (-valid? conn))
72 |
73 | (defn ^HostAddress host-address
74 | "Creates a host address instance using host and port"
75 | [host port]
76 | {:pre [(string? host) (number? port)]}
77 | (->HostAddress host port))
78 |
79 | (defn ^KeyedPooledObjectFactory tcp-conn-factory
80 | "Return a keyed pool factory that return ITCPConn instances
81 | The keys used should always be instances of HostAddress or implement host and port keys
82 |
83 | post-create-fn: is called after the connection has been created
84 | pre-destroy-fn is called before the connection is destroyed"
85 | ([]
86 | (tcp-conn-factory identity identity))
87 | ([post-create-fn pre-destroy-fn]
88 | (let [post-create-fn' (if post-create-fn post-create-fn :conn)
89 | pre-destroy-fn' (if pre-destroy-fn pre-destroy-fn :conn)]
90 |
91 | (proxy
92 | [BaseKeyedPooledObjectFactory]
93 | []
94 | (create [address]
95 | (s/validate HostAddressSchema address)
96 |
97 | (let [conn (create-tcp-conn address)]
98 | (post-create-fn' {:address address :conn conn})))
99 |
100 | (wrap [v] (DefaultPooledObject. v))
101 |
102 | (destroyObject [address ^PooledObject v]
103 | (let [conn (.getObject v)]
104 | (pre-destroy-fn' {:address address :conn conn})
105 | (close! conn)))
106 |
107 | (validateObject [_ ^PooledObject v]
108 | (valid? (.getObject v)))))))
--------------------------------------------------------------------------------
/src/clojure/tcp_driver/io/pool.clj:
--------------------------------------------------------------------------------
1 | (ns
2 | ^{:doc "TCP connection pools
3 | see: create-tcp-pool
4 |
5 | Pool keys:
6 | Note keys (not keywords) passed are in fact host addresses of the format {:host