|
1 | | -(ns piotr-yuxuan.closeable-map |
2 | | - ;; Manually keep this description in sync with relevant parts of the README. |
3 | | - "In your project, require: |
4 | | -
|
5 | | -``` clojure |
6 | | -(require '[piotr-yuxuan.closeable-map :as closeable-map :refer [close-with with-tag]]) |
7 | | -``` |
8 | | -
|
9 | | -Define an application that can be started, and closed. |
10 | | -
|
11 | | -``` clojure |
12 | | -(defn start |
13 | | - \"Return a map describing a running application, and which values may |
14 | | - be closed.\" |
15 | | - [config] |
16 | | - (closeable-map/closeable-map |
17 | | - {;; Kafka producers/consumers are `java.io.Closeable`. |
18 | | - :producer (kafka-producer config) |
19 | | - :consumer (kafka-consumer config)})) |
20 | | -``` |
21 | | -
|
22 | | -You can start/stop the app in the repl with: |
23 | | -
|
24 | | -``` clojure |
25 | | -(comment |
26 | | - (def config (load-config)) |
27 | | - (def system (start config)) |
28 | | -
|
29 | | - ;; Stop/close all processes/resources with: |
30 | | - (.close system) |
31 | | - ) |
32 | | -``` |
33 | | -
|
34 | | -It can be used in conjunction with `with-open` in test file to create |
35 | | -well-contained, independent tests: |
36 | | -
|
37 | | -``` clojure |
38 | | -(with-open [{:keys [consumer] :as app} (start config)] |
39 | | - (testing \"unit test with isolated, repeatable context\" |
40 | | - (is (= :yay/🚀 (some-business/function consumer))))) |
41 | | -``` |
42 | | -
|
43 | | -You could also use thi library while live-coding to stop and restart |
44 | | -your application whenever a file is changed. |
45 | | -
|
46 | | -## More details |
47 | | -
|
48 | | -``` clojure |
49 | | -(defn start |
50 | | - \"Return a map describing a running application, and which values may |
51 | | - be closed.\" |
52 | | - [config] |
53 | | - (closeable-map/closeable-map |
54 | | - {;; Kafka producers/consumers are `java.io.Closeable`. |
55 | | - :producer (kafka-producer config) |
56 | | - :consumer (kafka-consumer config) |
57 | | -
|
58 | | - ;; File streams are `java.io.Closeable` too: |
59 | | - :logfile (io/output-stream (io/file \"/tmp/log.txt\")) |
60 | | -
|
61 | | - ;; Closeable maps can be nested. Nested maps will be closed before the outer map. |
62 | | - :backend/api {:response-executor (close-with (memfn ^ExecutorService .shutdown) |
63 | | - (flow/utilization-executor (:executor config))) |
64 | | - :connection-pool (close-with (memfn ^IPool .shutdown) |
65 | | - (http/connection-pool {:pool-opts config})) |
66 | | -
|
67 | | - ;; These functions receive their map as argument. |
68 | | - ::closeable-map/before-close (fn [m] (backend/give-up-leadership config m)) |
69 | | - ::closeable-map/after-close (fn [m] (backend/close-connection config m))} |
70 | | -
|
71 | | - ;; Any exception when closing this nested map will be swallowed |
72 | | - ;; and not bubbled up. |
73 | | - :db ^::closeable-map/swallow {;; Connection are `java.io.Closeable`, too: |
74 | | - :db-conn (jdbc/get-connection (:db config))} |
75 | | -
|
76 | | - ;; Some libs return a zero-argument function which when called |
77 | | - ;; stops the server, like: |
78 | | - :server (with-tag ::closeable-map/fn (http/start-server (api config) (:server config))) |
79 | | - ;; Gotcha: Clojure meta data can only be attached on 'concrete' |
80 | | - ;; objects; they are lost on literal forms (see above). |
81 | | - :forensic ^::closeable-map/fn #(metrics/report-death!) |
82 | | -
|
83 | | - ::closeable-map/ex-handler |
84 | | - (fn [ex] |
85 | | - ;; Will be called for all exceptions thrown when closing this |
86 | | - ;; map and nested items. |
87 | | - (println (ex-message ex)))})) |
88 | | -``` |
89 | | -
|
90 | | -When `(.close system)` is executed, it will: |
91 | | -
|
92 | | - - Recursively close all instances of `java.io.Closeable` and |
93 | | - `java.lang.AutoCloseable`; |
94 | | - - Recursively call all stop zero-argument functions tagged with |
95 | | - `^::closeable-map/fn`; |
96 | | - - Skip all nested `Closeable` under a `^::closeable-map/ignore`; |
97 | | - - Silently swallow any exception with `^::closeable-map/swallow`; |
98 | | - - Exceptions to optional `::closeable-map/ex-handler` in key or |
99 | | - metadata; |
100 | | - - If keys (or metadata) `::closeable-map/before-close` or |
101 | | - `::closeable-map/after-close` are present, they will be assumed as |
102 | | - a function which takes one argument (the map itself) and used run |
103 | | - additional closing logic: |
104 | | -
|
105 | | - ``` clojure |
106 | | - (closeable-map |
107 | | - {;; This function will be executed before the auto close. |
108 | | - ::closeable-map/before-close (fn [this-map] (flush!)) |
109 | | -
|
110 | | - ;; Kafka producers/consumers are java.io.Closeable |
111 | | - :producer (kafka-producer config) |
112 | | - :consumer (kafka-consumer config) |
113 | | -
|
114 | | - ;; This function will be executed after the auto close. |
115 | | - ::closeable-map/after-close (fn [this-map] (garbage/collect!))}) |
116 | | - ``` |
117 | | -
|
118 | | -Some classes do not implement `java.lang.AutoCloseable` but present |
119 | | -some similar method. For example instances of |
120 | | -`java.util.concurrent.ExecutorService` can't be closed but they can be |
121 | | -`.shutdown`: |
122 | | -
|
123 | | -``` clojure |
124 | | -{:response-executor (close-with (memfn ^ExecutorService .shutdown) |
125 | | - (flow/utilization-executor (:executor config))) |
126 | | - :connection-pool (close-with (memfn ^IPool .shutdown) |
127 | | - (http/connection-pool {:pool-opts config}))} |
128 | | -``` |
129 | | -
|
130 | | -You may also extend this library by giving new dispatch values to |
131 | | -multimethod [[piotr-yuxuan.closeable-map/close!]]. Once evaluated, |
132 | | -this will work accross all your code. The multimethod is dispatched on |
133 | | -the concrete class of its argument: |
134 | | -
|
135 | | -``` clojure |
136 | | -(import '(java.util.concurrent ExecutorService)) |
137 | | -(defmethod closeable-map/close! ExecutorService |
138 | | - [x] |
139 | | - (.shutdown ^ExecutorService x)) |
140 | | -
|
141 | | -(import '(io.aleph.dirigiste IPool)) |
142 | | -(defmethod closeable-map/close! IPool |
143 | | - [x] |
144 | | - (.shutdown ^IPool x)) |
145 | | -``` |
146 | | -
|
147 | | -## All or nothing |
148 | | -
|
149 | | -### No half-broken closeable map |
150 | | -
|
151 | | -You may also avoid partially open state when an exception is thrown |
152 | | -when creating a `CloseableMap`. This is where `closeable-map*` comes |
153 | | -handy. It outcome in one of the following: |
154 | | -
|
155 | | -- Either everything went right, and all inner forms wrapped by |
156 | | - `closeable` correctly return a value; you get an open instance of `CloseableMap`. |
157 | | -
|
158 | | -- Either some inner form wrapped by `closeable` didn't return a |
159 | | - closeable object but threw an exception instead. Then all |
160 | | - `closeable` forms are closed, and finally the exception is |
161 | | - bubbled up. |
162 | | -
|
163 | | -``` clojure |
164 | | -(closeable-map* |
165 | | - {:server (closeable* (http/start-server (api config))) |
166 | | - :kafka {:consumer (closeable* (kafka-consumer config)) |
167 | | - :producer (closeable* (kafka-producer config)) |
168 | | - :schema.registry.url \"https://localhost\"}}) |
169 | | -``` |
170 | | -
|
171 | | -### No half-broken state in general code |
172 | | -
|
173 | | -In some circumstances you may need to handle exception on the creation |
174 | | -of a closeable map. If an exception happens during the creation of the |
175 | | -map, values already evaluated will be closed. No closeable objects |
176 | | -will be left open with no references to them. |
177 | | -
|
178 | | -For instance, this form would throw an exception: |
179 | | -
|
180 | | -``` clojure |
181 | | -(closeable-map/closeable-map {:server (http/start-server (api config)) |
182 | | - :kafka {:consumer (kafka-consumer config) |
183 | | - :producer (throw (ex-info \"Exception\" {}))}}) |
184 | | -;; => (ex-info \"Exception\" {}) |
185 | | -``` |
186 | | -
|
187 | | -`with-closeable*` prevents that kind of broken, partially open states for its bindings: |
188 | | -
|
189 | | -``` clojure |
190 | | -(with-closeable* [server (http/start-server (api config)) |
191 | | - consumer (kafka-consumer config) |
192 | | - producer (throw (ex-info \"Exception\" {}))] |
193 | | - ;; Your code goes here. |
194 | | -) |
195 | | -;; Close consumer, |
196 | | -;; close server, |
197 | | -;; finally throw `(ex-info \"Exception\" {})`. |
198 | | -``` |
199 | | -
|
200 | | -You now have the guarantee that your code will only be executed if |
201 | | -all these closeable are open. In the latter example an exception is |
202 | | -thrown when `producer` is evaluated, so `consumer` is closed, then |
203 | | -`server` is closed, and finally the exception is bubbled up. Your |
204 | | -code is not evaluated. In the next example the body is evaluated, |
205 | | -but throws an exception: all bindings are closed. |
206 | | -
|
207 | | -``` clojure |
208 | | -(with-closeable* [server (http/start-server (api config)) |
209 | | - consumer (kafka-consumer config) |
210 | | - producer (kafka-producer config)] |
211 | | - ;; Your code goes here. |
212 | | - (throw (ex-info \"Exception\" {}))) |
213 | | -;; Close producer, |
214 | | -;; close consumer, |
215 | | -;; close server, |
216 | | -;; finally throw `(ex-info \"Exception\" {})`. |
217 | | -``` |
218 | | -
|
219 | | -When no exception is thrown, leave bindings open and return like a |
220 | | -normal `let` form. If you prefer to close bindings, use `with-open` as |
221 | | -usual. |
222 | | - |
223 | | -``` clojure |
224 | | -(with-closeable* [server (http/start-server (api config)) |
225 | | - consumer (kafka-consumer config) |
226 | | - producer (kafka-producer config)] |
227 | | - ;; Your code goes here. |
228 | | - ) |
229 | | -;; All closeable in bindings stay open. |
230 | | -;; => result |
231 | | -```" |
| 1 | +(ns ^{:doc (clojure.core/slurp (clojure.java.io/file "README.md"))} |
| 2 | + piotr-yuxuan.closeable-map |
232 | 3 | (:require [clojure.data] |
233 | 4 | [clojure.walk :as walk] |
234 | 5 | [potemkin :refer [def-map-type]]) |
|
0 commit comments