Testing with MySQL embedded by Roberto Polli Integration Testing is not always common between C developers. MySQL API offer us a fast and pratical way to do it using a full-featured, dummy sql embedded server. Obviously an embedded server has its limitations, eg: no replication; one process at a time; missing more logic (eg. mysqldump...) but you can implement it. See mysql embedded documentation for further info! The MySQL C API To access a MySQL server from a C program, we can use the API provided by MySQL. Functions are provided by the following library files. Client: /usr/lib/libmysqlclient.so.16.0.0 - standard version /usr/lib/libmysqlclient_r.so.16.0.0 - reentrant version Server embedded: /usr/lib/mysql/libmysqld.a You can use /usr/bin/mysql_config to set the build flags. That’s the standard output of #mysql_config Usage: /usr/bin/mysql_config [OPTIONS] Options: --cflags aliasing [-I/usr/include/mysql -DBIG_JOINS=1 -fno-strict- -DUNIV_LINUX -DUNIV_LINUX] --include --libs [-I/usr/include/mysql] [-Wl,-Bsymbolic-functions -rdynamic -L/usr/lib/mysql - lmysqlclient] --libs_r [-Wl,-Bsymbolic-functions -rdynamic -L/usr/lib/mysql - lmysqlclient_r] --plugindir [/usr/lib/mysql/plugin] --socket [/var/run/mysqld/mysqld.sock] BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 --port [0] --version [5.1.41] --libmysqld-libs [-Wl,-Bsymbolic-functions -rdynamic -L/usr/lib/mysql lmysqld -ldl -lwrap -lrt] Here is the anatomy of a MySQL client application: uses a shared library and the external server. My test application occupy about 200KBytes. An embedded app is bigger, because it’s statically linked with the mysqld library. It’s an almost complete MySQL server instance! And it’s faster! Basic working cycle with a MySQL Server The simplest way to connect to MySQL consists to: - initialize mysql library - connect to server - execute queries, eventually freeing result sets - disconnect BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 Here is a sample program with some assertions, that show us the expected return values of the functions. #include<assert.h> #include<stdlib.h> #include<mysql/mysql.h> int main(int argc, char**argv) { int port = 0, mysql_option_flags=0; char *host = “localhost”, *user = “mysql”, *pass = “secret”, *dbname = NULL, *unix_sock = NULL; // Initialize mysql library. This should be done *once* // moreover this function is not thread-safe, nor reentrant // so we should protect it with a global mutex assert( mysql_library_init(0, NULL, NULL) == 0 ); MYSQL * mysql = NULL; // a pointer to a mysql database structure // Inizialize mysql database structure, containing all database informations // this function is not thread-safe too assert( (mysql = mysql_init(NULL)) != NULL); // connect to the RDBMS without specifying a database name int error = mysql_real_connect(mysql, host, user, pass, dbname, port, unix_sock, mysql_option_flags); // eventually print errors if (error) goto onerror; // do our job error = mysql_query(mysql, “CREATE DATABASE pluto;”); BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 if (error) goto onerror; error = mysql_query(mysql, “USE pluto;”); if (error) goto onerror; error = mysql_query(mysql, “DROP DATABASE pluto;”); if (error) goto onerror; // disconnet and free memory and resources mysql_close(mysql); mysql_library_end(); exit(0); onerror: printf(“Error: %s\n”, mysql_error(mysql)); abort(); } Going multithreads Using multiple threads - and multiple connections - require some logic. In fact the following functions must be called at once in the process - and must be protected by a global mutex: 1. mysql_library_init() 2. mysql_init() which may call mysql_library_init() Other MySQL methods are thread safe “per-connection”: the MYSQL* pointer is associated to a reference the connection. So every MYSQL* pointer can be used by a single thread at once, requiring us to do use some locking. Let’s create the following database structure, a global mutex and some helper methods. // a locking structure for each connection typedef struct my_db { MYSQL * mysql; pthread_mutex_t lock; int lock_init; char* user; char* pass; char* host; char* dbhost; char* unix_sock; BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 int port; int options; } mysql_db; // a mutex for global calls pthread_mutex_t db_global_mutex = PTHREAD_MUTEX_INITIALIZER; The structure simply ties the database connection parameters, its MYSQL structure and a mutex that makes it thread safe. The “global mutex” avoid us to call mysql_init() in different threads. Here are some methods, the missing ones are left as an exercise to the reader. We will add a my_flush_result() method, *required* to clean resultset after queries. Please see MySQL documentation for further info about it!. Non connection-safe functions There are two ways to protect the non thread-safe functions: call them in the main thread, before spawning the others; or use our global mutex. The mysql_library_init() should be called once by EVERY thread using mysql - so it’s better to run it in the main(), before spawning threads. Similarly you have to run mysq_library_end() at the end of the process to free memory resources. In the same way, we can allocate all our connection before running the client threads - creating a connection pool and passing it to our threads. /** * creating a connection pool */ int main(int argc, char** argv) { // some variables int i=0, pool_size = 5; mysql_db my_pool[pool_size]; // once in the process, so no mutexes mysql_library_init(0, NULL, NULL); // again...no locks for (i=0; i<pool_size; i++) { BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 // fill the my_pool with credential, user, passwd.... ...initialize_db_structure(&my_pool[i]);... // initialize lock pthread_mutex_init(&my_pool[i].lock); &my_pool[i]->lock_initialized = TRUE; //initialize db &my_pool[i]->mysql = mysql_init(NULL); } … pass &my_pool[i] to threads and run them... mysql_library_end(); } Otherwise we just have to protect each call with our global mutex and run our sequence at every connection... mysql_db* my_init(mysql_db *db) { /// initialize mutex, WARN can’t initialize it twice, /// so check if you have already initialized it! if (!db->lock_initialized) { pthread_mutex_init(&db->lock, NULL); db->lock_initialized=TRUE; } /// initialize mysql structure pthread_mutex_lock(&db->lock); // don’t use this db while initializing pthread_mutex_lock(&db_global_mutex); // don’t initialize two databases contemporarily db->mysql = mysql_init(NULL); pthread_mutex_unlock(&db_global_mutex); pthread_mutex_unlock(&db->lock); return db; } BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 Connection-safe functions Connection safe functions are now easy: just lock and unlock the MYSQL* pointer in use, to have consistent values for results, counters and errors. int my_connect(mysql_db *db) { pthread_mutex_lock(&db->lock); int error = mysql_real_connect(db->mysql, db->host, db->user, db->pass, db->dbname, db->port, db->unix_sock, db->options); if (error) { printf(“Error: %s\n”, mysql_error(db->mysql)); } /// release lock *after* mysql_error pthread_mutex_unlock(&db->lock); return error; } int my_query(mysql_db* db, char*query) { pthread_mutex_lock(&db->lock); error = mysql_query(db->mysql, query); if (error) { printf(“Error: %s\n”, mysql_error(db->mysql)); } else { if (mysql_field_count(db->mysql) == 0) { rows = mysql_affected_rows(db->mysql); printf("%lld rows affected\n",rows ); } } pthread_mutex_unlock(&db->lock); return error; } /** * clean result */ int my_flush_result(mysql_db *db) { MYSQL *mysql = db->mysql; MYSQL_RES* result; int qstatus = 0; BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 pthread_mutex_lock(&db->lock); do { if ((result = mysql_store_result(mysql))) { mysql_free_result(result); } if (mysql_field_count(mysql) == 0) { printf("%lld rows affected\n",mysql_affected_rows(mysql)); } else { printf("Could not retrieve result set\n"); break; } if ((qstatus = mysql_next_result(mysql)) > 0) printf("Could not execute statement\n"); } while(qstatus == 0); pthread_mutex_unlock(&db->lock); return qstatus; } So we’ve just create a simple MySQL client! Once you’ve understood the common pitfalls, it’s not so hard! Testing with a MySQL server Once we created our simple MySQL library, we can write our client code issuing some queries and printing reports. Provided we’ve a working MySQL server, we can create some test cases. First of all we’ll create the setup() and teardown() methods to prepare our environment. void setup(mysql_db *db) { mysql_library_init(0,NULL,NULL); db = my_init(db); my_connect(db); my_query(db, “create database pippo;”); my_query(db, “use pippo;”); } void teardown(mysql_db *db) { BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 my_query(db, “drop database pippo;”); my_close(db); mysql_library_end(); } Then we create a simple test case, which issues a lot of insert. Note that this code is shortened for better readability! void test_sql_client(mysql_db *db) { int i, error; error = my_query(db, “create table t1(“ \ “id int not null auto_increment primary key”\ ”, value varchar(25));”); for (i=0; i<10000; i++) { error = my_query(db, “insert into t1(id,value) values (‘%d’,’prova%d’);”, i, i); my_flush_result(db); } } int main(int argc, char**argv) { mysql_db *db = calloc(sizeof(mysql_db)); ...set db values.... setup(db); test_sql_client(db); teardown(db); free(db); exit(EXIT_SUCCESS); } To build our program, mysql_test.c we issue #gcc mysql_test.c -o mysql_test $(mysql_config --libs) and with time we’ll see how long will it take to run 10k insert. Note that this will include setup and teardown time. Writing a more complete test is left as an exercise to the reader ;) BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 # time ./mysql_test What’s MySQL embedded To run the previous file, named mysql_test.c, we need a working MySQL instance. Instead if we don’t have it - or we like more isolation and speed, we could use the MySQL embedded. Using MySQL embedded means you don’t need to connect to a server, but you’ll write directly into the MySQL database files. And it’s fast! To use MySQL embedded, you just need to link your file with the mysqld library: #gcc mysql_test.c -o mysql_test $(mysql_config --libmysqld-libs) The server instructions will be added to your program, which will increase its size. While the core client code remains the same, you need to modify a bit your initialization structure, passing some parameters to the mysql_library_init() function. Those parameters are the same that you would pass to mysqld server process, eg. --datadir=path. If you have never run MySQL server by command line, or you’re not familiar with my.conf please, take some time to play with it before continuing. In our case we should only modify the setup() function to reflect our changes. Before that we need to sketch the MySQL database directory structure. MySQL directory structure MySQL stores databases and authentication information in the /var/lib/mysql directory. The forementioned database would have the following structure: /var/lib/mysql/ ├── mysql <- the core database containing user/password credentials ├── pippo <- a newly created database By default, MySQL embedded will use the /var/lib/mysql directory, so interfering with our server and requiring RW access to /var/lib/mysql. This is usually not what we want. Instead we want to put everything in another place, eg. a temporary directory under /tmp/. So when we run the embedded server, we should create a new directory and tell it to bootstrap there a new MySQL installation. Let’s do it. BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 Bootstrapping MySQL embedded To run the embedded server we need to provide: a directory where to store the new mysql databases; the parametes to run the embedded server; the server groups. Server groups are the stanzas of my.conf file from which mysqld - and our embedded application read configuration options. A typical my.conf files has the following structure: [mysqld] ...options for server... [embedded] ...options for the embedded server... [server] ...options for server... To specify custom parameters relative to our application, we could add the following sections to our my.conf, or - with more isolation - use another my.conf containing the specified sections. [embedded] ...options for *all* embedded applications... key_buffer_size=32M [xxx_SERVER] ...replace xxx with program name (eg. argv[0]) and add options for this specific program … datadir=/tmp/ [xxx_CLIENT] ...replace xxx with program name (eg. argv[0]) and add options for this specific program... datadir=/tmp We can even specify those parameters as arguments to the mysql_server_init() BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 void embedded_mysql_library_init(char* progname) { /// create a temporary directory where to store the db char *datadir = strdup("/tmp/mysqlembedXXXXXX"); assert( mkdtemp(datadir)!=NULL ); /// prepare server parameters const char *server_options[] = { progname, /* this string is not used */ "-h", datadir, "--key_buffer_size=32M", "--bootstrap", // create the required mysql files NULL }; int num_elements = sizeof(server_options) / sizeof(char *) -1; /// create server groups for my.cnf sprintf(progname_client, "%s_CLIENT", progname); sprintf(progname_server, "%s_SERVER", progname); char *server_groups[] = { progname_server, progname_client, "embedded", NULL // end of arguments }; /// initialize the embedded server assert(mysql_library_init(num_elements, (char**) server_options, server_groups) == 0); } void embedded_my_init(mysql_db *db) { pthread_mutex_lock(&db->lock, NULL); /// initialize and set options assert( (mysql = mysql_init(db->mysql)) != NULL ); assert( mysql_options(db->mysql, MYSQL_READ_DEFAULT_GROUP, progname_client) == 0); assert( mysql_options(db->mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL) == 0); BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 pthread_mutex_unlock(&db->lock, NULL); return db; } With our new methods, the setup function become: void setup(mysql_db *db, char* progname) { embedded_mysql_library_init(progname); db = embeded_my_init(db); // from here everything is the same my_connect(db); my_query(db, “create database pippo;”); my_query(db, “use pippo;”); } And our new main(): int main(int argc, char**argv) { mysql_db *db = calloc(sizeof(mysql_db)); ...set db values.... setup(db, argv[0]); test_sql_client(db); teardown(db); exit(EXIT_SUCCESS); } Replacing the old one with this one, everything will work nicely in both cases. When you link against libmysqlclient, all the different initialization variables - only applicable to embedded server - are ignored. Duplicate test files with embedded sql To run both tests you have to compile twice your software: #gcc mysql_test.c -o mysql_server_test $(mysql_config --libs) BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 #gcc mysql_test.c -o mysql_embedded_test $(mysql_config --libmysqld-libs) Now you have two executables, one using the client connection, one the embedded server. Here are some speed results for 50k INSERTs run locally on my Laptop. You’re welcome to provide further stats for SELECT, UPDATE, INSERT or UPDATE and TRANSACTIONS! CPU: 2 x Intel(R) Core(TM)2 Duo CPU @2.1Ghz RAM: 8Gb MySQL Server InnoDB 21 sec MySQL Server MemoryDB 7sec MySQL Embedded InnoDB 5.7 sec MySQL Embedded MemoryDB 5.6 sec BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015 Licenza d'uso “Attribuzione - Non commerciale - Non opere derivate”, secondo i criteri internazionali Creative Commons (http://creativecommons.org/licenses/by-nc-nd/2.5/it/) BABEL S.r.l. - P.zza S.Benedetto da Norcia 33 - 00040, Pomezia (RM) - Tel:. +39 06.9826.9600 - Fax. +39 06.9826.9680 E-Mail: [email protected] – PEC: [email protected] – WEB: http://www.babel.it Res. Imprese di Roma N° 06062681009 - N.R.E.A. 953123 - P.I. e C.F. 06062681009 Cap. Soc. € 102.774,00 i.v. Società soggetta alla direzione e coordinamento della Par-tec S.p.A - Società iscritta al registro delle Imprese di Milano al numero 1293820015