1 /++
2 Connect to a MySQL/MariaDB database using a connection pool.
3 
4 This provides various benefits over creating a new connection manually,
5 such as automatically reusing old connections, and automatic cleanup (no need to close
6 the connection when done).
7 
8 Internally, this is based on vibe.d's
9 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool).
10 You have to include vibe.d in your project to be able to use this class.
11 If you don't want to, refer to `mysql.connection.Connection`.
12 +/
13 module mysql.pool;
14 
15 import std.conv;
16 import std.typecons;
17 import mysql.connection;
18 import mysql.prepared;
19 import mysql.protocol.constants;
20 
21 version(Have_vibe_core)
22 {
23 	version = IncludeMySQLPool;
24 	static if(is(typeof(ConnectionPool!Connection.init.removeUnused((c){}))))
25 		version = HaveCleanupFunction;
26 }
27 version(MySQLDocs)
28 {
29 	version = IncludeMySQLPool;
30 }
31 
32 version(IncludeMySQLPool)
33 {
34 	version(Have_vibe_core)
35 		import vibe.core.connectionpool;
36 	else version(MySQLDocs)
37 	{
38 		/++
39 		Vibe.d's
40 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool)
41 		class.
42 
43 		Not actually included in module `mysql.pool`. Only listed here for
44 		documentation purposes. For ConnectionPool and it's documentation, see:
45 		$(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool)
46 		+/
47 		class ConnectionPool(T)
48 		{
49 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this)
50 			this(Connection delegate() connection_factory, uint max_concurrent = (uint).max)
51 			{}
52 
53 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection)
54 			LockedConnection!T lockConnection() { return LockedConnection!T(); }
55 
56 			/// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency)
57 			uint maxConcurrency;
58 
59 			/// See: $(LINK https://github.com/vibe-d/vibe-core/blob/24a83434e4c788ebb9859dfaecbe60ad0f6e9983/source/vibe/core/connectionpool.d#L113)
60 			void removeUnused(scope void delegate(Connection conn) @safe nothrow disconnect_callback)
61 			{}
62 		}
63 
64 		/++
65 		Vibe.d's
66 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection)
67 		struct.
68 
69 		Not actually included in module `mysql.pool`. Only listed here for
70 		documentation purposes. For LockedConnection and it's documentation, see:
71 		$(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection)
72 		+/
73 		struct LockedConnection(Connection) { Connection c; alias c this; }
74 	}
75 
76 	/++
77 	Connect to a MySQL/MariaDB database using a connection pool.
78 
79 	This provides various benefits over creating a new connection manually,
80 	such as automatically reusing old connections, and automatic cleanup (no need to close
81 	the connection when done).
82 
83 	Internally, this is based on vibe.d's
84 	$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool).
85 	You have to include vibe.d in your project to be able to use this class.
86 	If you don't want to, refer to `mysql.connection.Connection`.
87 	+/
88 	class MySQLPool
89 	{
90 		private
91 		{
92 			string m_host;
93 			string m_user;
94 			string m_password;
95 			string m_database;
96 			ushort m_port;
97 			SvrCapFlags m_capFlags;
98 			void delegate(Connection) m_onNewConnection;
99 			ConnectionPool!Connection m_pool;
100 			PreparedRegistrations!PreparedInfo preparedRegistrations;
101 
102 			struct PreparedInfo
103 			{
104 				bool queuedForRelease = false;
105 			}
106 
107 		}
108 
109 		/// Sets up a connection pool with the provided connection settings.
110 		///
111 		/// The optional `onNewConnection` param allows you to set a callback
112 		/// which will be run every time a new connection is created.
113 		this(string host, string user, string password, string database,
114 			ushort port = 3306, uint maxConcurrent = (uint).max,
115 			SvrCapFlags capFlags = defaultClientFlags,
116 			void delegate(Connection) onNewConnection = null)
117 		{
118 			m_host = host;
119 			m_user = user;
120 			m_password = password;
121 			m_database = database;
122 			m_port = port;
123 			m_capFlags = capFlags;
124 			m_onNewConnection = onNewConnection;
125 			m_pool = new ConnectionPool!Connection(&createConnection);
126 		}
127 
128 		///ditto
129 		this(string host, string user, string password, string database,
130 			ushort port, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null)
131 		{
132 			this(host, user, password, database, port, (uint).max, capFlags, onNewConnection);
133 		}
134 
135 		///ditto
136 		this(string host, string user, string password, string database,
137 			ushort port, void delegate(Connection) onNewConnection)
138 		{
139 			this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection);
140 		}
141 
142 		///ditto
143 		this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags,
144 			void delegate(Connection) onNewConnection = null)
145 		{
146 			auto parts = Connection.parseConnectionString(connStr);
147 			this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection);
148 		}
149 
150 		///ditto
151 		this(string connStr, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null)
152 		{
153 			this(connStr, (uint).max, capFlags, onNewConnection);
154 		}
155 
156 		///ditto
157 		this(string connStr, void delegate(Connection) onNewConnection)
158 		{
159 			this(connStr, (uint).max, defaultClientFlags, onNewConnection);
160 		}
161 
162 		/++
163 		Obtain a connection. If one isn't available, a new one will be created.
164 
165 		The connection returned is actually a `LockedConnection!Connection`,
166 		but it uses `alias this`, and so can be used just like a Connection.
167 		(See vibe.d's
168 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).)
169 
170 		No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it.
171 
172 		There is no need to close, release or unlock this connection. It is
173 		reference-counted and will automatically be returned to the pool once
174 		your fiber is done with it.
175 
176 		If you have passed any prepared statements to  `autoRegister`
177 		or `autoRelease`, then those statements will automatically be
178 		registered/released on the connection. (Currently, this automatic
179 		register/release may actually occur upon the first command sent via
180 		the connection.)
181 		+/
182 		LockedConnection!Connection lockConnection()
183 		{
184 			auto conn = m_pool.lockConnection();
185 			if(conn.closed)
186 				conn.reconnect();
187 
188 			applyAuto(conn);
189 			return conn;
190 		}
191 
192 		/// Applies any `autoRegister`/`autoRelease` settings to a connection,
193 		/// if necessary.
194 		package void applyAuto(T)(T conn)
195 		{
196 			foreach(sql, info; preparedRegistrations.directLookup)
197 			{
198 				auto registeredOnPool = !info.queuedForRelease;
199 				auto registeredOnConnection = conn.isRegistered(sql);
200 
201 				if(registeredOnPool && !registeredOnConnection) // Need to register?
202 					conn.register(sql);
203 				else if(!registeredOnPool && registeredOnConnection) // Need to release?
204 					conn.release(sql);
205 			}
206 		}
207 
208 		private Connection createConnection()
209 		{
210 			auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags);
211 
212 			if(m_onNewConnection)
213 				m_onNewConnection(conn);
214 
215 			return conn;
216 		}
217 
218 		/// Get/set a callback delegate to be run every time a new connection
219 		/// is created.
220 		@property void onNewConnection(void delegate(Connection) onNewConnection)
221 		{
222 			m_onNewConnection = onNewConnection;
223 		}
224 
225 		///ditto
226 		@property void delegate(Connection) onNewConnection()
227 		{
228 			return m_onNewConnection;
229 		}
230 
231 		/++
232 		Forwards to vibe.d's
233 		$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency)
234 		+/
235 		@property uint maxConcurrency()
236 		{
237 			return m_pool.maxConcurrency;
238 		}
239 
240 		///ditto
241 		@property void maxConcurrency(uint maxConcurrent)
242 		{
243 			m_pool.maxConcurrency = maxConcurrent;
244 		}
245 
246 		/++
247 		Set a prepared statement to be automatically registered on all
248 		connections received from this pool.
249 
250 		This also clears any `autoRelease` which may have been set for this statement.
251 
252 		Calling this is not strictly necessary, as a prepared statement will
253 		automatically be registered upon its first use on any `Connection`.
254 		This is provided for those who prefer eager registration over lazy
255 		for performance reasons.
256 
257 		Once this has been called, obtaining a connection via `lockConnection`
258 		will automatically register the prepared statement on the connection
259 		if it isn't already registered on the connection. This single
260 		registration safely persists after the connection is reclaimed by the
261 		pool and locked again by another Vibe.d task.
262 
263 		Note, due to the way Vibe.d works, it is not possible to eagerly
264 		register or release a statement on all connections already sitting
265 		in the pool. This can only be done when locking a connection.
266 
267 		You can stop the pool from continuing to auto-register the statement
268 		by calling either `autoRelease` or `clearAuto`.
269 		+/
270 		void autoRegister(Prepared prepared)
271 		{
272 			autoRegister(prepared.sql);
273 		}
274 
275 		///ditto
276 		void autoRegister(const(char[]) sql)
277 		{
278 			preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo());
279 		}
280 
281 		/++
282 		Set a prepared statement to be automatically released from all
283 		connections received from this pool.
284 
285 		This also clears any `autoRegister` which may have been set for this statement.
286 
287 		Calling this is not strictly necessary. The server considers prepared
288 		statements to be per-connection, so they'll go away when the connection
289 		closes anyway. This is provided in case direct control is actually needed.
290 
291 		Once this has been called, obtaining a connection via `lockConnection`
292 		will automatically release the prepared statement from the connection
293 		if it isn't already releases from the connection.
294 
295 		Note, due to the way Vibe.d works, it is not possible to eagerly
296 		register or release a statement on all connections already sitting
297 		in the pool. This can only be done when locking a connection.
298 
299 		You can stop the pool from continuing to auto-release the statement
300 		by calling either `autoRegister` or `clearAuto`.
301 		+/
302 		void autoRelease(Prepared prepared)
303 		{
304 			autoRelease(prepared.sql);
305 		}
306 
307 		///ditto
308 		void autoRelease(const(char[]) sql)
309 		{
310 			preparedRegistrations.queueForRelease(sql);
311 		}
312 
313 		/// Is the given statement set to be automatically registered on all
314 		/// connections obtained from this connection pool?
315 		bool isAutoRegistered(Prepared prepared)
316 		{
317 			return isAutoRegistered(prepared.sql);
318 		}
319 		///ditto
320 		bool isAutoRegistered(const(char[]) sql)
321 		{
322 			return isAutoRegistered(preparedRegistrations[sql]);
323 		}
324 		///ditto
325 		package bool isAutoRegistered(Nullable!PreparedInfo info)
326 		{
327 			return info.isNull || !info.get.queuedForRelease;
328 		}
329 
330 		/// Is the given statement set to be automatically released on all
331 		/// connections obtained from this connection pool?
332 		bool isAutoReleased(Prepared prepared)
333 		{
334 			return isAutoReleased(prepared.sql);
335 		}
336 		///ditto
337 		bool isAutoReleased(const(char[]) sql)
338 		{
339 			return isAutoReleased(preparedRegistrations[sql]);
340 		}
341 		///ditto
342 		package bool isAutoReleased(Nullable!PreparedInfo info)
343 		{
344 			return info.isNull || info.get.queuedForRelease;
345 		}
346 
347 		/++
348 		Is the given statement set for NEITHER auto-register
349 		NOR auto-release on connections obtained from
350 		this connection pool?
351 
352 		Equivalent to `!isAutoRegistered && !isAutoReleased`.
353 		+/
354 		bool isAutoCleared(Prepared prepared)
355 		{
356 			return isAutoCleared(prepared.sql);
357 		}
358 		///ditto
359 		bool isAutoCleared(const(char[]) sql)
360 		{
361 			return isAutoCleared(preparedRegistrations[sql]);
362 		}
363 		///ditto
364 		package bool isAutoCleared(Nullable!PreparedInfo info)
365 		{
366 			return info.isNull;
367 		}
368 
369 		/++
370 		Removes any `autoRegister` or `autoRelease` which may have been set
371 		for this prepared statement.
372 
373 		Does nothing if the statement has not been set for auto-register or auto-release.
374 
375 		This releases any relevent memory for potential garbage collection.
376 		+/
377 		void clearAuto(Prepared prepared)
378 		{
379 			return clearAuto(prepared.sql);
380 		}
381 		///ditto
382 		void clearAuto(const(char[]) sql)
383 		{
384 			preparedRegistrations.directLookup.remove(sql);
385 		}
386 
387 		/++
388 		Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set.
389 
390 		This releases all relevent memory for potential garbage collection.
391 		+/
392 		void clearAllRegistrations()
393 		{
394 			preparedRegistrations.clear();
395 		}
396 
397 		version(MySQLDocs)
398 		{
399 			/++
400 			Removes all unused connections from the pool. This can
401 			be used to clean up before exiting the program to
402 			ensure the event core driver can be properly shut down.
403 
404 			Note: this is only available if vibe-core 1.7.0 or later is being
405 			used.
406 			+/
407 			void removeUnusedConnections() @safe {}
408 		}
409 		else version(HaveCleanupFunction)
410 		{
411 			void removeUnusedConnections() @safe
412 			{
413 				// Note: we squelch all exceptions here, because vibe-core
414 				// requires the function be nothrow, and because an exception
415 				// thrown while closing is probably not important enough to
416 				// interrupt cleanup.
417 				m_pool.removeUnused((conn) @trusted nothrow {
418 					try {
419 						conn.close();
420 					} catch(Exception) {}
421 				});
422 			}
423 		}
424 	}
425 }