1 /++ 2 Connect to a MySQL/MariaDB database using vibe.d's 3 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). 4 5 You have to include vibe.d in your project to be able to use this class. 6 If you don't want to, refer to `mysql.connection.Connection`. 7 8 This provides various benefits over creating a new connection manually, 9 such as automatically reusing old connections, and automatic cleanup (no need to close 10 the connection when done). 11 +/ 12 module mysql.pool; 13 14 import std.conv; 15 import std.typecons; 16 import mysql.connection; 17 import mysql.prepared; 18 import mysql.protocol.constants; 19 debug(MYSQLN_TESTS) 20 { 21 import mysql.test.common; 22 } 23 24 version(Have_vibe_d_core) version = IncludeMySQLPool; 25 version(MySQLDocs) version = IncludeMySQLPool; 26 27 version(IncludeMySQLPool) 28 { 29 version(Have_vibe_d_core) 30 import vibe.core.connectionpool; 31 else version(MySQLDocs) 32 { 33 /++ 34 Vibe.d's 35 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) 36 class. 37 38 Not actually included in module `mysql.pool`. Only listed here for 39 documentation purposes. For ConnectionPool and it's documentation, see: 40 $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool) 41 +/ 42 class ConnectionPool(T) 43 { 44 /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this) 45 this(Connection delegate() connection_factory, uint max_concurrent = (uint).max) 46 {} 47 48 /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection) 49 LockedConnection!T lockConnection() { return LockedConnection!T(); } 50 51 /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency) 52 uint maxConcurrency; 53 } 54 55 /++ 56 Vibe.d's 57 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection) 58 struct. 59 60 Not actually included in module `mysql.pool`. Only listed here for 61 documentation purposes. For LockedConnection and it's documentation, see: 62 $(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection) 63 +/ 64 struct LockedConnection(Connection) { Connection c; alias c this; } 65 } 66 67 /++ 68 A lightweight convenience interface to a MySQL/MariaDB database using vibe.d's 69 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). 70 71 You have to include vibe.d in your project to be able to use this class. 72 If you don't want to, refer to `mysql.connection.Connection`. 73 74 If, for any reason, this class doesn't suit your needs, it's easy to just 75 use vibe.d's $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) 76 directly. Simply provide it with a delegate that creates a new `mysql.connection.Connection` 77 and does any other custom processing if needed. 78 +/ 79 class MySQLPool 80 { 81 private 82 { 83 string m_host; 84 string m_user; 85 string m_password; 86 string m_database; 87 ushort m_port; 88 SvrCapFlags m_capFlags; 89 void delegate(Connection) m_onNewConnection; 90 ConnectionPool!Connection m_pool; 91 PreparedRegistrations!PreparedInfo preparedRegistrations; 92 93 struct PreparedInfo 94 { 95 bool queuedForRelease = false; 96 } 97 } 98 99 /// Sets up a connection pool with the provided connection settings. 100 /// 101 /// The optional `onNewConnection` param allows you to set a callback 102 /// which will be run every time a new connection is created. 103 this(string host, string user, string password, string database, 104 ushort port = 3306, uint maxConcurrent = (uint).max, 105 SvrCapFlags capFlags = defaultClientFlags, 106 void delegate(Connection) onNewConnection = null) 107 { 108 m_host = host; 109 m_user = user; 110 m_password = password; 111 m_database = database; 112 m_port = port; 113 m_capFlags = capFlags; 114 m_onNewConnection = onNewConnection; 115 m_pool = new ConnectionPool!Connection(&createConnection); 116 } 117 118 ///ditto 119 this(string host, string user, string password, string database, 120 ushort port, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) 121 { 122 this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); 123 } 124 125 ///ditto 126 this(string host, string user, string password, string database, 127 ushort port, void delegate(Connection) onNewConnection) 128 { 129 this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); 130 } 131 132 ///ditto 133 this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, 134 void delegate(Connection) onNewConnection = null) 135 { 136 auto parts = Connection.parseConnectionString(connStr); 137 this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); 138 } 139 140 ///ditto 141 this(string connStr, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) 142 { 143 this(connStr, (uint).max, capFlags, onNewConnection); 144 } 145 146 ///ditto 147 this(string connStr, void delegate(Connection) onNewConnection) 148 { 149 this(connStr, (uint).max, defaultClientFlags, onNewConnection); 150 } 151 152 /++ 153 Obtain a connection. If one isn't available, a new one will be created. 154 155 The connection returned is actually a `LockedConnection!Connection`, 156 but it uses `alias this`, and so can be used just like a Connection. 157 (See vibe.d's 158 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).) 159 160 No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it. 161 162 There is no need to close, release or unlock this connection. It is 163 reference-counted and will automatically be returned to the pool once 164 your fiber is done with it. 165 166 If you have passed any prepared statements to `autoRegister` 167 or `autoRelease`, then those statements will automatically be 168 registered/released on the connection. (Currently, this automatic 169 register/release may actually occur upon the first command sent via 170 the connection.) 171 +/ 172 auto lockConnection() 173 { 174 Connection conn = m_pool.lockConnection(); 175 applyAuto(conn); 176 return conn; 177 } 178 179 /// Applies any `autoRegister`/`autoRelease` settings to a connection, 180 /// if necessary. 181 private void applyAuto(T)(T conn) 182 { 183 foreach(sql, info; preparedRegistrations.directLookup) 184 { 185 auto registeredOnPool = !info.queuedForRelease; 186 auto registeredOnConnection = conn.isRegistered(sql); 187 188 if(registeredOnPool && !registeredOnConnection) // Need to register? 189 conn.register(sql); 190 else if(!registeredOnPool && registeredOnConnection) // Need to release? 191 conn.release(sql); 192 } 193 } 194 195 private Connection createConnection() 196 { 197 auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); 198 199 if(m_onNewConnection) 200 m_onNewConnection(conn); 201 202 return conn; 203 } 204 205 /// Get/set a callback delegate to be run every time a new connection 206 /// is created. 207 @property void onNewConnection(void delegate(Connection) onNewConnection) 208 { 209 m_onNewConnection = onNewConnection; 210 } 211 212 ///ditto 213 @property void delegate(Connection) onNewConnection() 214 { 215 return m_onNewConnection; 216 } 217 218 @("onNewConnection") 219 debug(MYSQLN_TESTS) 220 unittest 221 { 222 auto count = 0; 223 void callback(Connection conn) 224 { 225 count++; 226 } 227 228 // Test getting/setting 229 auto poolA = new MySQLPool(testConnectionStr, &callback); 230 auto poolB = new MySQLPool(testConnectionStr); 231 auto poolNoCallback = new MySQLPool(testConnectionStr); 232 233 assert(poolA.onNewConnection == &callback); 234 assert(poolB.onNewConnection is null); 235 assert(poolNoCallback.onNewConnection is null); 236 237 poolB.onNewConnection = &callback; 238 assert(poolB.onNewConnection == &callback); 239 assert(count == 0); 240 241 // Ensure callback is called 242 { 243 auto connA = poolA.lockConnection(); 244 assert(!connA.closed); 245 assert(count == 1); 246 247 auto connB = poolB.lockConnection(); 248 assert(!connB.closed); 249 assert(count == 2); 250 } 251 252 // Ensure works with no callback 253 { 254 auto oldCount = count; 255 auto poolC = new MySQLPool(testConnectionStr); 256 auto connC = poolC.lockConnection(); 257 assert(!connC.closed); 258 assert(count == oldCount); 259 } 260 } 261 262 /++ 263 Forwards to vibe.d's 264 $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) 265 +/ 266 @property uint maxConcurrency() 267 { 268 return m_pool.maxConcurrency; 269 } 270 271 ///ditto 272 @property void maxConcurrency(uint maxConcurrent) 273 { 274 m_pool.maxConcurrency = maxConcurrent; 275 } 276 277 /++ 278 Set a prepared statement to be automatically registered on all 279 connections received from this pool. 280 281 This also clears any `autoRelease` which may have been set for this statement. 282 283 Calling this is not strictly necessary, as a prepared statement will 284 automatically be registered upon its first use on any `Connection`. 285 This is provided for those who prefer eager registration over lazy 286 for performance reasons. 287 288 Once this has been called, obtaining a connection via `lockConnection` 289 will automatically register the prepared statement on the connection 290 if it isn't already registered on the connection. This single 291 registration safely persists after the connection is reclaimed by the 292 pool and locked again by another Vibe.d task. 293 294 Note, due to the way Vibe.d works, it is not possible to eagerly 295 register or release a statement on all connections already sitting 296 in the pool. This can only be done when locking a connection. 297 298 You can stop the pool from continuing to auto-register the statement 299 by calling either `autoRelease` or `clearAuto`. 300 +/ 301 void autoRegister(Prepared prepared) 302 { 303 autoRegister(prepared.sql); 304 } 305 306 ///ditto 307 void autoRegister(string sql) 308 { 309 preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo()); 310 } 311 312 /++ 313 Set a prepared statement to be automatically released from all 314 connections received from this pool. 315 316 This also clears any `autoRegister` which may have been set for this statement. 317 318 Calling this is not strictly necessary. The server considers prepared 319 statements to be per-connection, so they'll go away when the connection 320 closes anyway. This is provided in case direct control is actually needed. 321 322 Once this has been called, obtaining a connection via `lockConnection` 323 will automatically release the prepared statement from the connection 324 if it isn't already releases from the connection. 325 326 Note, due to the way Vibe.d works, it is not possible to eagerly 327 register or release a statement on all connections already sitting 328 in the pool. This can only be done when locking a connection. 329 330 You can stop the pool from continuing to auto-release the statement 331 by calling either `autoRegister` or `clearAuto`. 332 +/ 333 void autoRelease(Prepared prepared) 334 { 335 autoRelease(prepared.sql); 336 } 337 338 ///ditto 339 void autoRelease(string sql) 340 { 341 preparedRegistrations.queueForRelease(sql); 342 } 343 344 /// Is the given statement set to be automatically registered on all 345 /// connections obtained from this connection pool? 346 bool isAutoRegistered(Prepared prepared) 347 { 348 return isAutoRegistered(prepared.sql); 349 } 350 ///ditto 351 bool isAutoRegistered(string sql) 352 { 353 return isAutoRegistered(preparedRegistrations[sql]); 354 } 355 ///ditto 356 package bool isAutoRegistered(Nullable!PreparedInfo info) 357 { 358 return info.isNull || !info.queuedForRelease; 359 } 360 361 /// Is the given statement set to be automatically released on all 362 /// connections obtained from this connection pool? 363 bool isAutoReleased(Prepared prepared) 364 { 365 return isAutoReleased(prepared.sql); 366 } 367 ///ditto 368 bool isAutoReleased(string sql) 369 { 370 return isAutoReleased(preparedRegistrations[sql]); 371 } 372 ///ditto 373 package bool isAutoReleased(Nullable!PreparedInfo info) 374 { 375 return info.isNull || info.queuedForRelease; 376 } 377 378 /++ 379 Is the given statement set for NEITHER auto-register 380 NOR auto-release on connections obtained from 381 this connection pool? 382 383 Equivalent to `!isAutoRegistered && !isAutoReleased`. 384 +/ 385 bool isAutoCleared(Prepared prepared) 386 { 387 return isAutoCleared(prepared.sql); 388 } 389 ///ditto 390 bool isAutoCleared(string sql) 391 { 392 return isAutoCleared(preparedRegistrations[sql]); 393 } 394 ///ditto 395 package bool isAutoCleared(Nullable!PreparedInfo info) 396 { 397 return info.isNull; 398 } 399 400 /++ 401 Removes any `autoRegister` or `autoRelease` which may have been set 402 for this prepared statement. 403 404 Does nothing if the statement has not been set for auto-register or auto-release. 405 406 This releases any relevent memory for potential garbage collection. 407 +/ 408 void clearAuto(Prepared prepared) 409 { 410 return clearAuto(prepared.sql); 411 } 412 ///ditto 413 void clearAuto(string sql) 414 { 415 preparedRegistrations.directLookup.remove(sql); 416 } 417 418 /++ 419 Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set. 420 421 This releases all relevent memory for potential garbage collection. 422 +/ 423 void clearAllRegistrations() 424 { 425 preparedRegistrations.clear(); 426 } 427 } 428 429 @("registration") 430 debug(MYSQLN_TESTS) 431 unittest 432 { 433 import mysql.commands; 434 auto pool = new MySQLPool(testConnectionStr); 435 436 // Setup 437 Connection cn = pool.lockConnection(); 438 cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); 439 cn.exec("CREATE TABLE `poolRegistration` ( 440 `data` LONGBLOB 441 ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); 442 immutable sql = "SELECT * from `poolRegistration`"; 443 //auto cn2 = pool.lockConnection(); // Seems to return the same connection as `cn` 444 auto cn2 = pool.createConnection(); 445 pool.applyAuto(cn2); 446 assert(cn !is cn2); 447 448 // Tests: 449 // Initial 450 assert(pool.isAutoCleared(sql)); 451 assert(pool.isAutoRegistered(sql)); 452 assert(pool.isAutoReleased(sql)); 453 assert(!cn.isRegistered(sql)); 454 assert(!cn2.isRegistered(sql)); 455 456 // Register on connection #1 457 auto prepared = cn.prepare(sql); 458 { 459 assert(pool.isAutoCleared(sql)); 460 assert(pool.isAutoRegistered(sql)); 461 assert(pool.isAutoReleased(sql)); 462 assert(cn.isRegistered(sql)); 463 assert(!cn2.isRegistered(sql)); 464 465 //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` 466 auto cn3 = pool.createConnection(); 467 pool.applyAuto(cn3); 468 assert(!cn3.isRegistered(sql)); 469 } 470 471 // autoRegister 472 pool.autoRegister(prepared); 473 { 474 assert(!pool.isAutoCleared(sql)); 475 assert(pool.isAutoRegistered(sql)); 476 assert(!pool.isAutoReleased(sql)); 477 assert(cn.isRegistered(sql)); 478 assert(!cn2.isRegistered(sql)); 479 480 //auto cn3 = pool.lockConnection(); // Seems to return the *same* connection as `cn` 481 auto cn3 = pool.createConnection(); 482 pool.applyAuto(cn3); 483 assert(cn3.isRegistered(sql)); 484 } 485 486 // autoRelease 487 pool.autoRelease(prepared); 488 { 489 assert(!pool.isAutoCleared(sql)); 490 assert(!pool.isAutoRegistered(sql)); 491 assert(pool.isAutoReleased(sql)); 492 assert(cn.isRegistered(sql)); 493 assert(!cn2.isRegistered(sql)); 494 495 //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` 496 auto cn3 = pool.createConnection(); 497 pool.applyAuto(cn3); 498 assert(!cn3.isRegistered(sql)); 499 } 500 501 // clearAuto 502 pool.clearAuto(prepared); 503 { 504 assert(pool.isAutoCleared(sql)); 505 assert(pool.isAutoRegistered(sql)); 506 assert(pool.isAutoReleased(sql)); 507 assert(cn.isRegistered(sql)); 508 assert(!cn2.isRegistered(sql)); 509 510 //auto cn3 = pool.lockConnection(); // Seems to return the same connection as `cn` 511 auto cn3 = pool.createConnection(); 512 pool.applyAuto(cn3); 513 assert(!cn3.isRegistered(sql)); 514 } 515 } 516 }