Skip to content

Commit 6376308

Browse files
committed
2.1.0 type hints and upper limit for pool
1 parent e24237c commit 6376308

File tree

5 files changed

+187
-49
lines changed

5 files changed

+187
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
# 2.1.0
2+
3+
- Type hints
4+
- `:max-conn` and `:max-idle-conn` options for pool
5+
16
# 2.0.0
27

38
- [ BREAKING ] `datascript.storage.sql.core/make` now accepts `javax.sql.DataSource` instead of `java.sql.Connection`
4-
- [ BREAKING ] Removed `datascript.storage.sql.core/close`
59
- Added simple connection pool `datascript.storage.sql.core/pool`
610

711
# 1.0.0

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,26 @@ Currently supported `:dbtype`-s:
7171
- `:postgresql`
7272
- `:sqlite`
7373

74-
If needed, you can close connection through storage:
74+
If your JDBC driver only provides you with `DataSource` and you want to add some basic pooling on top, use `storage-sql/pool`:
75+
76+
```
77+
(def datasource
78+
(doto (SQLiteDataSource.)
79+
(.setUrl "jdbc:sqlite:target/db.sqlite")))
80+
81+
(def pooled-datasource
82+
(storage-sql/pool datasource
83+
{:max-conn 10
84+
:max-idle-conn 4}))
85+
86+
(def storage
87+
(storage-sql/make pooled-datasource
88+
{:dbtype :sqlite}))
89+
```
90+
91+
`pool` takes non-pooled `DataSource` and returns new `DataSource` that pools connections for you.
92+
93+
If you used pool to create storage, you can close it this way:
7594

