Repository: blackbeam/rust-mysql-simple (language: rust)

Issue:
Very high CPU usage when accessing BufferPool
We have a Rust service that has thousands of threads using their own `mysql::Conn` to multiple databases. We noticed up to 10x CPU usage increases when going from mysql v20 to v21. The regression appears to be caused by contention on the added global buffer pool and the implementation of `std::sync::Mutex` involving spinlocks (see below).
FYI we are using jemalloc and the `Vec` allocations that the usage of `BufferPool` replaced didn't appear to be a bottleneck for us.

![screenshot](https://user-images.githubusercontent.com/67885696/146881159-236c5500-7bdf-4b9c-82db-fcf4eb0c2e44.png)

Here are some suggestions that I can think of for how this can be addressed:
1) Revert to allocating `Vec`'s on demand
* Pros:
  - Known state; this is what was used prior to the introduction of `BufferPool`
  - Minimizes memory usage
  - Performs well enough with "good" heap allocators
* Cons:
  - Might not perform so well with e.g. glibc's malloc
  
2) Make each `mysq::Conn` object have its own buffer
* Pros:
  - Minimizes heap allocations
* Cons:
  - Likely increases memory usage as now each connection keeps a long-lived buffer

3) Have `BufferPool` use a lock-free pool data structure such as what the `pool` crate provides:
* Pros:
  - Minimal code changes
  - Should perform better than current approach under high concurrency
  - Minimizes heap allocations
* Cons:
  - May not fully address the CPU usage regression depending on how well `pool` or the chosen alternative performs
  - Adds another dependency
  - Still involves global state which is not ideal
  - Might cause higher memory usage than 1)

In addition to these a hybrid approach could be used where a new flag controls whether 1-2) or 3) are used.

  


Discussion / hints:
Hi. This might be addressed in blackbeam/mysql_async#170. I'm planning to port this for the next release.

Also I think that a flag that turns of the pool is the best option for your case, so I'll add it.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 69bd3f4..269f44a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mysql"
-version = "21.0.2"
+version = "22.0.0"
 authors = ["blackbeam"]
 description = "Mysql client library implemented in rust"
 license = "MIT/Apache-2.0"
@@ -9,7 +9,7 @@ repository = "https://github.com/blackbeam/rust-mysql-simple"
 keywords = ["database", "sql"]
 exclude = ["tests/*", ".*", "Makefile"]
 categories = ["database"]
-edition = "2018"
+edition = "2021"
 build = "build.rs"
 
 [badges.azure-devops]
