/// Test to see if the creation of connections in a pool do not exceed maximum. import 'dart:async'; import 'dart:core'; import 'dart:io'; import 'package:sqljocky5/sqljocky.dart'; //================================================================ // Constants /// Set this to _true_ to simulate the expected behaviour. /// /// When set to true, delays are added in between the starting of transactions /// to avoid the race condition. /// /// The delay allows time for the connection to be created and added to the /// `_pool` list. Without the delay, the connection may not get added to the /// pool list before the next connection/transaction is requested. That next /// request will create a new connection, not realising that the maximum /// number of connections for the pool has already been reached or exceeded. /// /// Actually, it is not so much the time delay, but yielding so the initial part /// of creating a connection can execute. bool suppressRaceCondition = false; /// Set to the `wait_timeout` that the _mysqld_ has been configured with. /// /// The second set of activities assume the _mysqld_ has disconnected the /// connections that were created in the first set of activities. /// /// Put something like this in _/etc/my.cnf_ or in a file under the /// _/etc/my.cnf.d_ directory and restart _mysqld_. /// /// [mysqld] /// wait_timeout=30 int mysqldWaitTimeout = 30; // seconds //================================================================ // Global used to track how many active transactions. int numSimultaneous; // Global used to track the maximum number of active transactions reached. int maxSimultaneous; //---------------------------------------------------------------- /// An activity that gets a transaction, waits 1 second and then does a rollback /// on the transaction (i.e. releasing it back into the pool). /// /// In theory, the maximum number of transactions is capped at the maximum /// number of connections in the pool. Other activities will be blocked when /// they try to start a transaction. Blocked activities will only run when /// they can start a transaction, because another activity has released the /// connection it had back into the pool. Future activity(ConnectionPool pool, int jobId) async { // Start a transaction //final cnx = await pool.getConnection(); final tx = await pool.startTransaction(consistent: true); // use above line instead to test connections // Track how many simultaneous transactions there are numSimultaneous += 1; if (maxSimultaneous < numSimultaneous) { maxSimultaneous = numSimultaneous; } print("[$jobId] acquired ($numSimultaneous concurrent)"); // Delay to allow other activities to run in parallel await new Future.delayed(const Duration(seconds: 3)); numSimultaneous -= 1; // Release the transaction //await cnx.release(); await tx.rollback(); // use above line instead to test connections print("[$jobId] released"); return true; } //---------------------------------------------------------------- Future main(List arguments) async { final maxPoolSize = 5; final excess1 = 1; final excess2 = 3; assert(0 < excess1); assert(0 < excess2); assert(excess1 < excess2); // Note: this works even for 0 milliseconds final noRaceConditionDelay = const Duration(milliseconds: 100); final pool = new ConnectionPool( host: "localhost", port: 13306, useSSL: false, user: "test", password: "p@ssw0rd", db: "qriscloud", max: maxPoolSize); if (pool == null) { stderr.write("Error: could not create pool\n"); exit(1); } if (suppressRaceCondition) { print("Note: delays will be added to AVOID the race condition"); } var passed = true; // Create more tasks then the maximum number of connections in the pool numSimultaneous = 0; maxSimultaneous = 0; final jobs1 = >[]; print("Trying ${maxPoolSize + excess1} jobs (with maxPoolSize=$maxPoolSize)"); for (var x = 1; x <= (maxPoolSize + excess1); x++) { jobs1.add(activity(pool, x)); if (suppressRaceCondition) { await new Future.delayed(noRaceConditionDelay); } } // Wait for all the tasks to complete await Future.wait(jobs1); assert(numSimultaneous == 0); // Check how many transaction were running at the same time if (maxPoolSize < maxSimultaneous) { print("UNEXPECTED: got $maxSimultaneous when maxPoolSize=$maxPoolSize"); passed = false; } // Wait for the server to time out the connections. // // Note: if the connections are not timed out, then the pool is populated // with connections (at least to the maximum, if not more) and the race // condition won't happen. But if they do time out, then the race condition // can occur again. print( "Waiting for server to timeout connections (assuming mysqld wait_timeout=$mysqldWaitTimeout seconds)"); await new Future.delayed(new Duration(seconds: (mysqldWaitTimeout + 3))); final firstMaxTx = maxSimultaneous; // Repeat. // Should recreate connections since mysqld should have terminated them. numSimultaneous = 0; maxSimultaneous = 0; final jobs2 = >[]; print("Trying ${maxPoolSize + excess2} jobs (with maxPoolSize=$maxPoolSize)"); for (var x = 1; x <= (maxPoolSize + excess2); x++) { jobs2.add(activity(pool, x)); if (suppressRaceCondition) { await new Future.delayed(noRaceConditionDelay); } } // Wait for all the tasks to complete await Future.wait(jobs2); assert(numSimultaneous == 0); // Check how many transaction were running at the same time if (maxPoolSize < maxSimultaneous) { print("UNEXPECTED: got $maxSimultaneous when maxPoolSize=$maxPoolSize"); if (maxSimultaneous == firstMaxTx) { print("WARNING: connection timeout did not happen (check mysqld config)"); } passed = false; } // Shutdown pool pool.closeConnectionsWhenNotInUse(); if (passed) { print("OK"); exit(0); } else { print("FAILED"); exit(1); } }