7695
```
7796
(storage-sql/close storage)

src/datascript/storage/sql/core.clj

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
[datascript.core :as d]
66
[datascript.storage :as storage])
77
(:import
8+
[java.lang AutoCloseable]
9+
[java.lang.reflect InvocationHandler Method Proxy]
810
[java.sql Connection DriverManager ResultSet SQLException Statement]
911
[javax.sql DataSource]
10-
[java.lang.reflect InvocationHandler Method Proxy]))
12+
[java.util.concurrent.locks Condition Lock ReentrantLock]))
1113

1214
(defmacro with-conn [[conn datasource] & body]
1315
(let [conn (vary-meta conn assoc :tag Connection)]
@@ -143,7 +145,25 @@
143145
:binary? (boolean (and (:freeze-bytes opts) (:thaw-bytes opts))))]
144146
(merge {:ddl (ddl opts)} opts)))
145147

146-
(defn make
148+
(defn make
149+
"Create new DataScript storage from javax.sql.DataSource.
150+
151+
Mandatory opts:
152+
153+
:dbtype :: keyword, one of :h2, :mysql, :postgresql or :sqlite
154+
155+
Optional opts:
156+
157+
:batch-size :: int, default 1000
158+
:table :: string, default \"datascript\"
159+
:ddl :: custom DDL to create :table. Must have `addr, int` and `content, text` columns
160+
:freeze-str :: (fn [any]) -> str, serialize DataScript segments, default pr-str
161+
:thaw-str :: (fn [str]) -> any, deserialize DataScript segments, default clojure.edn/read-string
162+
:freeze-bytes :: (fn [any]) -> bytes, same idea as freeze-str, but for binary serialization
163+
:thaw-bytes :: (fn [bytes]) -> any
164+
165+
:freeze-str and :thaw-str, :freeze-bytes and :thaw-bytes should come in pairs, and are mutually exclusive
166+
(it’s either binary or string serialization)"
147167
([datasource]
148168
{:pre [(instance? DataSource datasource)]}
149169
(make datasource {}))
@@ -170,42 +190,60 @@
170190

171191
'datascript.storage/-delete
172192
(fn [_ addr-seq]
173-
(with-open [conn datasource]
193+
(with-conn [conn datasource]
174194
(delete-impl conn opts addr-seq)))}))))
175195

176-
(defn swap-return! [*atom f & args]
177-
(let [*res (volatile! nil)]
178-
(swap! *atom
179-
(fn [atom]
180-
(let [[res atom'] (apply f atom args)]
181-
(vreset! *res res)
182-
atom')))
183-
@*res))
184-
185-
(defrecord Pool [*atom ^DataSource datasource opts]
186-
java.lang.AutoCloseable
196+
(defn close
197+
"If storage was created with DataSource that also implements AutoCloseable,
198+
it will close that DataSource"
199+
[storage]
200+
(let [datasource (:datasource storage)]
201+
(when (instance? AutoCloseable datasource)
202+
(.close ^AutoCloseable datasource))))
203+
204+
(defmacro with-lock [lock & body]
205+
`(let [^Lock lock# ~lock]
206+
(try
207+
(.lock lock#)
208+
~@body
209+
(finally
210+
(.unlock lock#)))))
211+
212+
(defrecord Pool [*atom ^Lock lock ^Condition condition ^DataSource datasource opts]
213+
AutoCloseable
187214
(close [_]
188215
(let [[{:keys [taken free]} _] (swap-vals! *atom #(-> % (update :taken empty) (update :idle empty)))]
189216
(doseq [conn (concat free taken)]
190217
(try
191-
(.close conn)
218+
(.close ^Connection conn)
192219
(catch Exception e
193220
(.printStackTrace e))))))
194221

195222
DataSource
196-
(getConnection [_]
197-
(let [conn (swap-return! *atom
198-
(fn [atom]
199-
(if-some [conn (peek (:idle atom))]
200-
[conn (-> atom
201-
(update :taken conj conn)
202-
(update :idle pop))]
203-
[nil atom])))
204-
conn (or conn
205-
(let [conn (.getConnection datasource)]
206-
(swap! *atom update :taken conj conn)
207-
conn))
208-
conn ^Connection conn
223+
(getConnection [this]
224+
(let [^Connection conn (with-lock lock
225+
(loop []
226+
(let [atom @*atom]
227+
(cond
228+
;; idle connections available
229+
(> (count (:idle atom)) 0)
230+
(let [conn (peek (:idle atom))]
231+
(swap! *atom #(-> %
232+
(update :taken conj conn)
233+
(update :idle pop)))
234+
conn)
235+
236+
;; has space for new connection
237+
(< (count (:taken atom)) (:max-conn opts))
238+
(let [conn (.getConnection datasource)]
239+
(swap! *atom update :taken conj conn)
240+
conn)
241+
242+
;; already at limit
243+
:else
244+
(do
245+
(.await condition)
246+
(recur))))))
209247
*closed? (volatile! false)]
210248
(Proxy/newProxyInstance
211249
(.getClassLoader Connection)
@@ -220,14 +258,18 @@
220258
(.rollback conn)
221259
(.setAutoCommit conn true))
222260
(vreset! *closed? true)
223-
(when-some [conn (swap-return! *atom
224-
(fn [atom]
225-
(if (>= (count (:idle atom)) (:max-conn opts))
226-
[conn (update atom :taken disj conn)]
227-
[nil (-> atom
228-
(update :taken disj conn)
229-
(update :idle conj conn))])))]
230-
(.close conn))
261+
(with-lock lock
262+
(if (< (count (:idle @*atom)) (:max-idle-conn opts))
263+
;; normal return to pool
264+
(do
265+
(swap! *atom #(-> %
266+
(update :taken disj conn)
267+
(update :idle conj conn)))
268+
(.signal condition))
269+
;; excessive idle conn
270+
(do
271+
(swap! *atom update :taken disj conn)
272+
(.close conn))))
231273
nil)
232274

233275
"isClosed"
@@ -237,13 +279,25 @@
237279
(.invoke method conn args)))))))))
238280

239281
(defn pool
240-
([datasource]
241-
(pool datasource {}))
242-
([datasource opts]
243-
(Pool.
244-
(atom {:taken #{}
245-
:idle []})
246-
datasource
247-
(merge
248-
{:max-conn 4}
249-
opts))))
282+
"Simple connection pool.
283+
284+
Accepts javax.sql.DataSource, returns javax.sql.DataSource implementation
285+
that creates java.sql.Connection on demand, up to :max-conn, and keeps up
286+
to :max-idle-conn when no demand.
287+
288+
Implements AutoCloseable, which closes all pooled connections."
289+
(^DataSource [datasource]
290+
(pool datasource {}))
291+
(^DataSource [datasource opts]
292+
{:pre [(instance? DataSource datasource)]}
293+
(let [lock (ReentrantLock.)]
294+
(Pool.
295+
(atom {:taken #{}
296+
:idle []})
297+
lock
298+
(.newCondition lock)
299+
datasource
300+
(merge
301+
{:max-idle-conn 4
302+
:max-conn 10}
303+
opts)))))

test/datascript/storage/sql/test_main.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
[datascript.storage.sql.test-h2]
66
[datascript.storage.sql.test-mysql]
77
[datascript.storage.sql.test-postgresql]
8-
[datascript.storage.sql.test-sqlite]))
8+
[datascript.storage.sql.test-sqlite]
9+
[datascript.storage.sql.test-pool]))
910