@@ -27,7 +27,10 @@ debug = true
 [features]
 default = [
     "native-tls",
+
+    # It is necessary to choose one of `flate2` backends.
     "flate2/zlib",
+
     "mysql_common/bigdecimal03",
     "mysql_common/rust_decimal",
     "mysql_common/time03",
@@ -46,6 +49,7 @@ time = "0.3"
 [dependencies]
 bufstream = "~0.1"
 bytes = "1.0.1"
+crossbeam = "0.8.1"
 io-enum = "1.0.0"
 flate2 = { version = "1.0", default-features = false }
 lru = "0.7"
diff --git a/README.md b/README.md
index 3b1dd65..85886b2 100644
--- a/README.md
+++ b/README.md
@@ -13,12 +13,13 @@ This crate offers:
 Features:
 
 *   macOS, Windows and Linux support;
-*   TLS support via **nativetls** create;
+*   TLS support via **nativetls** or **rustls** (see the [SSL Support](#ssl-support) section);
 *   MySql text protocol support, i.e. support of simple text queries and text result sets;
 *   MySql binary protocol support, i.e. support of prepared statements and binary result sets;
 *   support of multi-result sets;
-*   support of named parameters for prepared statements;
-*   optional per-connection cache of prepared statements;
+*   support of named parameters for prepared statements (see the [Named Parameters](#named-parameters) section);
+*   optional per-connection cache of prepared statements (see the [Statement Cache](#statement-cache) section);
+*   buffer pool (see the [Buffer Pool](#buffer-pool) section);
 *   support of MySql packets larger than 2^24;
 *   support of Unix sockets and Windows named pipes;
 *   support of custom LOCAL INFILE handlers;
@@ -50,8 +51,8 @@ struct Payment {
 }
 
 let url = "mysql://root:password@localhost:3307/db_name";
-let opts = Opts::from_url(url)?;
-let pool = Pool::new(opts)?;
+
+let pool = Pool::new(url)?;
 
 let mut conn = pool.get_conn()?;
 
@@ -306,8 +307,8 @@ match unknown_val {
         println!("A double precision float value: {}", from_value::<f64>(val))
     }
     val @ Value::Date(..) => {
-        use mysql::chrono::NaiveDateTime;
-        println!("A date value: {}", from_value::<NaiveDateTime>(val))
+        use time::PrimitiveDateTime;
+        println!("A date value: {}", from_value::<PrimitiveDateTime>(val))
     }
     val @ Value::Time(..) => {
         use std::time::Duration;
@@ -455,8 +456,7 @@ let mut conn = Conn::new(get_opts())?;
 let mut result = conn.query_iter("SELECT 1, 2; SELECT 3, 3.14;")?;
 
 let mut sets = 0;
-while let Some(result_set) = result.next_set() {
-    let result_set = result_set?;
+while let Some(result_set) = result.current_set() {
     sets += 1;
 
     println!("Result set columns: {:?}", result_set.columns());
@@ -598,13 +598,17 @@ Statement cache is completely disabled if `stmt_cache_size` is zero.
 #### Named parameters
 
 MySql itself doesn't have named parameters support, so it's implemented on the client side.
-One should use `:name` as a placeholder syntax for a named parameter.
+One should use `:name` as a placeholder syntax for a named parameter. Named parameters uses
+the following naming convention:
+
+* parameter name must start with either `_` or `a..z`
+* parameter name may continue with `_`, `a..z` and `0..9`
 
 Named parameters may be repeated within the statement, e.g `SELECT :foo, :foo` will require
 a single named parameter `foo` that will be repeated on the corresponding positions during
 statement execution.
 
-One should use the `params!` macro to build a parameters for execution.
+One should use the `params!` macro to build parameters for execution.
 
 **Note:** Positional and named parameters can't be mixed within the single statement.
 
@@ -626,6 +630,23 @@ assert_eq!((foo, 13, foo), val_42);
 assert_eq!((13, foo, 13), val_13);
 ```
 
+#### Buffer pool
+
+Crate uses the global lock-free buffer pool for the purpose of IO and data serialization/deserialization,
+that helps to avoid allocations for basic scenarios. You can control it's characteristics using
+the following environment variables:
+
+*   `RUST_MYSQL_BUFFER_POOL_CAP` (defaults to 128) – controls the pool capacity. Dropped buffer will
+    be immediately deallocated if the pool is full.
+
+    **Note:** it might be the case, that you don't need the pooling (say you are using jemalloc).
+    It's possible to disable the pool by setting the `RUST_MYSQL_BUFFER_POOL_CAP` environment
+    variable to `0`.
+
+*   `RUST_MYSQL_BUFFER_SIZE_CAP` (defaults to 4MiB) – controls the maximum capacity of a buffer
+    stored in the pool. Capacity of a dropped buffer will be shrinked to this value when buffer
+    is returning to the pool.
+
 #### `BinQuery` and `BatchQuery` traits.
 
 `BinQuery` and `BatchQuery` traits covers the set of `Queryable::exec*` methods from
@@ -697,6 +718,29 @@ These methods will consume only the first result set, other result sets will be
 The trait also defines the `exec_batch` function, which is a helper for batch statement
 execution.
 
+### SSL Support
+
+SSL support comes in two flavors:
+
+1.  Based on **native-tls** – this is the default option, that usually works without pitfalls
+    (see the `native-tls` crate feature).
+2.  Based on **rustls** – TLS backend written in Rust. Please use the `rustls-tls` crate feature
+    to enable:
+
+    ```toml
+    [dependencies]
+    mysql = { version = "*", no-default-features = true, features = ["rustls-tls"] }
+    # Please note, that the previous line disables default mysql features,
+    # so now we have to choose the flate2 backend (this is necessary):
+    flate2 = { version = "1.0", no-default-features = true, features = ["zlib"] }
+    ```
+
+    Please also note a few things about this backend:
+
+    *   it will fail if you'll try to connect to the server by its IP address, hostname is required;
+    *   it, most likely, won't work on windows, at least with default server certs, generated by the
+        MySql installer.
+
 [crate docs]: https://docs.rs/mysql
 [mysql_common docs]: https://docs.rs/mysql_common
 [max_prepared_stmt_count]: https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_prepared_stmt_count
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 064bf2c..5a6a5c0 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -52,7 +52,7 @@ jobs:
           SSL=false COMPRESS=false cargo test --no-default-features --features flate2/zlib,mysql_common/time03
         env:
           RUST_BACKTRACE: 1
-          DATABASE_URL: mysql://root:root@127.0.0.1:3306/mysql
+          DATABASE_URL: mysql://root:root@localhost:3306/mysql
         displayName: Run tests
 
   - job: "TestBasicMacOs"
@@ -98,7 +98,7 @@ jobs:
           SSL=false COMPRESS=false cargo test --no-default-features --features flate2/zlib,mysql_common/time03
         env:
           RUST_BACKTRACE: 1
-          DATABASE_URL: mysql://root@127.0.0.1/mysql
+          DATABASE_URL: mysql://root@localhost/mysql
         displayName: Run tests
 
   - job: "TestBasicWindows"
@@ -149,7 +149,7 @@ jobs:
           SSL=false COMPRESS=false cargo test --no-default-features --features flate2/zlib,mysql_common/time03
         env:
           RUST_BACKTRACE: 1
-          DATABASE_URL: mysql://root:password@127.0.0.1/mysql
+          DATABASE_URL: mysql://root:password@localhost/mysql
         displayName: Run tests
 
   - job: "TestMySql"
@@ -186,7 +186,7 @@ jobs:
           docker exec container bash -l -c "curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable"
         displayName: Install Rust in docker
       - bash: |
-          if [[ "5.6" != "$(DB_VERSION)" ]]; then SSL=true; else DATABASE_URL="mysql://root2:password@127.0.0.1/mysql?secure_auth=false"; fi
+          if [[ "5.6" != "$(DB_VERSION)" ]]; then SSL=true; else DATABASE_URL="mysql://root2:password@localhost/mysql?secure_auth=false"; fi
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL COMPRESS=true cargo test"
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=$SSL cargo test"
@@ -199,7 +199,7 @@ jobs:
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=false cargo test --no-default-features --features flate2/zlib,mysql_common/time03"
         env:
           RUST_BACKTRACE: 1
-          DATABASE_URL: mysql://root:password@127.0.0.1/mysql
+          DATABASE_URL: mysql://root:password@localhost/mysql
         displayName: Run tests in Docker
 
   - job: "TestMariaDb"
@@ -267,5 +267,5 @@ jobs:
           docker exec container bash -l -c "cd \$HOME && DATABASE_URL=$DATABASE_URL SSL=false COMPRESS=false cargo test --no-default-features --features flate2/zlib,mysql_common/time03"
         env:
           RUST_BACKTRACE: 1
-          DATABASE_URL: mysql://root:password@127.0.0.1/mysql
+          DATABASE_URL: mysql://root:password@localhost/mysql
         displayName: Run tests in Docker
diff --git a/src/buffer_pool.rs b/src/buffer_pool.rs
index 54b427e..8d9f2d0 100644
--- a/src/buffer_pool.rs
+++ b/src/buffer_pool.rs
@@ -6,63 +6,65 @@
 // option. All files in the project carrying such notice may not be copied,
 // modified, or distributed except according to those terms.
 
-use std::{
-    mem::replace,
-    ops::Deref,
-    sync::{Arc, Mutex},
-};
+use crossbeam::queue::ArrayQueue;
+
+use std::{mem::replace, ops::Deref, sync::Arc};
+
+const DEFAULT_MYSQL_BUFFER_POOL_CAP: usize = 128;
+const DEFAULT_MYSQL_BUFFER_SIZE_CAP: usize = 4 * 1024 * 1024;
 
 #[derive(Debug)]
-pub struct BufferPool {
-    pool_cap: usize,
+struct Inner {
     buffer_cap: usize,
-    pool: Mutex<Vec<Vec<u8>>>,
+    pool: ArrayQueue<Vec<u8>>,
 }
 
-impl BufferPool {
-    pub fn new() -> Self {
-        let pool_cap = std::env::var("MYSQL_BUFFER_POOL_CAP")
-            .ok()
-            .and_then(|x| x.parse().ok())
-            .unwrap_or(128_usize);
-
-        let buffer_cap = std::env::var("MYSQL_BUFFER_SIZE_CAP")
-            .ok()
-            .and_then(|x| x.parse().ok())
-            .unwrap_or(4 * 1024 * 1024);
-
-        Self {
-            pool: Default::default(),
-            pool_cap,
-            buffer_cap,
-        }
-    }
-
-    pub fn get(self: &Arc<Self>) -> PooledBuf {
-        let mut buf = self.pool.lock().unwrap().pop().unwrap_or_default();
+impl Inner {
+    fn get(self: &Arc<Self>) -> PooledBuf {
+        let mut buf = self.pool.pop().unwrap_or_default();
 
         // SAFETY:
         // 1. OK – 0 is always within capacity
         // 2. OK - nothing to initialize
         unsafe { buf.set_len(0) }
 
-        PooledBuf(buf, self.clone())
+        PooledBuf(buf, Some(self.clone()))
     }
 
-    fn put(self: &Arc<Self>, mut buf: Vec<u8>) {
-        if buf.len() > self.buffer_cap {
-            // TODO: until `Vec::shrink_to` stabilization
+    fn put(&self, mut buf: Vec<u8>) {
+        buf.shrink_to(self.buffer_cap);
+        let _ = self.pool.push(buf);
+    }
+}
 
-            // SAFETY:
-            // 1. OK – new_len <= capacity
-            // 2. OK - 0..new_len is initialized
-            unsafe { buf.set_len(self.buffer_cap) }
-            buf.shrink_to_fit();
-        }
+/// Smart pointer to a buffer pool.
+#[derive(Debug, Clone)]
+pub struct BufferPool(Option<Arc<Inner>>);
+
+impl BufferPool {
+    pub fn new() -> Self {
+        let pool_cap = std::env::var("RUST_MYSQL_BUFFER_POOL_CAP")
+            .ok()
+            .and_then(|x| x.parse().ok())
+            .unwrap_or(DEFAULT_MYSQL_BUFFER_POOL_CAP);
 
-        let mut pool = self.pool.lock().unwrap();
-        if pool.len() < self.pool_cap {
-            pool.push(buf);
+        let buffer_cap = std::env::var("RUST_MYSQL_BUFFER_SIZE_CAP")
+            .ok()
+            .and_then(|x| x.parse().ok())
+            .unwrap_or(DEFAULT_MYSQL_BUFFER_SIZE_CAP);
+
+        Self((pool_cap > 0).then(|| {
+            Arc::new(Inner {
+                buffer_cap,
+                pool: ArrayQueue::new(pool_cap),
+            })
+        }))
+    }
+
+    pub fn get(self: &Arc<Self>) -> PooledBuf {
+        match self.0 {
+            Some(ref inner) => inner.get(),
+            None => PooledBuf(Vec::new(), None),
         }
     }
 }
@@ -74,7 +76,7 @@ impl Default for BufferPool {
 }
 
 #[derive(Debug)]
-pub struct PooledBuf(Vec<u8>, Arc<BufferPool>);
+pub struct PooledBuf(Vec<u8>, Option<Arc<Inner>>);
 
 impl AsMut<Vec<u8>> for PooledBuf {
     fn as_mut(&mut self) -> &mut Vec<u8> {
@@ -92,6 +94,8 @@ impl Deref for PooledBuf {
 
 impl Drop for PooledBuf {
     fn drop(&mut self) {
-        self.1.put(replace(&mut self.0, vec![]))
+        if let Some(ref inner) = self.1 {
+            inner.put(replace(&mut self.0, vec![]));
+        }
     }
 }
diff --git a/src/conn/mod.rs b/src/conn/mod.rs
index 8592cc1..cc2899e 100644
--- a/src/conn/mod.rs
+++ b/src/conn/mod.rs
@@ -1703,6 +1703,19 @@ mod test {
             )
             .unwrap();
             conn.exec_drop("SELECT * FROM TEST_TABLE", ()).unwrap();
+
+            let mut query_result = conn
+                .query_iter(
+                    r"
+                SELECT * FROM TEST_TABLE;
+                INSERT INTO TEST_TABLE (name) VALUES ('one');
+                DO 0;",
+                )
+                .unwrap();
+
+            while let Some(result) = query_result.current_set() {
+                result.affected_rows();
+            }
         }
 
         #[test]
@@ -1739,9 +1752,9 @@ mod test {
             }
             let mut result = conn.query_iter("SELECT 1; SELECT 2; SELECT 3;").unwrap();
             let mut i = 0;
-            while let Some(result_set) = result.next_set() {
+            while let Some(result_set) = result.current_set() {
                 i += 1;
-                for row in result_set.unwrap() {
+                for row in result_set {
                     match i {
                         1 => assert_eq!(row.unwrap().unwrap(), vec![Bytes(b"1".to_vec())]),
                         2 => assert_eq!(row.unwrap().unwrap(), vec![Bytes(b"2".to_vec())]),
diff --git a/src/conn/opts/mod.rs b/src/conn/opts/mod.rs
index fb685e4..b9847b6 100644
--- a/src/conn/opts/mod.rs
+++ b/src/conn/opts/mod.rs
@@ -481,7 +481,7 @@ impl OptsBuilder {
     /// - stmt_cache_size = Number of prepared statements cached on the client side (per connection)
     /// - secure_auth = Disable `mysql_old_password` auth plugin
     ///
-    /// Login .cnf file parsing lib https://github.com/rjcortese/myloginrs returns a HashMap for client configs
+    /// Login .cnf file parsing lib <https://github.com/rjcortese/myloginrs> returns a HashMap for client configs
     ///
     /// **Note:** You do **not** have to use myloginrs lib.
     pub fn from_hash_map(mut self, client: &HashMap<String, String>) -> Result<Self, UrlError> {
diff --git a/src/conn/query.rs b/src/conn/query.rs
index b4a6498..4911ae9 100644
--- a/src/conn/query.rs
+++ b/src/conn/query.rs
@@ -384,8 +384,8 @@ where
             let params = params.into();
             let meta = conn._execute(&*statement, params)?;
             let mut query_result = QueryResult::<Binary>::new((&mut *conn).into(), meta);
-            while let Some(result_set) = query_result.next_set() {
-                for row in result_set? {
+            while let Some(result_set) = query_result.current_set() {
+                for row in result_set {
                     row?;
                 }
             }
diff --git a/src/conn/query_result.rs b/src/conn/query_result.rs
index 9bd35ac..341bec2 100644
--- a/src/conn/query_result.rs
+++ b/src/conn/query_result.rs
@@ -100,7 +100,7 @@ impl From<Or<Vec<Column>, OkPacket<'static>>> for SetIteratorState {
 /// Response to a query or statement execution.
 ///
 /// It is an iterator:
-/// *   over result sets (via `Self::next_set`)
+/// *   over result sets (via `Self::current_set`)
 /// *   over rows of a current result set (via `Iterator` impl)
 #[derive(Debug)]
 pub struct QueryResult<'c, 't, 'tc, T: crate::prelude::Protocol> {
@@ -153,7 +153,9 @@ impl<'c, 't, 'tc, T: crate::prelude::Protocol> QueryResult<'c, 't, 'tc, T> {
     }
 
     /// Returns an iterator over the current result set.
-    pub fn next_set<'d>(&'d mut self) -> Option<Result<ResultSet<'c, 't, 'tc, 'd, T>>> {
+    ///
+    /// Subsequent call will return the next result set and so on.
+    pub fn current_set<'d>(&'d mut self) -> Option<ResultSet<'c, 't, 'tc, 'd, T>> {
         use SetIteratorState::*;
 
         if let OnBoundary | Done = &self.state {
@@ -164,10 +166,10 @@ impl<'c, 't, 'tc, T: crate::prelude::Protocol> QueryResult<'c, 't, 'tc, T> {
 
             None
         } else {
-            Some(Ok(ResultSet {
+            Some(ResultSet {
                 set_index: self.set_index,
                 inner: self,
-            }))
+            })
         }
     }
 
@@ -229,7 +231,7 @@ impl<'c, 't, 'tc, T: crate::prelude::Protocol> QueryResult<'c, 't, 'tc, T> {
 
 impl<'c, 't, 'tc, T: crate::prelude::Protocol> Drop for QueryResult<'c, 't, 'tc, T> {
     fn drop(&mut self) {
-        while self.next_set().is_some() {}
+        while self.current_set().is_some() {}
     }
 }
 
diff --git a/src/io/tcp.rs b/src/io/tcp.rs
index cba7259..1ea21e5 100644
--- a/src/io/tcp.rs
+++ b/src/io/tcp.rs
@@ -6,7 +6,7 @@
 // option. All files in the project carrying such notice may not be copied,
 // modified, or distributed except according to those terms.
 
-use socket2::{Domain, Socket, Type};
+use socket2::{Domain, SockAddr, Socket, Type};
 
 use std::{
     io,
@@ -86,35 +86,67 @@ impl<T: ToSocketAddrs> MyTcpBuilder<T> {
             "could not connect to any address with specified bind address"
         };
         let err = io::Error::new(io::ErrorKind::Other, err_msg);
-        address
-            .to_socket_addrs()?
-            .fold(Err(err), |prev, sock_addr| {
-                prev.or_else(|_| {
-                    let domain = Domain::for_address(sock_addr);
+
+        let addrs = address.to_socket_addrs()?.collect::<Vec<_>>();
+
+        let socket = if let Some(bind_address) = bind_address {
+            let fold_fun = |prev, sock_addr: &SocketAddr| match prev {
+                Ok(socket) => Ok(socket),
+                Err(_) => {
+                    let domain = Domain::for_address(*sock_addr);
                     let socket = Socket::new(domain, Type::STREAM, None)?;
-                    if let Some(bind_address) = bind_address {
-                        if bind_address.is_ipv4() == sock_addr.is_ipv4() {
-                            socket.bind(&bind_address.into())?;
-                        }
-                    }
+                    socket.bind(&bind_address.into())?;
                     if let Some(connect_timeout) = connect_timeout {
-                        socket.connect_timeout(&sock_addr.into(), connect_timeout)?;
+                        socket.connect_timeout(&SockAddr::from(*sock_addr), connect_timeout)?;
                     } else {
-                        socket.connect(&sock_addr.into())?;
+                        socket.connect(&SockAddr::from(*sock_addr))?;
                     }
                     Ok(socket)
-                })
-            })
-            .and_then(|socket| {
-                socket.set_read_timeout(read_timeout)?;
-                socket.set_write_timeout(write_timeout)?;
-                if let Some(duration) = keepalive_time_ms {
-                    let conf = socket2::TcpKeepalive::new()
-                        .with_time(Duration::from_millis(duration as u64));
-                    socket.set_tcp_keepalive(&conf)?;
                 }
-                socket.set_nodelay(nodelay)?;
-                Ok(TcpStream::from(socket))
-            })
+            };
+
+            if bind_address.is_ipv4() {
+                // client wants to bind to ipv4, so let's look for ipv4 addresses first
+                addrs
+                    .iter()
+                    .filter(|x| x.is_ipv4())
+                    .fold(Err(err), fold_fun)
+                    .or_else(|e| addrs.iter().filter(|x| x.is_ipv6()).fold(Err(e), fold_fun))
+            } else {
+                // client wants to bind to ipv6, so let's look for ipv6 addresses first
+                addrs
+                    .iter()
+                    .filter(|x| x.is_ipv6())
+                    .fold(Err(err), fold_fun)
+                    .or_else(|e| addrs.iter().filter(|x| x.is_ipv4()).fold(Err(e), fold_fun))
+            }
+        } else {
+            // no bind address
+            addrs
+                .into_iter()
+                .fold(Err(err), |prev, sock_addr| match prev {
+                    Ok(socket) => Ok(socket),
+                    Err(_) => {
+                        let domain = Domain::for_address(sock_addr);
+                        let socket = Socket::new(domain, Type::STREAM, None)?;
+                        if let Some(connect_timeout) = connect_timeout {
+                            socket.connect_timeout(&sock_addr.into(), connect_timeout)?;
+                        } else {
+                            socket.connect(&sock_addr.into())?;
+                        }
+                        Ok(socket)
+                    }
+                })
+        }?;
+
+        socket.set_read_timeout(read_timeout)?;
+        socket.set_write_timeout(write_timeout)?;
+        if let Some(duration) = keepalive_time_ms {
+            let conf =
+                socket2::TcpKeepalive::new().with_time(Duration::from_millis(duration as u64));
+            socket.set_tcp_keepalive(&conf)?;
+        }
+        socket.set_nodelay(nodelay)?;
+        Ok(TcpStream::from(socket))
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index fc77af8..ed1b85f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,12 +14,13 @@
 //! Features:
 //!
 //! *   macOS, Windows and Linux support;
-//! *   TLS support via **nativetls** create;
+//! *   TLS support via **nativetls** or **rustls** (see the [SSL Support](#ssl-support) section);
 //! *   MySql text protocol support, i.e. support of simple text queries and text result sets;
 //! *   MySql binary protocol support, i.e. support of prepared statements and binary result sets;
 //! *   support of multi-result sets;
-//! *   support of named parameters for prepared statements;
-//! *   optional per-connection cache of prepared statements;
+//! *   support of named parameters for prepared statements (see the [Named Parameters](#named-parameters) section);
+//! *   optional per-connection cache of prepared statements (see the [Statement Cache](#statement-cache) section);
+//! *   buffer pool (see the [Buffer Pool](#buffer-pool) section);
 //! *   support of MySql packets larger than 2^24;
 //! *   support of Unix sockets and Windows named pipes;
 //! *   support of custom LOCAL INFILE handlers;
@@ -483,8 +484,7 @@
 //! let mut result = conn.query_iter("SELECT 1, 2; SELECT 3, 3.14;")?;
 //!
 //! let mut sets = 0;
-//! while let Some(result_set) = result.next_set() {
-//!     let result_set = result_set?;
+//! while let Some(result_set) = result.current_set() {
 //!     sets += 1;
 //!
 //!     println!("Result set columns: {:?}", result_set.columns());
@@ -641,13 +641,17 @@
 //! ### Named parameters
 //!
 //! MySql itself doesn't have named parameters support, so it's implemented on the client side.
-//! One should use `:name` as a placeholder syntax for a named parameter.
+//! One should use `:name` as a placeholder syntax for a named parameter. Named parameters uses
+//! the following naming convention:
+//!
+//! * parameter name must start with either `_` or `a..z`
+//! * parameter name may continue with `_`, `a..z` and `0..9`
 //!
 //! Named parameters may be repeated within the statement, e.g `SELECT :foo, :foo` will require
 //! a single named parameter `foo` that will be repeated on the corresponding positions during
 //! statement execution.
 //!
-//! One should use the `params!` macro to build a parameters for execution.
+//! One should use the `params!` macro to build parameters for execution.
 //!
 //! **Note:** Positional and named parameters can't be mixed within the single statement.
 //!
@@ -673,6 +677,23 @@
 //! # });
 //! ```
 //!
+//! ### Buffer pool
+//!
+//! Crate uses the global lock-free buffer pool for the purpose of IO and data serialization/deserialization,
+//! that helps to avoid allocations for basic scenarios. You can control it's characteristics using
+//! the following environment variables:
+//!
+//! *   `RUST_MYSQL_BUFFER_POOL_CAP` (defaults to 128) – controls the pool capacity. Dropped buffer will
+//!     be immediately deallocated if the pool is full.
+//!
+//!     **Note:** it might be the case, that you don't need the pooling (say you are using jemalloc).
+//!     It's possible to disable the pool by setting the `RUST_MYSQL_BUFFER_POOL_CAP` environment
+//!     variable to `0`.
+//!
+//! *   `RUST_MYSQL_BUFFER_SIZE_CAP` (defaults to 4MiB) – controls the maximum capacity of a buffer
+//!     stored in the pool. Capacity of a dropped buffer will be shrinked to this value when buffer
+//!     is returning to the pool.
+//!
 //! ### `BinQuery` and `BatchQuery` traits.
 //!
 //! `BinQuery` and `BatchQuery` traits covers the set of `Queryable::exec*` methods from
@@ -748,14 +769,34 @@
 //! The trait also defines the `exec_batch` function, which is a helper for batch statement
 //! execution.
 //!
+//! ## SSL Support
+//!
+//! SSL support comes in two flavors:
+//!
+//! 1.  Based on **native-tls** – this is the default option, that usually works without pitfalls
+//!     (see the `native-tls` crate feature).
+//! 2.  Based on **rustls** – TLS backend written in Rust. Please use the `rustls-tls` crate feature
+//!     to enable:
+//!
+//!     ```toml
+//!     [dependencies]
+//!     mysql = { version = "*", no-default-features = true, features = ["rustls-tls"] }
+//!     # Please note, that the previous line disables default mysql features,
+//!     # so now we have to choose the flate2 backend (this is necessary):
+//!     flate2 = { version = "1.0", no-default-features = true, features = ["zlib"] }
+//!     ```
+//!
+//!     Please also note a few things about this backend:
+//!
+//!     *   it will fail if you'll try to connect to the server by its IP address, hostname is required;
+//!     *   it, most likely, won't work on windows, at least with default server certs, generated by the
+//!         MySql installer.
+//!
 //! [crate docs]: https://docs.rs/mysql
 //! [mysql_common docs]: https://docs.rs/mysql_common
 //! [max_prepared_stmt_count]: https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_prepared_stmt_count
 //!
 
-#![crate_name = "mysql"]
-#![crate_type = "rlib"]
-#![crate_type = "dylib"]
 #![cfg_attr(feature = "nightly", feature(test))]
 #[cfg(feature = "nightly")]
 extern crate test;


Test patch:
diff --git a/tests/client.p12 b/tests/client.p12
deleted file mode 100644
index f05b65a..0000000
Binary files a/tests/client.p12 and /dev/null differ