├── config.toml ├── LICENSE ├── .travis.yml ├── README.md ├── cluster.go └── cluster_test.go /config.toml: -------------------------------------------------------------------------------- 1 | # Config file for the cluster test 2 | # Format: TOML version 0.2.0 3 | # https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md) 4 | 5 | # The DB Pool to work on 6 | [[Nodes]] 7 | Name = "maria1" 8 | HostName = "127.0.0.1" 9 | Port = 3301 10 | UserName = "root" 11 | DBName = "test" 12 | 13 | [[Nodes]] 14 | Name = "maria2" 15 | HostName = "127.0.0.1" 16 | Port = 3302 17 | UserName = "root" 18 | DBName = "test" 19 | 20 | [[Nodes]] 21 | Name = "maria3" 22 | HostName = "127.0.0.1" 23 | Port = 3303 24 | UserName = "root" 25 | DBName = "test" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, tkr@ecix.net (Peering GmbH) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | - 1.2 5 | - 1.1 6 | 7 | notifications: 8 | email: 9 | recipients: 10 | - tkr@ecix.net 11 | 12 | before_install: 13 | - sudo apt-get install -y python-software-properties dtach 14 | - sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db 15 | - sudo add-apt-repository 'deb http://mirror.netcologne.de/mariadb/repo/5.5/ubuntu precise main' 16 | - sudo apt-get update 17 | 18 | install: 19 | - sudo apt-get install mariadb-galera-server galera -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" 20 | 21 | before_script: 22 | - sudo mkdir -p /tmp/maria1 /tmp/maria2 /tmp/maria3 23 | - sudo chown mysql:mysql /tmp/maria* 24 | - sudo mysql_install_db --datadir=/tmp/maria1 25 | - sudo mysql_install_db --datadir=/tmp/maria2 26 | - sudo mysql_install_db --datadir=/tmp/maria3 27 | - sudo dtach -n /tmp/maria1.soc mysqld --binlog-format=ROW --log-error=/tmp/maria1.log --wsrep-cluster-name="galeratest" --wsrep-cluster-address="gcomm://127.0.0.1:2001,127.0.0.2:2002,127.0.0.3:2003" --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-node-address=127.0.0.1:2001 --wsrep-node-name="maria1" --wsrep-sst-auth=root:hunter2 --wsrep-new-cluster --wsrep-provider-options="pc.bootstrap=true" --datadir=/tmp/maria1 --port=3301 28 | - sudo dtach -n /tmp/maria2.soc mysqld --binlog-format=ROW --log-error=/tmp/maria2.log --wsrep-cluster-name="galeratest" --wsrep-cluster-address="gcomm://127.0.0.1:2001,127.0.0.2:2002,127.0.0.3:2003" --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-node-address=127.0.0.2:2002 --wsrep-node-name="maria2" --wsrep-sst-auth=root:hunter2 --datadir=/tmp/maria2 --port=3302 29 | - sudo dtach -n /tmp/maria3.soc mysqld --binlog-format=ROW --log-error=/tmp/maria3.log --wsrep-cluster-name="galeratest" --wsrep-cluster-address="gcomm://127.0.0.1:2001,127.0.0.2:2002,127.0.0.3:2003" --wsrep-provider=/usr/lib/galera/libgalera_smm.so --wsrep-node-address=127.0.0.3:2003 --wsrep-node-name="maria3" --wsrep-sst-auth=root:hunter2 --datadir=/tmp/maria3 --port=3303 30 | - go get -v github.com/go-sql-driver/mysql 31 | - go get -v github.com/BurntSushi/toml 32 | - sleep 10 33 | - sudo mysql -P 3301 -e "create database IF NOT EXISTS test;" 34 | 35 | 36 | script: 37 | - go test -v -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Archived repo] clustersql [![Build Status](https://travis-ci.org/benthor/clustersql.png)](https://travis-ci.org/benthor/clustersql) [![GoDoc](https://godoc.org/github.com/benthor/clustersql?status.svg)](http://godoc.org/github.com/benthor/clustersql) 2 | 3 | *This project is archived, I myself am not using it any more.* 4 | 5 | Go Clustering SQL Driver - A clustering, implementation-agnostic "meta"-driver for any backend implementing "database/sql/driver". 6 | 7 | It does (latency-based) load-balancing and error-recovery over all registered nodes. 8 | 9 | **It is assumed that database-state is transparently replicated over all nodes by some database-side clustering solution. This driver ONLY handles the client side of such a cluster.** 10 | 11 | This package simply multiplexes the driver.Open() function of sql/driver to every attached node. The function is called on each node, returning the first successfully opened connection. (Any connections opening subsequently will be closed.) If opening does not succeed for any node, the latest error gets returned. Any other errors will be masked by default. However, any given latest error for any attached node will remain exposed through expvar, as well as some basic counters and timestamps. 12 | 13 | To make use of this kind of clustering, use this package with any backend driver implementing "database/sql/driver" like so: 14 | 15 | import "database/sql" 16 | import "github.com/go-sql-driver/mysql" 17 | import "github.com/benthor/clustersql" 18 | 19 | There is currently no way around instanciating the backend driver explicitly 20 | 21 | mysqlDriver := mysql.MySQLDriver{} 22 | 23 | You can perform backend-driver specific settings such as 24 | 25 | err := mysql.SetLogger(mylogger) 26 | 27 | Create a new clustering driver with the backend driver 28 | 29 | clusterDriver := clustersql.NewDriver(mysqlDriver) 30 | 31 | Add nodes, including driver-specific name format, in this case Go-MySQL DSN. Here, we add three nodes belonging to a [galera](https://mariadb.com/kb/en/mariadb/documentation/replication-cluster-multi-master/galera/) cluster 32 | 33 | clusterDriver.AddNode("galera1", "user:password@tcp(dbhost1:3306)/db") 34 | clusterDriver.AddNode("galera2", "user:password@tcp(dbhost2:3306)/db") 35 | clusterDriver.AddNode("galera3", "user:password@tcp(dbhost3:3306)/db") 36 | 37 | Make the clusterDriver available to the go sql interface under an arbitrary name 38 | 39 | sql.Register("myCluster", clusterDriver) 40 | 41 | Open the registered clusterDriver with an arbitrary DSN string (not used) 42 | 43 | db, err := sql.Open("myCluster", "whatever") 44 | 45 | Continue to use the sql interface as documented at http://golang.org/pkg/database/sql/ 46 | 47 | 48 | Before using this in production, you should configure your cluster details in config.toml and run 49 | 50 | go test -v . 51 | 52 | Note however, that non-failure of the above is no guarantee for a correctly set-up cluster. 53 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 by tkr@ecix.net (Peering GmbH) 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // Package clustersql is an SQL "meta"-Driver - A clustering, implementation- 27 | // agnostic wrapper for any backend implementing "database/sql/driver". 28 | // 29 | // It does (latency-based) load-balancing and error-recovery over the registered 30 | // set of nodes. 31 | // 32 | // It is assumed that database-state is transparently replicated over all 33 | // nodes by some database-side clustering solution. This driver ONLY handles 34 | // the client side of such a cluster. 35 | // 36 | // This package simply multiplexes the driver.Open() function of sql/driver to every attached node. The function is called on each node, returning the first successfully opened connection. (Any connections opening subsequently will be closed.) If opening does not succeed for any node, the latest error gets returned. Any other errors will be masked by default. However, any given latest error for any attached node will remain exposed through expvar, as well as some basic counters and timestamps. 37 | // 38 | // To make use of this kind of clustering, use this package with any backend driver 39 | // implementing "database/sql/driver" like so: 40 | // 41 | // import "database/sql" 42 | // import "github.com/go-sql-driver/mysql" 43 | // import "github.com/benthor/clustersql" 44 | // 45 | // There is currently no way around instanciating the backend driver explicitly 46 | // 47 | // mysqlDriver := mysql.MySQLDriver{} 48 | // 49 | // You can perform backend-driver specific settings such as 50 | // 51 | // err := mysql.SetLogger(mylogger) 52 | // 53 | // Create a new clustering driver with the backend driver 54 | // 55 | // clusterDriver := clustersql.NewDriver(mysqlDriver) 56 | // 57 | // Add nodes, including driver-specific name format, in this case Go-MySQL DSN. 58 | // Here, we add three nodes belonging to a galera (https://mariadb.com/kb/en/mariadb/documentation/replication-cluster-multi-master/galera/) cluster 59 | // 60 | // clusterDriver.AddNode("galera1", "user:password@tcp(dbhost1:3306)/db") 61 | // clusterDriver.AddNode("galera2", "user:password@tcp(dbhost2:3306)/db") 62 | // clusterDriver.AddNode("galera3", "user:password@tcp(dbhost3:3306)/db") 63 | // 64 | // Make the clusterDriver available to the go sql interface under an arbitrary 65 | // name 66 | // 67 | // sql.Register("myCluster", clusterDriver) 68 | // 69 | // Open the registered clusterDriver with an arbitrary DSN string (not used) 70 | // 71 | // db, err := sql.Open("myCluster", "whatever") 72 | // 73 | // Continue to use the sql interface as documented at 74 | // http://golang.org/pkg/database/sql/ 75 | // 76 | // Before using this in production, you should configure your cluster details in config.toml and run 77 | // 78 | // go test -v . 79 | // 80 | // Note however, that non-failure of the above is no guarantee for a correctly set-up cluster. 81 | // 82 | // Finally, you SHOULD set db.MaxIdleConns and db.MaxOpenConns to a non-zero value. Although the sql 83 | // driver usually does a good job of doing its own pooling, file descriptors can leak in corner cases 84 | // (of which this library might constitue an example). 85 | package clustersql 86 | 87 | import ( 88 | "database/sql/driver" 89 | "expvar" 90 | "sort" 91 | "time" 92 | ) 93 | 94 | type Driver struct { 95 | nodes map[string]*node 96 | upstreamDriver driver.Driver 97 | exp *expvar.Map 98 | } 99 | 100 | type node struct { 101 | Name string 102 | DSN string 103 | exp *expvar.Map 104 | } 105 | 106 | // AddNode registers a new DSN as name with the upstream Driver. 107 | func (d *Driver) AddNode(name, DSN string) { 108 | m := new(expvar.Map).Init() 109 | n := node{name, DSN, m} 110 | d.exp.Set(name, m) 111 | d.nodes[name] = &n 112 | } 113 | 114 | // DelNode unregisters a named Node from the upstream Driver. This SHOULD(TM) be non-invasive, allowing all pending SQL actions on that node to complete as expected 115 | func (d *Driver) DelNode(name string) { 116 | d.nodes[name] = nil 117 | } 118 | 119 | // Nodes returns a sorted list of the names of the registered Nodes. 120 | func (d *Driver) Nodes() []string { 121 | var list []string 122 | for name := range d.nodes { 123 | list = append(list, name) 124 | } 125 | sort.Strings(list) 126 | return list 127 | } 128 | 129 | // Open will be called by sql.Open once registered. The name argument is ignored (it is only there to satisfy the driver interface) 130 | func (d Driver) Open(name string) (driver.Conn, error) { 131 | type c struct { 132 | conn driver.Conn 133 | err error 134 | n *node 135 | } 136 | die := make(chan bool) 137 | cc := make(chan c) 138 | for _, n := range d.nodes { 139 | go func(n *node, cc chan c, die chan bool) { 140 | conn, err := d.upstreamDriver.Open(n.DSN) 141 | select { 142 | case cc <- c{conn, err, n}: 143 | //log.Println("selected", node.Name) 144 | case <-die: 145 | if conn != nil { 146 | conn.Close() 147 | } 148 | } 149 | }(n, cc, die) 150 | } 151 | var n c 152 | for i := 0; i < len(d.nodes); i++ { 153 | Time := new(expvar.String) 154 | n = <-cc 155 | Time.Set(time.Now().String()) 156 | if n.err == nil { 157 | n.n.exp.Add("Connections", 1) 158 | n.n.exp.Set("LastSuccess", Time) 159 | close(die) 160 | break 161 | } else { 162 | Err := new(expvar.String) 163 | Err.Set(n.err.Error()) 164 | n.n.exp.Add("Errors", 1) 165 | n.n.exp.Set("LastError", Time) 166 | n.n.exp.Set("LastErrorMessage", Err) 167 | //log.Println(n.n.Name, n.err) 168 | if n.conn != nil { 169 | n.conn.Close() 170 | } 171 | } 172 | } 173 | return n.conn, n.err 174 | } 175 | 176 | // NewDriver returns an initialized Cluster driver, using upstreamDriver as backend 177 | func NewDriver(upstreamDriver driver.Driver) Driver { 178 | m := expvar.NewMap("ClusterSql") 179 | Time := new(expvar.String) 180 | Time.Set(time.Now().String()) 181 | m.Set("FirstInstanciated", Time) 182 | cl := Driver{map[string]*node{}, upstreamDriver, m} 183 | return cl 184 | } 185 | -------------------------------------------------------------------------------- /cluster_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 by tkr@ecix.net (Peering GmbH) 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, 8 | // this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | // POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package clustersql 27 | 28 | import ( 29 | "database/sql" 30 | "fmt" 31 | "github.com/BurntSushi/toml" 32 | "github.com/go-sql-driver/mysql" 33 | "os" 34 | "sync" 35 | "sync/atomic" 36 | "testing" 37 | ) 38 | 39 | var db *sql.DB 40 | 41 | type NodeCfg struct { 42 | Name string 43 | HostName string 44 | Port int 45 | UserName string 46 | Password string 47 | DBName string 48 | } 49 | 50 | type Config struct { 51 | Nodes []NodeCfg 52 | } 53 | 54 | func TestOpen(t *testing.T) { 55 | cfgfile := os.Getenv("DBCONFIG") 56 | if cfgfile == "" { 57 | cfgfile = "config.toml" 58 | } 59 | cfg := new(Config) 60 | if f, err := os.Open(cfgfile); err != nil { 61 | t.Fatal(err, "(did you set the DBCONFIG env variable?)") 62 | } else { 63 | if _, err := toml.DecodeReader(f, cfg); err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | d := NewDriver(mysql.MySQLDriver{}) 69 | 70 | for _, ncfg := range cfg.Nodes { 71 | if ncfg.Password != "" { 72 | d.AddNode(ncfg.Name, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.Password, ncfg.HostName, ncfg.Port, ncfg.DBName)) 73 | } else { 74 | d.AddNode(ncfg.Name, fmt.Sprintf("%s@tcp(%s:%d)/%s", ncfg.UserName, ncfg.HostName, ncfg.Port, ncfg.DBName)) 75 | } 76 | } 77 | 78 | sql.Register("cluster", d) 79 | var err error 80 | db, err = sql.Open("cluster", "galera") 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | } 85 | 86 | func TestDrop(t *testing.T) { 87 | _, err := db.Exec("DROP TABLE IF EXISTS test") 88 | if err != nil { 89 | t.Fatalf(err.Error()) 90 | } 91 | } 92 | 93 | func TestCreate(t *testing.T) { 94 | _, err := db.Exec("CREATE TABLE test (value BOOL)") 95 | if err != nil { 96 | t.Fatalf(err.Error()) 97 | } 98 | } 99 | 100 | func TestEmptySelect(t *testing.T) { 101 | rows, err := db.Query("SELECT * FROM test") 102 | if err != nil { 103 | t.Fatalf(err.Error()) 104 | } 105 | if rows.Next() { 106 | t.Fatalf("unexpected data in empty table") 107 | } 108 | } 109 | 110 | func TestInsert(t *testing.T) { 111 | res, err := db.Exec("INSERT INTO test VALUES (1)") 112 | if err != nil { 113 | t.Fatalf(err.Error()) 114 | } 115 | count, err := res.RowsAffected() 116 | if err != nil { 117 | t.Fatalf("res.RowsAffected() returned error: %s", err.Error()) 118 | } 119 | if count != 1 { 120 | t.Fatalf("Expected 1 affected row, got %d", count) 121 | } 122 | id, err := res.LastInsertId() 123 | if err != nil { 124 | t.Fatalf("res.LastInsertId() returned error: %s", err.Error()) 125 | } 126 | if id != 0 { 127 | t.Fatalf("Expected InsertID 0, got %d", id) 128 | } 129 | } 130 | 131 | func TestSelect(t *testing.T) { 132 | rows, err := db.Query("SELECT value FROM test") 133 | if err != nil { 134 | t.Fatalf(err.Error()) 135 | } 136 | if rows.Next() { 137 | var out bool 138 | rows.Scan(&out) 139 | if true != out { 140 | t.Errorf("true != %t", out) 141 | } 142 | if rows.Next() { 143 | t.Error("unexpected data") 144 | } 145 | } else { 146 | t.Error("no data") 147 | } 148 | } 149 | 150 | func TestUpdate(t *testing.T) { 151 | res, err := db.Exec("UPDATE test SET value = ? WHERE value = ?", false, true) 152 | if err != nil { 153 | t.Fatalf(err.Error()) 154 | } 155 | count, err := res.RowsAffected() 156 | if err != nil { 157 | t.Fatalf("res.RowsAffected() returned error: %s", err.Error()) 158 | } 159 | if count != 1 { 160 | t.Fatalf("Expected 1 affected row, got %d", count) 161 | } 162 | 163 | rows, err := db.Query("SELECT value FROM test") 164 | if err != nil { 165 | t.Fatalf(err.Error()) 166 | } 167 | if rows.Next() { 168 | var out bool 169 | rows.Scan(&out) 170 | if false != out { 171 | t.Errorf("false != %t", out) 172 | } 173 | 174 | if rows.Next() { 175 | t.Error("unexpected data") 176 | } 177 | } else { 178 | t.Error("no data") 179 | } 180 | } 181 | 182 | func TestDelete(t *testing.T) { 183 | res, err := db.Exec("DELETE FROM test WHERE value = ?", false) 184 | if err != nil { 185 | t.Fatalf(err.Error()) 186 | } 187 | count, err := res.RowsAffected() 188 | if err != nil { 189 | t.Fatalf("res.RowsAffected() returned error: %s", err.Error()) 190 | } 191 | if count != 1 { 192 | t.Fatalf("Expected 1 affected row, got %d", count) 193 | } 194 | 195 | // Check for unexpected rows 196 | res, err = db.Exec("DELETE FROM test") 197 | if err != nil { 198 | t.Fatalf(err.Error()) 199 | } 200 | count, err = res.RowsAffected() 201 | if err != nil { 202 | t.Fatalf("res.RowsAffected() returned error: %s", err.Error()) 203 | } 204 | if count != 0 { 205 | t.Fatalf("Expected 0 affected row, got %d", count) 206 | } 207 | } 208 | 209 | func TestConcurrent(t *testing.T) { 210 | var max int 211 | err := db.QueryRow("SELECT @@max_connections").Scan(&max) 212 | if err != nil { 213 | t.Fatalf("%s", err.Error()) 214 | } 215 | // max = 100 216 | fmt.Printf("Testing up to %d concurrent connections \r\n", max) 217 | var remaining, succeeded int32 = int32(max), 0 218 | 219 | var wg sync.WaitGroup 220 | wg.Add(max) 221 | var fatalError string 222 | var once sync.Once 223 | fatalf := func(s string, vals ...interface{}) { 224 | once.Do(func() { 225 | fatalError = fmt.Sprintf(s, vals...) 226 | }) 227 | } 228 | 229 | for i := 0; i < max; i++ { 230 | go func(id int) { 231 | defer wg.Done() 232 | 233 | tx, err := db.Begin() 234 | atomic.AddInt32(&remaining, -1) 235 | fmt.Printf("%d ", remaining) 236 | 237 | if err != nil { 238 | if err.Error() != "Error 1040: Too many connections" { 239 | //fmt.Printf("Begin: Error on Conn %d: %s", id, err.Error()) 240 | fatalf("Begin: Error on Conn %d: %s", id, err.Error()) 241 | // t.Logf("Begin: Error on Conn %d: %s", id, err.Error()) 242 | } 243 | //fmt.Printf(" whoops ") 244 | return 245 | } 246 | 247 | // keep the connection busy until all connections are open 248 | for remaining > 0 { 249 | if _, err = tx.Exec("DO 1"); err != nil { 250 | //fmt.Printf("Exec: Error on Conn %d: %s", id, err.Error()) 251 | fatalf("Exec: Error on Conn %d: %s", id, err.Error()) 252 | return 253 | } 254 | } 255 | 256 | if err = tx.Commit(); err != nil { 257 | //fmt.Printf("Error on Conn %d: %s", id, err.Error()) 258 | fatalf("Error on Conn %d: %s", id, err.Error()) 259 | return 260 | } 261 | 262 | // everything went fine with this connection 263 | atomic.AddInt32(&succeeded, 1) 264 | }(i) 265 | } 266 | 267 | fmt.Println("waiting") 268 | // wait until all conections are open 269 | wg.Wait() 270 | fmt.Println("waited") 271 | 272 | if fatalError != "" { 273 | t.Fatal(fatalError) 274 | } 275 | 276 | t.Logf("Reached %d concurrent connections\r\n", succeeded) 277 | 278 | } 279 | --------------------------------------------------------------------------------