Source file sqlgg_stmt_cache.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
module Async_lru = struct
module type Evict = sig
type 'a future
type key
type value
val on_evict : value -> unit future
end
module Make (IO : Sqlgg_io.M_control) (Evict : Evict with type 'a future = 'a IO.future) = struct
open IO
type ('k, 'v) node = {
mutable prev: ('k, 'v) node option;
mutable next: ('k, 'v) node option;
key: 'k;
value: 'v;
}
type ('k, 'v) t = {
mutable head: ('k, 'v) node option;
mutable tail: ('k, 'v) node option;
hash: ('k, ('k, 'v) node) Hashtbl.t;
capacity: int;
mutable size: int;
}
let create capacity = {
head = None;
tail = None;
hash = Hashtbl.create capacity;
capacity;
size = 0;
}
let remove_node t node =
match node.prev with
| None -> t.head <- node.next
| Some prev -> prev.next <- node.next;
match node.next with
| None -> t.tail <- node.prev
| Some next -> next.prev <- node.prev;
node.prev <- None;
node.next <- None
let add_to_head t node =
match t.head with
| None ->
t.head <- Some node;
t.tail <- Some node;
node.prev <- None;
node.next <- None
| Some head ->
head.prev <- Some node;
node.next <- Some head;
node.prev <- None;
t.head <- Some node
let move_to_head t node =
remove_node t node;
add_to_head t node
let remove_tail t =
Option.map (fun tail ->
remove_node t tail;
tail
) t.tail
let add key value t =
begin match Hashtbl.find_opt t.hash key with
| Some existing_node ->
remove_node t existing_node;
Hashtbl.remove t.hash key;
t.size <- t.size - 1;
Evict.on_evict existing_node.value >>= fun () ->
IO.return ()
| None -> IO.return ()
end >>= fun () ->
let new_node = { prev = None; next = None; key; value } in
begin if t.size >= t.capacity then (
match remove_tail t with
| Some evicted_node ->
Hashtbl.remove t.hash evicted_node.key;
t.size <- t.size - 1;
Evict.on_evict evicted_node.value
| None -> IO.return ()
) else IO.return ()
end >>= fun () ->
add_to_head t new_node;
Hashtbl.replace t.hash key new_node;
t.size <- t.size + 1;
IO.return ()
let find key t =
key |> Hashtbl.find_opt t.hash |> Option.map (fun node ->
move_to_head t node;
node.value
)
let remove key t =
match Hashtbl.find_opt t.hash key with
| Some node ->
remove_node t node;
Hashtbl.remove t.hash key;
t.size <- t.size - 1;
Evict.on_evict node.value >>= fun () ->
IO.return true
| None -> IO.return false
let size t = t.size
let capacity t = t.capacity
let is_empty t = t.size = 0
let clear t =
let all_values = Hashtbl.fold (fun _ node acc -> node.value :: acc) t.hash [] in
Hashtbl.clear t.hash;
t.head <- None;
t.tail <- None;
t.size <- 0;
List.iter_s Evict.on_evict all_values
end
end
module type Cached_m = sig
include Sqlgg_traits.M_control_io
val prepare : 'a connection -> string -> statement IO.future
val close_stmt : statement -> unit IO.future
val select_with_stmt :
statement -> (statement -> result IO.future) -> (row -> unit) -> unit IO.future
val select_one_with_stmt :
statement -> (statement -> result IO.future) -> (row -> 'a) -> 'a IO.future
val select_one_maybe_with_stmt :
statement -> (statement -> result IO.future) -> (row -> 'a) -> 'a option IO.future
val execute_with_stmt :
statement -> (statement -> result IO.future) -> execute_response IO.future
end
module type Cache_config = sig
val max_cache_size : int
val ttl_seconds : float option
end
module type Clock = sig
val now_s : unit -> float
end
module Make_with_clock (Clock : Clock) (Config : Cache_config) (Impl : Cached_m) = struct
include Impl
type cache_entry = {
stmt : Impl.statement;
mutable last_use_s : float;
}
module Evict = struct
type 'a future = 'a Impl.IO.future
type key = string
type value = cache_entry
let on_evict (entry : cache_entry) =
let open Impl.IO in
catch
(fun () -> Impl.close_stmt entry.stmt)
(fun _ -> return ())
end
module LRU = Async_lru.Make(Impl.IO)(Evict)
type cache_state = {
mutable cache : (string, cache_entry) LRU.t;
mutable ops_count : int;
}
type -'a connection = {
original : 'a Impl.connection;
mutable state : cache_state;
}
let now_s = Clock.now_s
let is_expired entry =
match Config.ttl_seconds with
| None -> false
| Some ttl -> (now_s () -. entry.last_use_s) >= ttl
let create_cached_connection original =
if Config.max_cache_size <= 0 then
invalid_arg "max_cache_size must be positive";
let cache = LRU.create Config.max_cache_size in
let state = { cache; ops_count = 0 } in
{ original; state }
let with_prepared_stmt cached_conn sql f =
let open Impl.IO in
let state = cached_conn.state in
state.ops_count <- succ state.ops_count;
let touch entry = entry.last_use_s <- now_s () in
let with_stmt_cleanup ?(on_success=(fun () -> return ())) stmt =
try_bind
(fun () -> f stmt)
(fun result -> on_success () >>= fun () -> return result)
(fun exn ->
LRU.remove sql state.cache >>= fun _ ->
raise exn)
in
begin match LRU.find sql state.cache with
| Some entry when not (is_expired entry) ->
touch entry;
with_stmt_cleanup entry.stmt
| Some entry ->
with_stmt_cleanup
~on_success:(fun () ->
LRU.remove sql state.cache >>= fun _ -> return ())
entry.stmt
| None ->
Impl.prepare cached_conn.original sql >>= fun stmt ->
let entry = { stmt; last_use_s = now_s () } in
LRU.add sql entry state.cache >>= fun () ->
with_stmt_cleanup stmt
end
let select cached_conn sql set_params callback =
with_prepared_stmt cached_conn sql (fun stmt ->
Impl.select_with_stmt stmt set_params callback)
let select_one cached_conn sql set_params convert =
with_prepared_stmt cached_conn sql (fun stmt ->
Impl.select_one_with_stmt stmt set_params convert)
let select_one_maybe cached_conn sql set_params convert =
with_prepared_stmt cached_conn sql (fun stmt ->
Impl.select_one_maybe_with_stmt stmt set_params convert)
let execute cached_conn sql set_params =
with_prepared_stmt cached_conn sql (fun stmt ->
Impl.execute_with_stmt stmt set_params)
let cache_stats cached_conn =
let state = cached_conn.state in
Printf.sprintf "Cache: %d/%d items, %d ops since start"
(LRU.size state.cache)
(LRU.capacity state.cache)
state.ops_count
let reset_metrics cached_conn =
let state = cached_conn.state in
state.ops_count <- 0
let clear_cache cached_conn =
let state = cached_conn.state in
LRU.clear state.cache
let close_cached_connection ?(close_underlying=(fun _ -> Impl.IO.return ())) cached_conn =
let open Impl.IO in
LRU.clear cached_conn.state.cache >>= fun () ->
close_underlying cached_conn.original
end
module Make (Config : Cache_config) (Impl : Cached_m) =
Make_with_clock(struct let now_s = Unix.gettimeofday end)(Config)(Impl)
module type Mutex_intf = sig
type 'a future
type t
val create : unit -> t
val with_lock : t -> (unit -> 'a future) -> 'a future
end
module SharedConnectionCache (MutexImpl : Mutex_intf) (Config : Cache_config)
(Impl : Cached_m with type 'a IO.future = 'a MutexImpl.future) = struct
module Stmt_cache = Make(Config)(Impl)
include Stmt_cache
type nonrec 'a connection = {
base_conn : 'a connection;
mutex : MutexImpl.t;
}
let create_cached_connection original =
{
base_conn = create_cached_connection original;
mutex = MutexImpl.create ();
}
let select conn sql set_params callback =
MutexImpl.with_lock conn.mutex (fun () ->
select conn.base_conn sql set_params callback)
let select_one conn sql set_params convert =
MutexImpl.with_lock conn.mutex (fun () ->
select_one conn.base_conn sql set_params convert)
let select_one_maybe conn sql set_params convert =
MutexImpl.with_lock conn.mutex (fun () ->
select_one_maybe conn.base_conn sql set_params convert)
let execute conn sql set_params =
MutexImpl.with_lock conn.mutex (fun () ->
execute conn.base_conn sql set_params)
let cache_stats conn = cache_stats conn.base_conn
let clear_cache conn =
MutexImpl.with_lock conn.mutex (fun () ->
clear_cache conn.base_conn)
let close_cached_connection ?close_underlying conn =
MutexImpl.with_lock conn.mutex (fun () ->
close_cached_connection ?close_underlying conn.base_conn)
end