1011
(defn -main [& args]
1112
(t/run-all-tests #"datascript\.storage\.sql\..*"))
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
(ns datascript.storage.sql.test-pool
2+
(:require
3+
[clojure.test :as t :refer [deftest is are testing]]
4+
[datascript.core :as d]
5+
[datascript.storage.sql.core :as storage-sql]
6+
[datascript.storage.sql.test-core :as test-core])
7+
(:import
8+
[java.nio.file Files Path]
9+
[javax.sql DataSource]
10+
[java.util.concurrent Executors Future]
11+
[org.sqlite SQLiteDataSource]))
12+
13+
(deftest test-pool []
14+
(Files/deleteIfExists (Path/of "target/db.sqlite" (make-array String 0)))
15+
(with-open [datasource (storage-sql/pool
16+
(doto (SQLiteDataSource.)
17+
(.setUrl "jdbc:sqlite:target/db.sqlite"))
18+
{:max-conn 10
19+
:max-idle-conn 4})
20+
thread-pool (Executors/newFixedThreadPool 100)]
21+
(let [*stats (atom {:min-idle Long/MAX_VALUE
22+
:max-idle 0
23+
:min-taken Long/MAX_VALUE
24+
:max-taken 0})
25+
_ (add-watch (:*atom datasource) ::stats
26+
(fn [_ _ _ new]
27+
(swap! *stats
28+
#(-> %
29+
(update :min-idle min (count (:idle new)))
30+
(update :max-idle max (count (:idle new)))
31+
(update :min-taken min (count (:taken new)))
32+
(update :max-taken max (count (:taken new)))))))
33+
_ (with-open [conn (.getConnection datasource)]
34+
(with-open [stmt (.createStatement conn)]
35+
(.execute stmt "create table T (id INTEGER primary key)"))
36+
(with-open [stmt (.prepareStatement conn "insert into T (id) values (?)")]
37+
(dotimes [i 1000]
38+
(.setLong stmt 1 i)
39+
(.addBatch stmt))
40+
(.executeBatch stmt)))
41+
select (fn [i]
42+
(with-open [conn (.getConnection datasource)
43+
stmt (doto (.prepareStatement conn "select id from T where id = ?")
44+
(.setLong 1 i))
45+
rs (.executeQuery stmt)]
46+
(.next rs)
47+
(.getLong rs 1)))
48+
tasks (mapv #(fn [] (select %)) (range 1000))
49+
futures (.invokeAll thread-pool tasks)]
50+
(is (= (range 1000) (map #(.get ^Future %) futures)))
51+
(is (= 4 (count (:idle @(:*atom datasource)))))
52+
(is (= 0 (count (:taken @(:*atom datasource)))))
53+
(is (= 0 (:min-idle @*stats)))
54+
(is (= 4 (:max-idle @*stats)))
55+
(is (= 0 (:min-taken @*stats)))
56+
(is (= 10 (:max-taken @*stats))))))
57+
58+
(comment
59+
(t/run-test-var #'test-sqlite)
60+
(t/run-tests *ns*))

0 commit comments

Comments
 (0)