From 1601be45654c6313c7263a9a8c69b4bdb54b3ff2 Mon Sep 17 00:00:00 2001 From: "Yang,Liming" Date: Mon, 18 Feb 2019 10:56:59 +0800 Subject: [PATCH] Add Mysql Protocol, only support text protocol now, not support transaction, not support prepare statement. issue #209 --- docs/cn/mysql_client.md | 562 ++++++++ docs/images/mysql_memory.png | Bin 0 -> 8711 bytes docs/images/mysql_select.png | Bin 0 -> 107811 bytes docs/images/mysqlclient_select.png | Bin 0 -> 46453 bytes example/mysql_c++/CMakeLists.txt | 148 ++ example/mysql_c++/mysql_cli.cpp | 168 +++ example/mysql_c++/mysql_go_press.go | 63 + example/mysql_c++/mysql_press.cpp | 237 ++++ example/mysql_c++/mysql_stmt.cpp | 204 +++ example/mysql_c++/mysql_tx.cpp | 116 ++ example/mysql_c++/mysqlclient_press.cpp | 239 ++++ src/brpc/controller.cpp | 38 +- src/brpc/controller.h | 16 + .../details/controller_private_accessor.h | 16 + src/brpc/global.cpp | 15 + src/brpc/mysql.cpp | 695 ++++++++++ src/brpc/mysql.h | 286 ++++ src/brpc/mysql_command.cpp | 260 ++++ src/brpc/mysql_command.h | 92 ++ src/brpc/mysql_common.cpp | 86 ++ src/brpc/mysql_common.h | 419 ++++++ src/brpc/mysql_reply.cpp | 1189 +++++++++++++++++ src/brpc/mysql_reply.h | 801 +++++++++++ src/brpc/mysql_statement.cpp | 75 ++ src/brpc/mysql_statement.h | 66 + src/brpc/mysql_statement_inl.h | 58 + src/brpc/mysql_transaction.cpp | 113 ++ src/brpc/mysql_transaction.h | 89 ++ src/brpc/options.proto | 1 + src/brpc/policy/mysql_auth_hash.cpp | 227 ++++ src/brpc/policy/mysql_auth_hash.h | 50 + src/brpc/policy/mysql_authenticator.cpp | 176 +++ src/brpc/policy/mysql_authenticator.h | 88 ++ src/brpc/policy/mysql_protocol.cpp | 416 ++++++ src/brpc/policy/mysql_protocol.h | 52 + src/brpc/socket.cpp | 5 +- src/brpc/socket.h | 4 + test/brpc_mysql_unittest.cpp | 852 ++++++++++++ 38 files changed, 7916 insertions(+), 6 deletions(-) create mode 100644 docs/cn/mysql_client.md create mode 100644 docs/images/mysql_memory.png create mode 100644 docs/images/mysql_select.png create mode 100644 docs/images/mysqlclient_select.png create mode 100644 example/mysql_c++/CMakeLists.txt create mode 100644 example/mysql_c++/mysql_cli.cpp create mode 100644 example/mysql_c++/mysql_go_press.go create mode 100644 example/mysql_c++/mysql_press.cpp create mode 100644 example/mysql_c++/mysql_stmt.cpp create mode 100644 example/mysql_c++/mysql_tx.cpp create mode 100644 example/mysql_c++/mysqlclient_press.cpp create mode 100644 src/brpc/mysql.cpp create mode 100644 src/brpc/mysql.h create mode 100644 src/brpc/mysql_command.cpp create mode 100644 src/brpc/mysql_command.h create mode 100644 src/brpc/mysql_common.cpp create mode 100644 src/brpc/mysql_common.h create mode 100644 src/brpc/mysql_reply.cpp create mode 100644 src/brpc/mysql_reply.h create mode 100644 src/brpc/mysql_statement.cpp create mode 100644 src/brpc/mysql_statement.h create mode 100644 src/brpc/mysql_statement_inl.h create mode 100644 src/brpc/mysql_transaction.cpp create mode 100644 src/brpc/mysql_transaction.h create mode 100644 src/brpc/policy/mysql_auth_hash.cpp create mode 100644 src/brpc/policy/mysql_auth_hash.h create mode 100644 src/brpc/policy/mysql_authenticator.cpp create mode 100644 src/brpc/policy/mysql_authenticator.h create mode 100644 src/brpc/policy/mysql_protocol.cpp create mode 100644 src/brpc/policy/mysql_protocol.h create mode 100644 test/brpc_mysql_unittest.cpp diff --git a/docs/cn/mysql_client.md b/docs/cn/mysql_client.md new file mode 100644 index 0000000000..a7efe88a26 --- /dev/null +++ b/docs/cn/mysql_client.md @@ -0,0 +1,562 @@ +[MySQL](https://www.mysql.com/)是著名的开源的关系型数据库,为了使用户更快捷地访问mysql并充分利用bthread的并发能力,brpc直接支持mysql协议。示例程序:[example/mysql_c++](https://github.com/brpc/brpc/tree/master/example/mysql_c++/) + +**注意**:只支持MySQL 4.1 及之后的版本的文本协议,支持事务,不支持Prepared statement。目前支持的鉴权方式为mysql_native_password,使用事务的时候不支持single模式。 + +相比使用[libmysqlclient](https://dev.mysql.com/downloads/connector/c/)(官方client)的优势有: + +- 线程安全。用户不需要为每个线程建立独立的client。 +- 支持同步、异步、半同步等访问方式,能使用[ParallelChannel等](combo_channel.md)组合访问方式。 +- 支持多种[连接方式](client.md#连接方式)。支持超时、backup request、取消、tracing、内置服务等一系列brpc提供的福利。 +- 明确的返回类型校验,如果使用了不正确的变量接受mysql的数据类型,将抛出异常。 +- 调用mysql标准库会阻塞框架的并发能力,使用本实现将能充分利用brpc框架的并发能力。 +- 使用brpc实现的mysql不会造成pthread的阻塞,使用libmysqlclient会阻塞pthread [线程相关](bthread.md),使用mysql的异步api会使编程变得很复杂。 +# 访问mysql + +创建一个访问mysql的Channel: + +```c++ +# include +# include +# include + +brpc::ChannelOptions options; +options.protocol = brpc::PROTOCOL_MYSQL; +options.connection_type = FLAGS_connection_type; +options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; +options.max_retry = FLAGS_max_retry; +options.auth = new brpc::policy::MysqlAuthenticator("yangliming01", "123456", "test", + "charset=utf8&collation_connection=utf8_unicode_ci"); +if (channel.Init("127.0.0.1", 3306, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; +} +``` + +向mysql发起命令。 + +```c++ +// 执行各种mysql命令,可以批量执行命令如:"select * from tab1;select * from tab2" +std::string command = "show databases"; // select,delete,update,insert,create,drop ... +brpc::MysqlRequest request; +if (!request.Query(command)) { + LOG(ERROR) << "Fail to add command"; + return false; +} +brpc::MysqlResponse response; +brpc::Controller cntl; +channel.CallMethod(NULL, &cntl, &request, &response, NULL); +if (!cntl.Failed()) { + std::cout << response << std::endl; +} else { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return false; +} +return true; +``` + +上述代码的说明: + +- 请求类型必须为MysqlRequest,回复类型必须为MysqlResponse,否则CallMethod会失败。不需要stub,直接调用channel.CallMethod,method填NULL。 +- 调用request.Query()传入要执行的命令,可以批量执行命令,多个命令用分号隔开。 +- 依次调用response.reply(X)弹出操作结果,根据返回类型的不同,选择不同的类型接收,如:MysqlReply::Ok,MysqlReply::Error,const MysqlReply::Columnconst MysqlReply::Row等。 +- 如果只有一条命令则reply为1个,如果为批量操作返回的reply为多个。 + +目前支持的请求操作有: + +```c++ +bool Query(const butil::StringPiece& command); +``` + +对应的回复操作: + +```c++ +// 返回不同类型的结果 +const MysqlReply::Auth& auth() const; +const MysqlReply::Ok& ok() const; +const MysqlReply::Error& error() const; +const MysqlReply::Eof& eof() const; +// 对result set结果集的操作 +// get column number +uint64_t MysqlReply::column_number() const; +// get one column +const MysqlReply::Column& MysqlReply::column(const uint64_t index) const; +// get row number +uint64_t MysqlReply::row_number() const; +// get one row +const MysqlReply::Row& MysqlReply::next() const; +// 结果集中每个字段的操作 +const MysqlReply::Field& MysqlReply::Row::field(const uint64_t index) const; +``` + +# 事务操作 + +事务可以保证在一个事务中的多个RPC请求最终要么都成功,要么都失败。 + +```c++ +rpc::Channel channel; +// Initialize the channel, NULL means using default options. +brpc::ChannelOptions options; +options.protocol = brpc::PROTOCOL_MYSQL; +options.connection_type = FLAGS_connection_type; +options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; +options.connect_timeout_ms = FLAGS_connect_timeout_ms; +options.max_retry = FLAGS_max_retry; +options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params); +if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; +} + +// create transaction +brpc::MysqlTransactionOptions options; +options.readonly = FLAGS_readonly; +options.isolation_level = brpc::MysqlIsolationLevel(FLAGS_isolation_level); +auto tx(brpc::NewMysqlTransaction(channel, options)); +if (tx == NULL) { + LOG(ERROR) << "Fail to create transaction"; + return false; +} + +brpc::MysqlRequest request(tx.get()); +if (!request.Query(*it)) { + LOG(ERROR) << "Fail to add command"; + tx->rollback(); + return false; +} +brpc::MysqlResponse response; +brpc::Controller cntl; +channel.CallMethod(NULL, &cntl, &request, &response, NULL); +if (cntl.Failed()) { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + tx->rollback(); + return false; +} +// handle response +std::cout << response << std::endl; +bool rc = tx->commit(); +``` + +# Prepared Statement + +Prepared statement对于一个需要执行很多次的SQL语句,它把这个SQL语句注册到mysql-server,避免了每次请求在mysql-server端都去解析这个SQL语句,能得到性能上的提升。 + +```c++ +rpc::Channel channel; +// Initialize the channel, NULL means using default options. +brpc::ChannelOptions options; +options.protocol = brpc::PROTOCOL_MYSQL; +options.connection_type = FLAGS_connection_type; +options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; +options.connect_timeout_ms = FLAGS_connect_timeout_ms; +options.max_retry = FLAGS_max_retry; +options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params); +if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; +} + +auto stmt(brpc::NewMysqlStatement(channel, "select * from tb where name=?")); +if (stmt == NULL) { + LOG(ERROR) << "Fail to create mysql statement"; + return -1; +} + +brpc::MysqlRequest request(stmt.get()); +if (!request.AddParam("lilei")) { + LOG(ERROR) << "Fail to add name param"; + return NULL; +} + +brpc::MysqlResponse response; +brpc::Controller cntl; +channel->CallMethod(NULL, &cntl, &request, &response, NULL); +if (cntl.Failed()) { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return NULL; +} + +std::cout << response << std::endl; +``` + + + +# 性能测试 + +我在example/mysql_c++目录下面写了两个测试程序,mysql_press.cpp mysqlclient_press.cpp,mysql_go_press.go 一个是使用了brpc框架,一个是使用了的libmysqlclient访问mysql,一个是使用[go-sql-driver](https://github.com/go-sql-driver)/**go-mysql**访问mysql + +启动单线程测试 + +##### brpc框架访问mysql(单线程) + +./mysql_press -thread_num=1 -op_type=0 // insert + +``` +qps=3071 latency=320 +qps=3156 latency=311 +qps=3166 latency=310 +qps=3151 latency=312 +qps=3093 latency=317 +qps=3146 latency=312 +qps=3139 latency=313 +qps=3114 latency=315 +qps=3055 latency=321 +qps=3135 latency=313 +qps=2611 latency=376 +qps=3072 latency=320 +qps=3026 latency=324 +qps=2792 latency=352 +qps=3181 latency=309 +qps=3181 latency=309 +qps=3197 latency=307 +qps=3024 latency=325 +``` + +./mysql_press -thread_num=1 -op_type=1 + +``` +qps=6414 latency=151 +qps=5292 latency=182 +qps=6700 latency=144 +qps=6858 latency=141 +qps=6915 latency=140 +qps=6822 latency=142 +qps=6722 latency=144 +qps=6852 latency=141 +qps=6713 latency=144 +qps=6741 latency=144 +qps=6734 latency=144 +qps=6611 latency=146 +qps=6554 latency=148 +qps=6810 latency=142 +qps=6787 latency=143 +qps=6737 latency=144 +qps=6579 latency=147 +qps=6634 latency=146 +qps=6716 latency=144 +qps=6711 latency=144 +``` + +./mysql_press -thread_num=1 -op_type=2 // update + +``` +qps=3090 latency=318 +qps=3452 latency=284 +qps=3239 latency=303 +qps=3328 latency=295 +qps=3218 latency=305 +qps=3251 latency=302 +qps=2516 latency=391 +qps=2874 latency=342 +qps=3366 latency=292 +qps=3249 latency=302 +qps=3346 latency=294 +qps=3486 latency=282 +qps=3457 latency=284 +qps=3439 latency=286 +qps=3386 latency=290 +qps=3352 latency=293 +qps=3253 latency=302 +qps=3341 latency=294 +``` + +##### libmysqlclient实现(单线程) + +./mysqlclient_press -thread_num=1 -op_type=0 // insert + +``` +qps=3166 latency=313 +qps=3157 latency=314 +qps=2941 latency=337 +qps=3270 latency=303 +qps=3305 latency=300 +qps=3445 latency=287 +qps=3455 latency=287 +qps=3449 latency=287 +qps=3486 latency=284 +qps=3551 latency=279 +qps=3517 latency=281 +qps=3283 latency=302 +qps=3353 latency=295 +qps=2564 latency=386 +qps=3243 latency=305 +qps=3333 latency=297 +qps=3598 latency=275 +qps=3714 latency=267 +``` + +./mysqlclient_press -thread_num=1 -op_type=1 + +``` +qps=8209 latency=120 +qps=8022 latency=123 +qps=7879 latency=125 +qps=8083 latency=122 +qps=8504 latency=116 +qps=8112 latency=121 +qps=8278 latency=119 +qps=8698 latency=113 +qps=8817 latency=112 +qps=8755 latency=112 +qps=8734 latency=113 +qps=8390 latency=117 +qps=8230 latency=120 +qps=8486 latency=116 +qps=8038 latency=122 +qps=8640 latency=114 +``` + +./mysqlclient_press -thread_num=1 -op_type=2 // update + +``` +qps=3583 latency=276 +qps=3530 latency=280 +qps=3610 latency=274 +qps=3492 latency=283 +qps=3508 latency=282 +qps=3465 latency=286 +qps=3543 latency=279 +qps=3610 latency=274 +qps=3567 latency=278 +qps=3381 latency=293 +qps=3514 latency=282 +qps=3461 latency=286 +qps=3456 latency=286 +qps=3517 latency=281 +qps=3492 latency=284 +``` + +##### golang访问mysql(单线程) + +go run test.go -thread_num=1 + +``` +qps = 6905 latency = 144 +qps = 6922 latency = 143 +qps = 6931 latency = 143 +qps = 6998 latency = 142 +qps = 6780 latency = 146 +qps = 6980 latency = 142 +qps = 6901 latency = 144 +qps = 6887 latency = 144 +qps = 6943 latency = 143 +qps = 6880 latency = 144 +qps = 6815 latency = 146 +qps = 6089 latency = 163 +qps = 6626 latency = 150 +qps = 6361 latency = 156 +qps = 6783 latency = 146 +qps = 6789 latency = 146 +qps = 6883 latency = 144 +qps = 6795 latency = 146 +qps = 6724 latency = 148 +qps = 6861 latency = 145 +qps = 6878 latency = 144 +qps = 6842 latency = 146 +``` + +从以上测试结果看来,使用brpc实现的mysql协议和使用libmysqlclient在插入、修改、删除操作上性能是类似的,但是在查询操作看会逊色于libmysqlclient,查询的性能和golang实现的mysql类似。 + +##### brpc框架访问mysql(50线程) + +./mysql_press -thread_num=50 -op_type=1 -use_bthread=true + +``` +qps=18843 latency=2656 +qps=22426 latency=2226 +qps=22536 latency=2203 +qps=22560 latency=2193 +qps=22270 latency=2226 +qps=22302 latency=2247 +qps=22147 latency=2225 +qps=22517 latency=2228 +qps=22762 latency=2176 +qps=23061 latency=2162 +qps=23819 latency=2070 +qps=23852 latency=2077 +qps=22682 latency=2214 +qps=22381 latency=2213 +qps=24041 latency=2069 +qps=24562 latency=2022 +qps=24874 latency=2004 +qps=24821 latency=1988 +qps=24209 latency=2073 +qps=21706 latency=2281 +``` + +##### libmysqlclient实现(50线程) + +./mysql_press -thread_num=50 -op_type=1 -use_bthread=true + +``` +qps=23656 latency=378 +qps=16190 latency=555 +qps=20136 latency=445 +qps=22238 latency=401 +qps=22229 latency=403 +qps=19109 latency=470 +qps=22569 latency=394 +qps=26250 latency=343 +qps=28208 latency=318 +qps=29649 latency=301 +qps=29874 latency=301 +qps=30033 latency=301 +qps=25911 latency=345 +qps=28048 latency=317 +qps=27398 latency=329 +``` + +##### golang访问mysql(50协程) + +go run ../mysql_go_press.go -thread_num=50 + +``` +qps = 23660 latency = 2049 +qps = 23198 latency = 2160 +qps = 23765 latency = 2181 +qps = 23323 latency = 2149 +qps = 14833 latency = 2136 +qps = 23822 latency = 2853 +qps = 20389 latency = 2474 +qps = 23290 latency = 2151 +qps = 23526 latency = 2153 +qps = 21426 latency = 2613 +qps = 23339 latency = 2155 +qps = 25623 latency = 2084 +qps = 23048 latency = 2210 +qps = 20694 latency = 2423 +qps = 23705 latency = 2122 +qps = 23445 latency = 2125 +qps = 24368 latency = 2054 +qps = 23027 latency = 2175 +qps = 24307 latency = 2063 +qps = 23227 latency = 2096 +qps = 23646 latency = 2173 +``` + +以上是启动50并发的查询请求,看上去qps都比较相似,但是libmysqlclient延时明显低。 + +##### brpc框架访问mysql(100线程) + +./mysql_press -thread_num=100 -op_type=1 -use_bthread=true + +``` +qps=26428 latency=3764 +qps=26305 latency=3780 +qps=26390 latency=3779 +qps=26278 latency=3787 +qps=26326 latency=3787 +qps=26266 latency=3792 +qps=26394 latency=3773 +qps=26263 latency=3797 +qps=26250 latency=3783 +qps=26362 latency=3782 +qps=26212 latency=3796 +qps=26260 latency=3800 +qps=24666 latency=4035 +qps=25569 latency=3896 +qps=26223 latency=3794 +qps=25538 latency=3890 +qps=20065 latency=4958 +qps=23023 latency=4331 +qps=25808 latency=3875 +``` + +##### libmysqlclient实现(100线程) + +./mysql_press -thread_num=50 -op_type=1 -use_bthread=true + +``` +qps=29467 latency=304 +qps=29413 latency=305 +qps=29459 latency=304 +qps=29562 latency=302 +qps=30657 latency=291 +qps=30445 latency=295 +qps=30179 latency=298 +qps=30072 latency=297 +qps=29802 latency=299 +qps=29752 latency=301 +qps=29701 latency=304 +qps=29731 latency=301 +qps=29622 latency=299 +qps=29440 latency=304 +qps=29495 latency=306 +qps=29297 latency=303 +qps=29626 latency=306 +qps=29482 latency=300 +qps=28649 latency=313 +qps=29537 latency=305 +qps=29634 latency=299 +``` + +##### golang访问mysql(100协程) + +go run ../mysql_go_press.go -thread_num=100 + +``` +qps = 22108 latency = 4553 +qps = 21930 latency = 4536 +qps = 20653 latency = 4906 +qps = 22100 latency = 4443 +qps = 21091 latency = 4850 +qps = 21718 latency = 4600 +qps = 21444 latency = 4488 +qps = 17832 latency = 5859 +qps = 18296 latency = 5378 +qps = 20463 latency = 4963 +qps = 21611 latency = 4880 +qps = 18441 latency = 5424 +qps = 20731 latency = 4834 +qps = 20611 latency = 4837 +qps = 20188 latency = 4979 +qps = 15450 latency = 5723 +qps = 20927 latency = 5328 +qps = 19893 latency = 5027 +qps = 21080 latency = 4782 +qps = 20192 latency = 4970 +``` + +以上是启动100并发的查询请求,看上去qps都比较相似,但是libmysqlclient延时明显低。 + +并发调整到150的时候,mysql-server已经报错"Too many connections"。 + +为什么并发数50或者100的时候libmysqlclient的延时会那么低呢?因为libmysqlclient使用的IO模式为阻塞模式,我们运行的mysql_press和mysqlclient_press都是使用的bthread模式(-use_bthread=true),底层默认都是9个pthread,使用阻塞模式的libmysqlclient和mysql交互的相当于并发度是9个线程,mysql会启动9个线程,使用非阻塞模式的rpc访问mysql并发度相当于100个,mysql会启动100个线程,所以会造成mysql的频繁上线文切换。 + +如果将libmysqlclient的执行方式改为不使用bthread,那么100个线程的执行效果为如下: + +``` +qps=26919 latency=1927 +qps=27155 latency=2037 +qps=28054 latency=1784 +qps=26738 latency=1856 +qps=27807 latency=1781 +qps=26734 latency=1730 +qps=26562 latency=1939 +qps=27473 latency=1845 +qps=26677 latency=1806 +qps=27369 latency=1948 +qps=27955 latency=1618 +qps=26574 latency=2151 +qps=27343 latency=1777 +qps=26705 latency=1822 +qps=26668 latency=1807 +qps=25347 latency=2104 +qps=26651 latency=1560 +qps=27815 latency=1979 +qps=27221 latency=1762 +qps=26516 latency=2017 +``` + +这个结果就和brpc框架启动100个bthread访问mysql的效果类似了。 + +##### 内存使用 + +在内存占用上,mysql_press和mysqlclient_press都运行了一个晚上,两个程序的内存占用 + +![libmysqlclient](../images/mysql_memory.png) + + + +以上为我的一些简单测试,以及一些简单的分析,在低并发的情况下同步IO的效率高于异步IO,可以阅读[IO相关的内容](io.md)有更多解释,后续还将继续分析性能问题,优化协议,给出更多测试。 \ No newline at end of file diff --git a/docs/images/mysql_memory.png b/docs/images/mysql_memory.png new file mode 100644 index 0000000000000000000000000000000000000000..bf7fe1caa9014ade5e69c80b9dbc30c1699aee33 GIT binary patch literal 8711 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%0D00H%AXZ5aw>rWB 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 M9kRSQn_$dulE!YdFnD?YbQC*s6PhdJ+Y$o9(` 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 MKqmJEAS$ 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/mysqlclient_select.png b/docs/images/mysqlclient_select.png new file mode 100644 index 0000000000000000000000000000000000000000..508567bbe586d1ecdb6f56538c6dfb91b449d964 GIT binary patch literal 46453 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 literal 0 HcmV?d00001 diff --git a/example/mysql_c++/CMakeLists.txt b/example/mysql_c++/CMakeLists.txt new file mode 100644 index 0000000000..1e0b953180 --- /dev/null +++ b/example/mysql_c++/CMakeLists.txt @@ -0,0 +1,148 @@ +cmake_minimum_required(VERSION 2.8.10) +project(mysql_c++ C CXX) + +# Install dependencies: +# With apt: +# sudo apt-get install libreadline-dev +# sudo apt-get install ncurses-dev +# With yum: +# sudo yum install readline-devel +# sudo yum install ncurses-devel + +option(EXAMPLE_LINK_SO "Whether examples are linked dynamically" OFF) + +execute_process( + COMMAND bash -c "find ${PROJECT_SOURCE_DIR}/../.. -type d -regex \".*output/include$\" | head -n1 | xargs dirname | tr -d '\n'" + OUTPUT_VARIABLE OUTPUT_PATH +) + +set(CMAKE_PREFIX_PATH ${OUTPUT_PATH}) + +include(FindThreads) +include(FindProtobuf) + +# Search for libthrift* by best effort. If it is not found and brpc is +# compiled with thrift protocol enabled, a link error would be reported. +find_library(THRIFT_LIB NAMES thrift) +if (NOT THRIFT_LIB) + set(THRIFT_LIB "") +endif() +find_library(THRIFTNB_LIB NAMES thriftnb) +if (NOT THRIFTNB_LIB) + set(THRIFTNB_LIB "") +endif() + +find_path(BRPC_INCLUDE_PATH NAMES brpc/server.h) +if(EXAMPLE_LINK_SO) + find_library(BRPC_LIB NAMES brpc) +else() + find_library(BRPC_LIB NAMES libbrpc.a brpc) +endif() +if((NOT BRPC_INCLUDE_PATH) OR (NOT BRPC_LIB)) + message(FATAL_ERROR "Fail to find brpc") +endif() +include_directories(${BRPC_INCLUDE_PATH}) + +find_path(GFLAGS_INCLUDE_PATH gflags/gflags.h) +find_library(GFLAGS_LIBRARY NAMES gflags libgflags) +if((NOT GFLAGS_INCLUDE_PATH) OR (NOT GFLAGS_LIBRARY)) + message(FATAL_ERROR "Fail to find gflags") +endif() +include_directories(${GFLAGS_INCLUDE_PATH}) + +execute_process( + COMMAND bash -c "grep \"namespace [_A-Za-z0-9]\\+ {\" ${GFLAGS_INCLUDE_PATH}/gflags/gflags_declare.h | head -1 | awk '{print $2}' | tr -d '\n'" + OUTPUT_VARIABLE GFLAGS_NS +) +if(${GFLAGS_NS} STREQUAL "GFLAGS_NAMESPACE") + execute_process( + COMMAND bash -c "grep \"#define GFLAGS_NAMESPACE [_A-Za-z0-9]\\+\" ${GFLAGS_INCLUDE_PATH}/gflags/gflags_declare.h | head -1 | awk '{print $3}' | tr -d '\n'" + OUTPUT_VARIABLE GFLAGS_NS + ) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + include(CheckFunctionExists) + CHECK_FUNCTION_EXISTS(clock_gettime HAVE_CLOCK_GETTIME) + if(NOT HAVE_CLOCK_GETTIME) + set(DEFINE_CLOCK_GETTIME "-DNO_CLOCK_GETTIME_IN_MAC") + endif() +endif() + +set(CMAKE_CPP_FLAGS "${DEFINE_CLOCK_GETTIME} -DGFLAGS_NS=${GFLAGS_NS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CPP_FLAGS} -DNDEBUG -O2 -D__const__= -pipe -W -Wall -Wno-unused-parameter -fPIC -fno-omit-frame-pointer") + +if(CMAKE_VERSION VERSION_LESS "3.1.3") + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + endif() + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + endif() +else() + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +find_path(LEVELDB_INCLUDE_PATH NAMES leveldb/db.h) +find_library(LEVELDB_LIB NAMES leveldb) +if ((NOT LEVELDB_INCLUDE_PATH) OR (NOT LEVELDB_LIB)) + message(FATAL_ERROR "Fail to find leveldb") +endif() +include_directories(${LEVELDB_INCLUDE_PATH}) + +find_library(SSL_LIB NAMES ssl) +if (NOT SSL_LIB) + message(FATAL_ERROR "Fail to find ssl") +endif() + +find_library(CRYPTO_LIB NAMES crypto) +if (NOT CRYPTO_LIB) + message(FATAL_ERROR "Fail to find crypto") +endif() + +# find_path(MYSQL_INCLUDE_PATH NAMES mysql/mysql.h) +# find_library(MYSQL_LIB NAMES mysqlclient) +# if (NOT MYSQL_LIB) +# message(FATAL_ERROR "Fail to find mysqlclient") +# endif() +# include_directories(${MYSQL_INCLUDE_PATH}) + +set(DYNAMIC_LIB + ${CMAKE_THREAD_LIBS_INIT} + ${GFLAGS_LIBRARY} + ${PROTOBUF_LIBRARIES} + ${LEVELDB_LIB} + ${SSL_LIB} + ${CRYPTO_LIB} + ${THRIFT_LIB} + ${THRIFTNB_LIB} +# ${MYSQL_LIB} + dl + ) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(DYNAMIC_LIB ${DYNAMIC_LIB} + pthread + "-framework CoreFoundation" + "-framework CoreGraphics" + "-framework CoreData" + "-framework CoreText" + "-framework Security" + "-framework Foundation" + "-Wl,-U,_MallocExtension_ReleaseFreeMemory" + "-Wl,-U,_ProfilerStart" + "-Wl,-U,_ProfilerStop") +endif() + +add_executable(mysql_cli mysql_cli.cpp) +add_executable(mysql_tx mysql_tx.cpp) +add_executable(mysql_stmt mysql_stmt.cpp) +add_executable(mysql_press mysql_press.cpp) +# add_executable(mysqlclient_press mysqlclient_press.cpp) + +set(AUX_LIB readline ncurses) +target_link_libraries(mysql_cli ${BRPC_LIB} ${DYNAMIC_LIB} ${AUX_LIB}) +target_link_libraries(mysql_tx ${BRPC_LIB} ${DYNAMIC_LIB}) +target_link_libraries(mysql_stmt ${BRPC_LIB} ${DYNAMIC_LIB}) +target_link_libraries(mysql_press ${BRPC_LIB} ${DYNAMIC_LIB}) +# target_link_libraries(mysqlclient_press ${BRPC_LIB} ${DYNAMIC_LIB}) diff --git a/example/mysql_c++/mysql_cli.cpp b/example/mysql_c++/mysql_cli.cpp new file mode 100644 index 0000000000..776f847a9c --- /dev/null +++ b/example/mysql_c++/mysql_cli.cpp @@ -0,0 +1,168 @@ +// Copyright (c) 2014 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A brpc based command-line interface to talk with mysql-server + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DEFINE_string(connection_type, "pooled", "Connection type. Available values: pooled, short"); +DEFINE_string(server, "127.0.0.1", "IP Address of server"); +DEFINE_int32(port, 3306, "Port of server"); +DEFINE_string(user, "brpcuser", "user name"); +DEFINE_string(password, "12345678", "password"); +DEFINE_string(schema, "brpc_test", "schema"); +DEFINE_string(params, "", "params"); +DEFINE_string(collation, "utf8mb4_general_ci", "collation"); +DEFINE_int32(timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(connect_timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(max_retry, 0, "Max retries(not including the first RPC)"); + +namespace brpc { +const char* logo(); +} + +// Send `command' to mysql-server via `channel' +static bool access_mysql(brpc::Channel& channel, const char* command) { + brpc::MysqlRequest request; + if (!request.Query(command)) { + LOG(ERROR) << "Fail to add command"; + return false; + } + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (!cntl.Failed()) { + std::cout << response << std::endl; + } else { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return false; + } + return true; +} + +// For freeing the memory returned by readline(). +struct Freer { + void operator()(char* mem) { + free(mem); + } +}; + +static void dummy_handler(int) {} + +// The getc for readline. The default getc retries reading when meeting +// EINTR, which is not what we want. +static bool g_canceled = false; +static int cli_getc(FILE* stream) { + int c = getc(stream); + if (c == EOF && errno == EINTR) { + g_canceled = true; + return '\n'; + } + return c; +} + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true); + + // A Channel represents a communication line to a Server. Notice that + // Channel is thread-safe and can be shared by all threads in your program. + brpc::Channel channel; + + // Initialize the channel, NULL means using default options. + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; + options.connect_timeout_ms = FLAGS_connect_timeout_ms; + options.max_retry = FLAGS_max_retry; + options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params, FLAGS_collation); + if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; + } + + if (argc <= 1) { // interactive mode + // We need this dummy signal hander to interrupt getc (and returning + // EINTR), SIG_IGN did not work. + signal(SIGINT, dummy_handler); + + // Hook getc of readline. + rl_getc_function = cli_getc; + + // Print welcome information. + printf("%s\n", brpc::logo()); + printf( + "This command-line tool mimics the look-n-feel of official " + "mysql-cli, as a demostration of brpc's capability of" + " talking to mysql-server. The output and behavior is " + "not exactly same with the official one.\n\n"); + + for (;;) { + char prompt[128]; + snprintf(prompt, sizeof(prompt), "mysql %s> ", FLAGS_server.c_str()); + std::unique_ptr command(readline(prompt)); + if (command == NULL || *command == '\0') { + if (g_canceled) { + // No input after the prompt and user pressed Ctrl-C, + // quit the CLI. + return 0; + } + // User entered an empty command by just pressing Enter. + continue; + } + if (g_canceled) { + // User entered sth. and pressed Ctrl-C, start a new prompt. + g_canceled = false; + continue; + } + // Add user's command to history so that it's browse-able by + // UP-key and search-able by Ctrl-R. + add_history(command.get()); + + if (!strcmp(command.get(), "help")) { + printf("This is a mysql CLI written in brpc.\n"); + continue; + } + if (!strcmp(command.get(), "quit")) { + // Although quit is a valid mysql command, it does not make + // too much sense to run it in this CLI, just quit. + return 0; + } + access_mysql(channel, command.get()); + } + } else { + std::string command; + command.reserve(argc * 16); + for (int i = 1; i < argc; ++i) { + if (i != 1) { + command.push_back(';'); + } + command.append(argv[i]); + } + if (!access_mysql(channel, command.c_str())) { + return -1; + } + } + return 0; +} diff --git a/example/mysql_c++/mysql_go_press.go b/example/mysql_c++/mysql_go_press.go new file mode 100644 index 0000000000..b68f9d78b6 --- /dev/null +++ b/example/mysql_c++/mysql_go_press.go @@ -0,0 +1,63 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + _ "github.com/go-sql-driver/mysql" + "log" + "sync/atomic" + "time" +) + +var thread_num int + +func init() { + flag.IntVar(&thread_num, "thread_num", 1, "thread number") +} + +var cost int64 +var qps int64 = 1 + +func main() { + flag.Parse() + + db, err := sql.Open("mysql", "brpcuser:12345678@tcp(127.0.0.1:3306)/brpc_test?charset=utf8") + if err != nil { + log.Fatal(err) + } + + for i := 0; i < thread_num; i++ { + go func() { + for { + var ( + id int + col1 string + col2 string + col3 string + col4 string + ) + start := time.Now() + rows, err := db.Query("select * from brpc_press where id = 1") + if err != nil { + log.Fatal(err) + } + for rows.Next() { + if err := rows.Scan(&id, &col1, &col2, &col3, &col4); err != nil { + log.Fatal(err) + } + } + atomic.AddInt64(&cost, time.Since(start).Nanoseconds()) + atomic.AddInt64(&qps, 1) + } + }() + } + + var q int64 = 0 + for { + fmt.Println("qps =", qps-q, "latency =", cost/(qps-q)/1000) + q = atomic.LoadInt64(&qps) + atomic.StoreInt64(&cost, 0) + time.Sleep(1 * time.Second) + } +} diff --git a/example/mysql_c++/mysql_press.cpp b/example/mysql_c++/mysql_press.cpp new file mode 100644 index 0000000000..d58500ff75 --- /dev/null +++ b/example/mysql_c++/mysql_press.cpp @@ -0,0 +1,237 @@ +// Copyright (c) 2014 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A brpc based command-line interface to talk with mysql-server + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DEFINE_string(connection_type, "pooled", "Connection type. Available values: pooled, short"); +DEFINE_string(server, "127.0.0.1", "IP Address of server"); +DEFINE_int32(port, 3306, "Port of server"); +DEFINE_string(user, "brpcuser", "user name"); +DEFINE_string(password, "12345678", "password"); +DEFINE_string(schema, "brpc_test", "schema"); +DEFINE_string(params, "", "params"); +DEFINE_string(collation, "utf8mb4_general_ci", "collation"); +DEFINE_string(data, "ABCDEF", "data"); +DEFINE_int32(timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(connect_timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(max_retry, 3, "Max retries(not including the first RPC)"); +DEFINE_int32(thread_num, 50, "Number of threads to send requests"); +DEFINE_bool(use_bthread, false, "Use bthread to send requests"); +DEFINE_int32(dummy_port, -1, "port of dummy server(for monitoring)"); +DEFINE_int32(op_type, 0, "CRUD operation, 0:INSERT, 1:SELECT, 2:UPDATE"); +DEFINE_bool(dont_fail, false, "Print fatal when some call failed"); + +bvar::LatencyRecorder g_latency_recorder("client"); +bvar::Adder g_error_count("client_error_count"); + +struct SenderArgs { + int base_index; + brpc::Channel* mysql_channel; +}; + +const std::string insert = + "insert into brpc_press(col1,col2,col3,col4) values " + "('" + "ABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCA" + "BCABCABCABCABCABCABCA', '" + + FLAGS_data + + "' ,1.5, " + "now())"; +// Send `command' to mysql-server via `channel' +static void* sender(void* void_args) { + SenderArgs* args = (SenderArgs*)void_args; + std::stringstream command; + if (FLAGS_op_type == 0) { + command << insert; + } else if (FLAGS_op_type == 1) { + command << "select * from brpc_press where id = " << args->base_index + 1; + } else if (FLAGS_op_type == 2) { + command << "update brpc_press set col2 = '" + FLAGS_data + "' where id = " + << args->base_index + 1; + } else { + LOG(ERROR) << "wrong op type " << FLAGS_op_type; + } + + brpc::MysqlRequest request; + if (!request.Query(command.str())) { + LOG(ERROR) << "Fail to execute command"; + return NULL; + } + + while (!brpc::IsAskedToQuit()) { + brpc::MysqlResponse response; + brpc::Controller cntl; + args->mysql_channel->CallMethod(NULL, &cntl, &request, &response, NULL); + const int64_t elp = cntl.latency_us(); + if (!cntl.Failed()) { + g_latency_recorder << elp; + if (FLAGS_op_type == 0) { + CHECK_EQ(response.reply(0).is_ok(), true); + } else if (FLAGS_op_type == 1) { + CHECK_EQ(response.reply(0).row_count(), 1); + } else if (FLAGS_op_type == 2) { + CHECK_EQ(response.reply(0).is_ok(), true); + } + } else { + g_error_count << 1; + CHECK(brpc::IsAskedToQuit() || !FLAGS_dont_fail) + << "error=" << cntl.ErrorText() << " latency=" << elp; + // We can't connect to the server, sleep a while. Notice that this + // is a specific sleeping to prevent this thread from spinning too + // fast. You should continue the business logic in a production + // server rather than sleeping. + bthread_usleep(50000); + } + } + return NULL; +} + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true); + + // A Channel represents a communication line to a Server. Notice that + // Channel is thread-safe and can be shared by all threads in your program. + brpc::Channel channel; + + // Initialize the channel, NULL means using default options. + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; + options.connect_timeout_ms = FLAGS_connect_timeout_ms; + options.max_retry = FLAGS_max_retry; + options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params, FLAGS_collation); + if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; + } + + // create table brpc_press + { + brpc::MysqlRequest request; + if (!request.Query( + "CREATE TABLE IF NOT EXISTS `brpc_press`(`id` INT UNSIGNED AUTO_INCREMENT, `col1` " + "VARCHAR(100) NOT NULL, `col2` VARCHAR(1024) NOT NULL, `col3` decimal(10,0) NOT " + "NULL, `col4` DATE, PRIMARY KEY ( `id` )) ENGINE=InnoDB DEFAULT CHARSET=utf8;")) { + LOG(ERROR) << "Fail to create table"; + return -1; + } + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (!cntl.Failed()) { + std::cout << response << std::endl; + } else { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return -1; + } + } + + // truncate table + { + brpc::MysqlRequest request; + if (!request.Query("truncate table brpc_press")) { + LOG(ERROR) << "Fail to truncate table"; + return -1; + } + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (!cntl.Failed()) { + std::cout << response << std::endl; + } else { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return -1; + } + } + + // prepare data for select, update + if (FLAGS_op_type != 0) { + for (int i = 0; i < FLAGS_thread_num; ++i) { + brpc::MysqlRequest request; + if (!request.Query(insert)) { + LOG(ERROR) << "Fail to execute command"; + return -1; + } + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (cntl.Failed()) { + LOG(ERROR) << cntl.ErrorText(); + return -1; + } + if (!response.reply(0).is_ok()) { + LOG(ERROR) << "prepare data failed"; + return -1; + } + } + } + + if (FLAGS_dummy_port >= 0) { + brpc::StartDummyServerAt(FLAGS_dummy_port); + } + + // test CRUD operations + std::vector bids; + std::vector pids; + bids.resize(FLAGS_thread_num); + pids.resize(FLAGS_thread_num); + std::vector args; + args.resize(FLAGS_thread_num); + for (int i = 0; i < FLAGS_thread_num; ++i) { + args[i].base_index = i; + args[i].mysql_channel = &channel; + if (!FLAGS_use_bthread) { + if (pthread_create(&pids[i], NULL, sender, &args[i]) != 0) { + LOG(ERROR) << "Fail to create pthread"; + return -1; + } + } else { + if (bthread_start_background(&bids[i], NULL, sender, &args[i]) != 0) { + LOG(ERROR) << "Fail to create bthread"; + return -1; + } + } + } + + while (!brpc::IsAskedToQuit()) { + sleep(1); + + LOG(INFO) << "Accessing mysql-server at qps=" << g_latency_recorder.qps(1) + << " latency=" << g_latency_recorder.latency(1); + } + + LOG(INFO) << "mysql_client is going to quit"; + for (int i = 0; i < FLAGS_thread_num; ++i) { + if (!FLAGS_use_bthread) { + pthread_join(pids[i], NULL); + } else { + bthread_join(bids[i], NULL); + } + } + + return 0; +} diff --git a/example/mysql_c++/mysql_stmt.cpp b/example/mysql_c++/mysql_stmt.cpp new file mode 100644 index 0000000000..8e15f2ba8e --- /dev/null +++ b/example/mysql_c++/mysql_stmt.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A brpc based mysql transaction example +#include +#include +#include +#include +#include +#include + +DEFINE_string(connection_type, "pooled", "Connection type. Available values: pooled, short"); +DEFINE_string(server, "127.0.0.1", "IP Address of server"); +DEFINE_int32(port, 3306, "Port of server"); +DEFINE_string(user, "brpcuser", "user name"); +DEFINE_string(password, "12345678", "password"); +DEFINE_string(schema, "brpc_test", "schema"); +DEFINE_string(params, "", "params"); +DEFINE_string(collation, "utf8mb4_general_ci", "collation"); +DEFINE_int32(timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(connect_timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(max_retry, 0, "Max retries(not including the first RPC)"); +DEFINE_int32(thread_num, 1, "Number of threads to send requests"); +DEFINE_int32(count, 1, "Number of request to send pre thread"); + +namespace brpc { +const char* logo(); +} + +struct SenderArgs { + brpc::Channel* mysql_channel; + brpc::MysqlStatement* mysql_stmt; + std::vector commands; +}; + +// Send `command' to mysql-server via `channel' +static void* access_mysql(void* void_args) { + SenderArgs* args = (SenderArgs*)void_args; + brpc::Channel* channel = args->mysql_channel; + brpc::MysqlStatement* stmt = args->mysql_stmt; + const std::vector& commands = args->commands; + + for (int i = 0; i < FLAGS_count; ++i) { + // for (;;) { + brpc::MysqlRequest request(stmt); + for (size_t i = 1; i < commands.size(); i += 2) { + if (commands[i] == "int8") { + int8_t val = strtol(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add int8 param"; + return NULL; + } + } else if (commands[i] == "uint8") { + uint8_t val = strtoul(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add uint8 param"; + return NULL; + } + } else if (commands[i] == "int16") { + int16_t val = strtol(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add uint16 param"; + return NULL; + } + } else if (commands[i] == "uint16") { + uint16_t val = strtoul(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add uint16 param"; + return NULL; + } + } else if (commands[i] == "int32") { + int32_t val = strtol(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add int32 param"; + return NULL; + } + } else if (commands[i] == "uint32") { + uint32_t val = strtoul(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add uint32 param"; + return NULL; + } + } else if (commands[i] == "int64") { + int64_t val = strtol(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add int64 param"; + return NULL; + } + } else if (commands[i] == "uint64") { + uint64_t val = strtoul(commands[i + 1].c_str(), NULL, 10); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add uint64 param"; + return NULL; + } + } else if (commands[i] == "float") { + float val = strtof(commands[i + 1].c_str(), NULL); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add float param"; + return NULL; + } + } else if (commands[i] == "double") { + double val = strtod(commands[i + 1].c_str(), NULL); + if (!request.AddParam(val)) { + LOG(ERROR) << "Fail to add double param"; + return NULL; + } + } else if (commands[i] == "string") { + if (!request.AddParam(commands[i + 1])) { + LOG(ERROR) << "Fail to add string param"; + return NULL; + } + } else { + LOG(ERROR) << "Wrong param type " << commands[i]; + } + } + + brpc::MysqlResponse response; + brpc::Controller cntl; + channel->CallMethod(NULL, &cntl, &request, &response, NULL); + if (cntl.Failed()) { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + return NULL; + } + + // if (response.reply(0).is_error()) { + // check response + std::cout << response << std::endl; + // } + } + + return NULL; +} + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true); + + // A Channel represents a communication line to a Server. Notice that + // Channel is thread-safe and can be shared by all threads in your program. + brpc::Channel channel; + + // Initialize the channel, NULL means using default options. + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; + options.connect_timeout_ms = FLAGS_connect_timeout_ms; + options.max_retry = FLAGS_max_retry; + options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params, FLAGS_collation); + if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; + } + + if (argc <= 1) { + LOG(ERROR) << "No sql statement args"; + } else { + std::vector commands; + commands.reserve(argc * 16); + for (int i = 1; i < argc; ++i) { + commands.push_back(argv[i]); + } + auto stmt(brpc::NewMysqlStatement(channel, commands[0])); + if (stmt == NULL) { + LOG(ERROR) << "Fail to create mysql statement"; + return -1; + } + + std::vector args; + std::vector bids; + args.resize(FLAGS_thread_num); + bids.resize(FLAGS_thread_num); + + for (int i = 0; i < FLAGS_thread_num; ++i) { + args[i].mysql_channel = &channel; + args[i].mysql_stmt = stmt.get(); + args[i].commands = commands; + if (bthread_start_background(&bids[i], NULL, access_mysql, &args[i]) != 0) { + LOG(ERROR) << "Fail to create bthread"; + return -1; + } + } + + for (int i = 0; i < FLAGS_thread_num; ++i) { + bthread_join(bids[i], NULL); + } + } + + return 0; +} + +/* vim: set expandtab ts=4 sw=4 sts=4 tw=100: */ diff --git a/example/mysql_c++/mysql_tx.cpp b/example/mysql_c++/mysql_tx.cpp new file mode 100644 index 0000000000..af9077c2f5 --- /dev/null +++ b/example/mysql_c++/mysql_tx.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A brpc based mysql transaction example +#include +#include +#include +#include +#include + +DEFINE_string(connection_type, "pooled", "Connection type. Available values: pooled, short"); +DEFINE_string(server, "127.0.0.1", "IP Address of server"); +DEFINE_int32(port, 3306, "Port of server"); +DEFINE_string(user, "brpcuser", "user name"); +DEFINE_string(password, "12345678", "password"); +DEFINE_string(schema, "brpc_test", "schema"); +DEFINE_string(params, "", "params"); +DEFINE_string(collation, "utf8mb4_general_ci", "collation"); +DEFINE_int32(timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(connect_timeout_ms, 5000, "RPC timeout in milliseconds"); +DEFINE_int32(max_retry, 0, "Max retries(not including the first RPC)"); +DEFINE_bool(readonly, false, "readonly transaction"); +DEFINE_int32(isolation_level, 0, "transaction isolation level"); + +namespace brpc { +const char* logo(); +} + +// Send `command' to mysql-server via `channel' +static bool access_mysql(brpc::Channel& channel, const std::vector& commands) { + brpc::MysqlTransactionOptions options; + options.readonly = FLAGS_readonly; + options.isolation_level = brpc::MysqlIsolationLevel(FLAGS_isolation_level); + auto tx(brpc::NewMysqlTransaction(channel, options)); + if (tx == NULL) { + LOG(ERROR) << "Fail to create transaction"; + return false; + } + + for (auto it = commands.begin(); it != commands.end(); ++it) { + brpc::MysqlRequest request(tx.get()); + if (!request.Query(*it)) { + LOG(ERROR) << "Fail to add command"; + tx->rollback(); + return false; + } + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (cntl.Failed()) { + LOG(ERROR) << "Fail to access mysql, " << cntl.ErrorText(); + tx->rollback(); + return false; + } + // check response + std::cout << response << std::endl; + for (size_t i = 0; i < response.reply_size(); ++i) { + if (response.reply(i).is_error()) { + tx->rollback(); + return false; + } + } + } + tx->commit(); + return true; +} + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true); + + // A Channel represents a communication line to a Server. Notice that + // Channel is thread-safe and can be shared by all threads in your program. + brpc::Channel channel; + + // Initialize the channel, NULL means using default options. + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms /*milliseconds*/; + options.connect_timeout_ms = FLAGS_connect_timeout_ms; + options.max_retry = FLAGS_max_retry; + options.auth = new brpc::policy::MysqlAuthenticator( + FLAGS_user, FLAGS_password, FLAGS_schema, FLAGS_params, FLAGS_collation); + if (channel.Init(FLAGS_server.c_str(), FLAGS_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel"; + return -1; + } + + if (argc <= 1) { + LOG(ERROR) << "No sql statement args"; + } else { + std::vector commands; + commands.reserve(argc * 16); + for (int i = 1; i < argc; ++i) { + commands.push_back(argv[i]); + } + if (!access_mysql(channel, commands)) { + return -1; + } + } + return 0; +} + +/* vim: set expandtab ts=4 sw=4 sts=4 tw=100: */ diff --git a/example/mysql_c++/mysqlclient_press.cpp b/example/mysql_c++/mysqlclient_press.cpp new file mode 100644 index 0000000000..b1f27a8c9c --- /dev/null +++ b/example/mysql_c++/mysqlclient_press.cpp @@ -0,0 +1,239 @@ +// Copyright (c) 2014 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A brpc based command-line interface to talk with mysql-server + +#include +#include +#include +extern "C" { +#include +} +#include +#include +#include +#include + +DEFINE_string(server, "127.0.0.1", "IP Address of server"); +DEFINE_int32(port, 3306, "Port of server"); +DEFINE_string(user, "brpcuser", "user name"); +DEFINE_string(password, "12345678", "password"); +DEFINE_string(schema, "brpc_test", "schema"); +DEFINE_string(params, "", "params"); +DEFINE_string(data, "ABCDEF", "data"); +DEFINE_int32(thread_num, 50, "Number of threads to send requests"); +DEFINE_bool(use_bthread, false, "Use bthread to send requests"); +DEFINE_int32(dummy_port, -1, "port of dummy server(for monitoring)"); +DEFINE_int32(op_type, 0, "CRUD operation, 0:INSERT, 1:SELECT, 3:UPDATE"); +DEFINE_bool(dont_fail, false, "Print fatal when some call failed"); + +bvar::LatencyRecorder g_latency_recorder("client"); +bvar::Adder g_error_count("client_error_count"); + +struct SenderArgs { + int base_index; + MYSQL* mysql_conn; +}; + +const std::string insert = + "insert into mysqlclient_press(col1,col2,col3,col4) values " + "('" + "ABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCA" + "BCABCABCABCABCABCABCA', '" + + FLAGS_data + + "' ,1.5, " + "now())"; +// Send `command' to mysql-server via `channel' +static void* sender(void* void_args) { + SenderArgs* args = (SenderArgs*)void_args; + std::stringstream command; + if (FLAGS_op_type == 0) { + command << insert; + } else if (FLAGS_op_type == 1) { + command << "select * from mysqlclient_press where id = " << args->base_index + 1; + } else if (FLAGS_op_type == 2) { + command << "update brpc_press set col2 = '" + FLAGS_data + "' where id = " + << args->base_index + 1; + } else { + LOG(ERROR) << "wrong op type " << FLAGS_op_type; + } + + std::string command_str = command.str(); + + while (!brpc::IsAskedToQuit()) { + const int64_t begin_time_us = butil::cpuwide_time_us(); + const int rc = mysql_real_query(args->mysql_conn, command_str.c_str(), command_str.size()); + if (rc != 0) { + goto ERROR; + } + + if (mysql_errno(args->mysql_conn) == 0) { + if (FLAGS_op_type == 0) { + CHECK_EQ(mysql_affected_rows(args->mysql_conn), 1); + } else if (FLAGS_op_type == 1) { + MYSQL_RES* res = mysql_store_result(args->mysql_conn); + if (res == NULL) { + LOG(INFO) << "not found"; + } else { + CHECK_EQ(mysql_num_rows(res), 1); + mysql_free_result(res); + } + } else if (FLAGS_op_type == 2) { + } + const int64_t elp = butil::cpuwide_time_us() - begin_time_us; + g_latency_recorder << elp; + } else { + goto ERROR; + } + + if (false) { + ERROR: + const int64_t elp = butil::cpuwide_time_us() - begin_time_us; + g_error_count << 1; + CHECK(brpc::IsAskedToQuit() || !FLAGS_dont_fail) + << "error=" << mysql_error(args->mysql_conn) << " latency=" << elp; + // We can't connect to the server, sleep a while. Notice that this + // is a specific sleeping to prevent this thread from spinning too + // fast. You should continue the business logic in a production + // server rather than sleeping. + bthread_usleep(50000); + } + } + return NULL; +} + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true); + + if (FLAGS_dummy_port >= 0) { + brpc::StartDummyServerAt(FLAGS_dummy_port); + } + + MYSQL* conn = mysql_init(NULL); + if (!mysql_real_connect(conn, + FLAGS_server.c_str(), + FLAGS_user.c_str(), + FLAGS_password.c_str(), + FLAGS_schema.c_str(), + FLAGS_port, + NULL, + 0)) { + LOG(ERROR) << mysql_error(conn); + return -1; + } + + // create table mysqlclient_press + { + const char* sql = + "CREATE TABLE IF NOT EXISTS `mysqlclient_press`(`id` INT UNSIGNED AUTO_INCREMENT, " + "`col1` " + "VARCHAR(100) NOT NULL, `col2` VARCHAR(1024) NOT NULL, `col3` decimal(10,0) NOT " + "NULL, `col4` DATE, PRIMARY KEY ( `id` )) ENGINE=InnoDB DEFAULT CHARSET=utf8;"; + const int rc = mysql_real_query(conn, sql, strlen(sql)); + if (rc != 0) { + LOG(ERROR) << "Fail to execute sql, " << mysql_error(conn); + return -1; + } + + if (mysql_errno(conn) != 0) { + LOG(ERROR) << "Fail to store result, " << mysql_error(conn); + return -1; + } + } + + // truncate table + { + const char* sql = "truncate table mysqlclient_press"; + const int rc = mysql_real_query(conn, sql, strlen(sql)); + if (rc != 0) { + LOG(ERROR) << "Fail to execute sql, " << mysql_error(conn); + return -1; + } + + if (mysql_errno(conn) != 0) { + LOG(ERROR) << "Fail to store result, " << mysql_error(conn); + return -1; + } + } + + // prepare data for select, update + if (FLAGS_op_type != 0) { + for (int i = 0; i < FLAGS_thread_num; ++i) { + const int rc = mysql_real_query(conn, insert.c_str(), insert.size()); + if (rc != 0) { + LOG(ERROR) << "Fail to execute sql, " << mysql_error(conn); + return -1; + } + + if (mysql_errno(conn) != 0) { + LOG(ERROR) << "Fail to store result, " << mysql_error(conn); + return -1; + } + } + } + + // test CRUD operations + std::vector bids; + std::vector pids; + bids.resize(FLAGS_thread_num); + pids.resize(FLAGS_thread_num); + std::vector args; + args.resize(FLAGS_thread_num); + for (int i = 0; i < FLAGS_thread_num; ++i) { + MYSQL* conn = mysql_init(NULL); + if (!mysql_real_connect(conn, + FLAGS_server.c_str(), + FLAGS_user.c_str(), + FLAGS_password.c_str(), + FLAGS_schema.c_str(), + FLAGS_port, + NULL, + 0)) { + LOG(ERROR) << mysql_error(conn); + return -1; + } + args[i].base_index = i; + args[i].mysql_conn = conn; + if (!FLAGS_use_bthread) { + if (pthread_create(&pids[i], NULL, sender, &args[i]) != 0) { + LOG(ERROR) << "Fail to create pthread"; + return -1; + } + } else { + if (bthread_start_background(&bids[i], NULL, sender, &args[i]) != 0) { + LOG(ERROR) << "Fail to create bthread"; + return -1; + } + } + } + + while (!brpc::IsAskedToQuit()) { + sleep(1); + + LOG(INFO) << "Accessing mysql-server at qps=" << g_latency_recorder.qps(1) + << " latency=" << g_latency_recorder.latency(1); + } + + LOG(INFO) << "mysql_client is going to quit"; + for (int i = 0; i < FLAGS_thread_num; ++i) { + if (!FLAGS_use_bthread) { + pthread_join(pids[i], NULL); + } else { + bthread_join(bids[i], NULL); + } + } + + return 0; +} diff --git a/src/brpc/controller.cpp b/src/brpc/controller.cpp index b6c8e750fe..ccec56e67f 100644 --- a/src/brpc/controller.cpp +++ b/src/brpc/controller.cpp @@ -278,6 +278,8 @@ void Controller::ResetPods() { _request_stream = INVALID_STREAM_ID; _response_stream = INVALID_STREAM_ID; _remote_stream_settings = NULL; + _bind_sock_action = BIND_SOCK_NONE; + _stmt = NULL; _auth_flags = 0; } @@ -308,6 +310,7 @@ void Controller::Call::Reset() { peer_id = INVALID_SOCKET_ID; begin_time_us = 0; sending_sock.reset(NULL); + bind_sock_action = BIND_SOCK_NONE; stream_user_data = NULL; } @@ -760,8 +763,14 @@ void Controller::Call::OnComplete( // Otherwise in-flight responses may come back in future and break the // assumption that one pooled connection cannot have more than one // message at the same time. + // If bind sock action is active take the ownship of sending sock + // If bind sock is not INVALID, RPC complete, reset the sock to INVALID if (sending_sock != NULL && (error_code == 0 || responded)) { - if (!sending_sock->is_read_progressive()) { + if (bind_sock_action == BIND_SOCK_ACTIVE) { + c->_bind_sock.reset(sending_sock.release()); + } else if (bind_sock_action == BIND_SOCK_USE) { + // do nothing + } else if (!sending_sock->is_read_progressive()) { // Normally-read socket which will not be used after RPC ends, // safe to return. Notice that Socket::is_read_progressive may // differ from Controller::is_response_read_progressively() @@ -778,7 +787,11 @@ void Controller::Call::OnComplete( case CONNECTION_TYPE_SHORT: if (sending_sock != NULL) { // Check the comment in CONNECTION_TYPE_POOLED branch. - if (!sending_sock->is_read_progressive()) { + if (bind_sock_action == BIND_SOCK_ACTIVE) { + c->_bind_sock.reset(sending_sock.release()); + } else if (bind_sock_action == BIND_SOCK_USE) { + // do nothing + } else if (!sending_sock->is_read_progressive()) { if (c->_stream_creator == NULL) { sending_sock->SetFailed(); } @@ -845,6 +858,8 @@ void Controller::EndRPC(const CompletionInfo& info) { } // TODO: Replace this with stream_creator. HandleStreamConnection(_current_call.sending_sock.get()); + // Only bind the success sock of the RPC + _current_call.bind_sock_action = _bind_sock_action; _current_call.OnComplete(this, _error_code, info.responded, true); } else { // Even if _unfinished_call succeeded, we don't use EBACKUPREQUEST @@ -1017,7 +1032,17 @@ void Controller::IssueRPC(int64_t start_realtime_us) { _current_call.need_feedback = false; _current_call.enable_circuit_breaker = has_enabled_circuit_breaker(); SocketUniquePtr tmp_sock; - if (SingleServer()) { + if ((_connection_type & CONNECTION_TYPE_POOLED_AND_SHORT) && + _bind_sock_action == BIND_SOCK_USE) { + tmp_sock.reset(_bind_sock.release()); + if (!tmp_sock || (!is_health_check_call() && !tmp_sock->IsAvailable())) { + SetFailed(EHOSTDOWN, "Not connected to bind socket yet, server_id=%" PRIu64, + tmp_sock->id()); + tmp_sock.reset(); // Release ref ASAP + return HandleSendFailed(); + } + _current_call.peer_id = tmp_sock->id(); + } else if (SingleServer()) { // Don't use _current_call.peer_id which is set to -1 after construction // of the backup call. const int rc = Socket::Address(_single_server_id, &tmp_sock); @@ -1083,7 +1108,10 @@ void Controller::IssueRPC(int64_t start_realtime_us) { _current_call.sending_sock->set_preferred_index(_preferred_index); } else { int rc = 0; - if (_connection_type == CONNECTION_TYPE_POOLED) { + // if _bind_sock is not NULL, use it + if (_bind_sock_action == BIND_SOCK_USE) { + _current_call.sending_sock.reset(tmp_sock.release()); + } else if (_connection_type == CONNECTION_TYPE_POOLED) { rc = tmp_sock->GetPooledSocket(&_current_call.sending_sock); } else if (_connection_type == CONNECTION_TYPE_SHORT) { rc = tmp_sock->GetShortSocket(&_current_call.sending_sock); @@ -1105,7 +1133,7 @@ void Controller::IssueRPC(int64_t start_realtime_us) { _current_call.sending_sock->set_preferred_index(_preferred_index); // Set preferred_index of main_socket as well to make it easier to // debug and observe from /connections. - if (tmp_sock->preferred_index() < 0) { + if (tmp_sock && tmp_sock->preferred_index() < 0) { tmp_sock->set_preferred_index(_preferred_index); } tmp_sock.reset(); diff --git a/src/brpc/controller.h b/src/brpc/controller.h index 658cc6957c..92fe7c76a2 100644 --- a/src/brpc/controller.h +++ b/src/brpc/controller.h @@ -104,6 +104,14 @@ enum StopStyle { const int32_t UNSET_MAGIC_NUM = -123456789; +// if controller want to reserve a sock after RPC, set BIND_SOCK_ACTIVE +enum BindSockAction { + BIND_SOCK_ACTIVE, + BIND_SOCK_USE, + BIND_SOCK_NONE, +}; +// mysql prepared statement declare +class MysqlStatementStub; // A Controller mediates a single method call. The primary purpose of // the controller is to provide a way to manipulate settings per RPC-call // and to find out about RPC-level errors. @@ -674,6 +682,7 @@ friend void policy::ProcessThriftRequest(InputMessageBase*); // CONNECTION_TYPE_SINGLE. Otherwise, it may be a temporary // socket fetched from socket pool SocketUniquePtr sending_sock; + BindSockAction bind_sock_action; StreamUserData* stream_user_data; }; @@ -815,6 +824,13 @@ friend void policy::ProcessThriftRequest(InputMessageBase*); // Defined at both sides StreamSettings *_remote_stream_settings; + // controller bind socket action + BindSockAction _bind_sock_action; + // controller bind sock + SocketUniquePtr _bind_sock; + // sql prepare statement + MysqlStatementStub *_stmt; + // Thrift method name, only used when thrift protocol enabled std::string _thrift_method_name; diff --git a/src/brpc/details/controller_private_accessor.h b/src/brpc/details/controller_private_accessor.h index 1be7df8bb8..5c130054d5 100644 --- a/src/brpc/details/controller_private_accessor.h +++ b/src/brpc/details/controller_private_accessor.h @@ -152,6 +152,22 @@ class ControllerPrivateAccessor { return *this; } + // Set bind socket action + void set_bind_sock_action(BindSockAction action) { _cntl->_bind_sock_action = action; } + // Transfer ownership to other + void get_bind_sock(SocketUniquePtr* ptr) { + _cntl->_bind_sock->ReAddress(ptr); + } + // Use a external socket + void use_bind_sock(SocketId sock_id) { + _cntl->_bind_sock_action = BIND_SOCK_USE; + Socket::Address(sock_id, &_cntl->_bind_sock); + } + // set prepare statement + void set_stmt(MysqlStatementStub *stmt) { _cntl->_stmt = stmt; } + // get prepare statement + MysqlStatementStub* get_stmt() { return _cntl->_stmt; } + private: Controller* _cntl; }; diff --git a/src/brpc/global.cpp b/src/brpc/global.cpp index 30c2f1a3b9..aedeea6c8c 100644 --- a/src/brpc/global.cpp +++ b/src/brpc/global.cpp @@ -72,6 +72,7 @@ #include "brpc/policy/nshead_mcpack_protocol.h" #include "brpc/policy/rtmp_protocol.h" #include "brpc/policy/esp_protocol.h" +#include "brpc/policy/mysql_protocol.h" #ifdef ENABLE_THRIFT_FRAMED_PROTOCOL # include "brpc/policy/thrift_protocol.h" #endif @@ -582,6 +583,20 @@ static void GlobalInitializeOrDieImpl() { exit(1); } + Protocol mysql_protocol = {ParseMysqlMessage, + SerializeMysqlRequest, + PackMysqlRequest, + NULL, + ProcessMysqlResponse, + NULL, + NULL, + GetMysqlMethodName, + CONNECTION_TYPE_POOLED_AND_SHORT, + "mysql"}; + if (RegisterProtocol(PROTOCOL_MYSQL, mysql_protocol) != 0) { + exit(1); + } + std::vector protocols; ListProtocols(&protocols); for (size_t i = 0; i < protocols.size(); ++i) { diff --git a/src/brpc/mysql.cpp b/src/brpc/mysql.cpp new file mode 100644 index 0000000000..5f3501a2e4 --- /dev/null +++ b/src/brpc/mysql.cpp @@ -0,0 +1,695 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION +#include +#include +#include +#include +#include +#include +#include +#include +#include "butil/string_printf.h" +#include "butil/macros.h" +#include "brpc/controller.h" +#include "brpc/mysql.h" +#include "brpc/mysql_common.h" + +namespace brpc { + +DEFINE_int32(mysql_multi_replies_size, 10, "multi replies size in one MysqlResponse"); + +// Internal implementation detail -- do not call these. +void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_impl(); +void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); +void protobuf_AssignDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); +void protobuf_ShutdownFile_baidu_2frpc_2fmysql_5fbase_2eproto(); + +namespace { + +const ::google::protobuf::Descriptor* MysqlRequest_descriptor_ = NULL; +const ::google::protobuf::Descriptor* MysqlResponse_descriptor_ = NULL; + +} // namespace + +void protobuf_AssignDesc_baidu_2frpc_2fmysql_5fbase_2eproto() { + protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + const ::google::protobuf::FileDescriptor* file = + ::google::protobuf::DescriptorPool::generated_pool()->FindFileByName( + "baidu/rpc/mysql_base.proto"); + GOOGLE_CHECK(file != NULL); + MysqlRequest_descriptor_ = file->message_type(0); + MysqlResponse_descriptor_ = file->message_type(1); +} + +namespace { + +GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AssignDescriptors_once_); +inline void protobuf_AssignDescriptorsOnce() { + ::google::protobuf::GoogleOnceInit(&protobuf_AssignDescriptors_once_, + &protobuf_AssignDesc_baidu_2frpc_2fmysql_5fbase_2eproto); +} + +void protobuf_RegisterTypes(const ::std::string&) { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + MysqlRequest_descriptor_, &MysqlRequest::default_instance()); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( + MysqlResponse_descriptor_, &MysqlResponse::default_instance()); +} + +} // namespace + +void protobuf_ShutdownFile_baidu_2frpc_2fmysql_5fbase_2eproto() { + delete MysqlRequest::default_instance_; + delete MysqlResponse::default_instance_; +} + +void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_impl() { + GOOGLE_PROTOBUF_VERIFY_VERSION; + +#if GOOGLE_PROTOBUF_VERSION >= 3002000 + ::google::protobuf::internal::InitProtobufDefaults(); +#else + ::google::protobuf::protobuf_AddDesc_google_2fprotobuf_2fdescriptor_2eproto(); +#endif + ::google::protobuf::DescriptorPool::InternalAddGeneratedFile( + "\n\032baidu/rpc/mysql_base.proto\022\tbaidu.rpc\032" + " google/protobuf/descriptor.proto\"\016\n\014Mys" + "qlRequest\"\017\n\rMysqlResponseB\003\200\001\001", + 111); + ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile("baidu/rpc/mysql_base.proto", + &protobuf_RegisterTypes); + MysqlRequest::default_instance_ = new MysqlRequest(); + MysqlResponse::default_instance_ = new MysqlResponse(); + MysqlRequest::default_instance_->InitAsDefaultInstance(); + MysqlResponse::default_instance_->InitAsDefaultInstance(); + ::google::protobuf::internal::OnShutdown( + &protobuf_ShutdownFile_baidu_2frpc_2fmysql_5fbase_2eproto); +} + +GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_once); +void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto() { + ::google::protobuf::GoogleOnceInit(&protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_once, + &protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_impl); +} + +// Force AddDescriptors() to be called at static initialization time. +struct StaticDescriptorInitializer_baidu_2frpc_2fmysql_5fbase_2eproto { + StaticDescriptorInitializer_baidu_2frpc_2fmysql_5fbase_2eproto() { + protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + } +} static_descriptor_initializer_baidu_2frpc_2fmysql_5fbase_2eproto_; + + +// =================================================================== + +#ifndef _MSC_VER +#endif // !_MSC_VER + +butil::Status MysqlStatementStub::PackExecuteCommand(butil::IOBuf* outbuf, uint32_t stmt_id) { + butil::Status st; + // long data + for (const auto& i : _long_data) { + st = MysqlMakeLongDataPacket(outbuf, stmt_id, i.param_id, i.long_data); + if (!st.ok()) { + LOG(ERROR) << "make long data header error " << st; + return st; + } + } + _long_data.clear(); + // execute data + st = MysqlMakeExecutePacket(outbuf, stmt_id, _execute_data); + if (!st.ok()) { + LOG(ERROR) << "make execute header error " << st; + return st; + } + _execute_data.clear(); + _null_mask.mask.clear(); + _null_mask.area = butil::IOBuf::INVALID_AREA; + _param_types.types.clear(); + _param_types.area = butil::IOBuf::INVALID_AREA; + + return st; +} + +MysqlRequest::MysqlRequest() : ::google::protobuf::Message() { + SharedCtor(); +} + +MysqlRequest::MysqlRequest(const MysqlTransaction* tx) : ::google::protobuf::Message() { + SharedCtor(); + _tx = tx; +} + +MysqlRequest::MysqlRequest(MysqlStatement* stmt) : ::google::protobuf::Message() { + SharedCtor(); + _stmt = new MysqlStatementStub(stmt); +} + +MysqlRequest::MysqlRequest(const MysqlTransaction* tx, MysqlStatement* stmt) + : ::google::protobuf::Message() { + SharedCtor(); + _tx = tx; + _stmt = new MysqlStatementStub(stmt); +} + +void MysqlRequest::InitAsDefaultInstance() {} + +MysqlRequest::MysqlRequest(const MysqlRequest& from) : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); +} + +void MysqlRequest::SharedCtor() { + _has_error = false; + _cached_size_ = 0; + _has_command = false; + _tx = NULL; + _stmt = NULL; + _param_index = 0; +} + +MysqlRequest::~MysqlRequest() { + SharedDtor(); + if (_stmt != NULL) { + delete _stmt; + } + _stmt = NULL; +} + +void MysqlRequest::SharedDtor() { + if (this != default_instance_) { + } +} + +void MysqlRequest::SetCachedSize(int size) const { + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); +} +const ::google::protobuf::Descriptor* MysqlRequest::descriptor() { + protobuf_AssignDescriptorsOnce(); + return MysqlRequest_descriptor_; +} + +const MysqlRequest& MysqlRequest::default_instance() { + if (default_instance_ == NULL) { + protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + } + return *default_instance_; +} + +MysqlRequest* MysqlRequest::default_instance_ = NULL; + +MysqlRequest* MysqlRequest::New() const { + return new MysqlRequest; +} + +void MysqlRequest::Clear() { + _has_error = false; + _buf.clear(); + _has_command = false; + _tx = NULL; + _stmt = NULL; +} + +bool MysqlRequest::MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream*) { + LOG(WARNING) << "You're not supposed to parse a MysqlRequest"; + return true; +} + +void MysqlRequest::SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream*) const { + LOG(WARNING) << "You're not supposed to serialize a MysqlRequest"; +} + +::google::protobuf::uint8* MysqlRequest::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + return target; +} + +int MysqlRequest::ByteSize() const { + int total_size = _buf.size(); + GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN(); + _cached_size_ = total_size; + GOOGLE_SAFE_CONCURRENT_WRITES_END(); + return total_size; +} + +void MysqlRequest::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const MysqlRequest* source = + ::google::protobuf::internal::dynamic_cast_if_available(&from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void MysqlRequest::MergeFrom(const MysqlRequest& from) { + // TODO: maybe need to optimize + // GOOGLE_CHECK_NE(&from, this); + // const int header_size = 4; + // const uint32_t size_l = from._buf.size() - header_size - 1; // payload - type + // const uint32_t size_r = _buf.size() - header_size + 1; // payload + seqno + // const uint32_t payload_size = butil::ByteSwapToLE32(size_l + size_r); + // if (payload_size > mysql_max_package_size) { + // CHECK(false) + // << "[MysqlRequest::MergeFrom] statement size is too big, merge from do nothing"; + // return; + // } + // butil::IOBuf buf; + // butil::IOBuf result; + // _has_error = _has_error || from._has_error; + // buf.append(from._buf); + // buf.pop_front(header_size + 1); + // _buf.pop_front(header_size - 1); + // result.append(&payload_size, 3); + // result.append(_buf); + // result.append(buf); + // _buf = result; + // _has_command = _has_command || from._has_command; +} + +void MysqlRequest::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) + return; + Clear(); + MergeFrom(from); +} + +void MysqlRequest::CopyFrom(const MysqlRequest& from) { + if (&from == this) + return; + Clear(); + MergeFrom(from); +} + +void MysqlRequest::Swap(MysqlRequest* other) { + if (other != this) { + _buf.swap(other->_buf); + std::swap(_has_error, other->_has_error); + std::swap(_cached_size_, other->_cached_size_); + std::swap(_has_command, other->_has_command); + } +} + +bool MysqlRequest::SerializeTo(butil::IOBuf* buf) const { + if (_has_error) { + LOG(ERROR) << "Reject serialization due to error in CommandXXX[V]"; + return false; + } + *buf = _buf; + return true; +} + +::google::protobuf::Metadata MysqlRequest::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = MysqlRequest_descriptor_; + metadata.reflection = NULL; + return metadata; +} + +bool MysqlRequest::Query(const butil::StringPiece& command) { + if (_has_error) { + return false; + } + + if (_has_command) { + return false; + } + + const butil::Status st = MysqlMakeCommand(&_buf, MYSQL_COM_QUERY, command); + if (st.ok()) { + _has_command = true; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} + +bool MysqlRequest::AddParam(int8_t p) { + if (_has_error) { + return false; + } + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_TINY); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(uint8_t p) { + const butil::Status st = + MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_TINY, true); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(int16_t p) { + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_SHORT); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(uint16_t p) { + const butil::Status st = + MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_SHORT, true); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(int32_t p) { + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_LONG); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(uint32_t p) { + const butil::Status st = + MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_LONG, true); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(int64_t p) { + const butil::Status st = + MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_LONGLONG); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(uint64_t p) { + const butil::Status st = + MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_LONGLONG, true); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(float p) { + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_FLOAT); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(double p) { + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_DOUBLE); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} +bool MysqlRequest::AddParam(const butil::StringPiece& p) { + const butil::Status st = MysqlMakeExecuteData(_stmt, _param_index, &p, MYSQL_FIELD_TYPE_STRING); + if (st.ok()) { + ++_param_index; + return true; + } else { + CHECK(st.ok()) << st; + _has_error = true; + return false; + } +} + +void MysqlRequest::Print(std::ostream& os) const { + butil::IOBuf cp = _buf; + { + uint8_t buf[3]; + cp.cutn(buf, 3); + os << "size:" << mysql_uint3korr(buf) << ","; + } + { + uint8_t buf; + cp.cut1((char*)&buf); + os << "sequence:" << (unsigned)buf << ","; + } + os << "payload(hex):"; + while (!cp.empty()) { + uint8_t buf; + cp.cut1((char*)&buf); + os << std::hex << (unsigned)buf; + } +} + +std::ostream& operator<<(std::ostream& os, const MysqlRequest& r) { + r.Print(os); + return os; +} + +// =================================================================== + +#ifndef _MSC_VER +#endif // !_MSC_VER + +MysqlResponse::MysqlResponse() : ::google::protobuf::Message() { + SharedCtor(); +} + +void MysqlResponse::InitAsDefaultInstance() {} + +MysqlResponse::MysqlResponse(const MysqlResponse& from) : ::google::protobuf::Message() { + SharedCtor(); + MergeFrom(from); +} + +void MysqlResponse::SharedCtor() { + _nreply = 0; + _cached_size_ = 0; +} + +MysqlResponse::~MysqlResponse() { + SharedDtor(); +} + +void MysqlResponse::SharedDtor() { + if (this != default_instance_) { + } +} + +void MysqlResponse::SetCachedSize(int size) const { + _cached_size_ = size; +} +const ::google::protobuf::Descriptor* MysqlResponse::descriptor() { + protobuf_AssignDescriptorsOnce(); + return MysqlResponse_descriptor_; +} + +const MysqlResponse& MysqlResponse::default_instance() { + if (default_instance_ == NULL) { + protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + } + return *default_instance_; +} + +MysqlResponse* MysqlResponse::default_instance_ = NULL; + +MysqlResponse* MysqlResponse::New() const { + return new MysqlResponse; +} + +void MysqlResponse::Clear() {} + +bool MysqlResponse::MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream*) { + LOG(WARNING) << "You're not supposed to parse a MysqlResponse"; + return true; +} + +void MysqlResponse::SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream*) const { + LOG(WARNING) << "You're not supposed to serialize a MysqlResponse"; +} + +::google::protobuf::uint8* MysqlResponse::SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* target) const { + return target; +} + +int MysqlResponse::ByteSize() const { + return _cached_size_; +} + +void MysqlResponse::MergeFrom(const ::google::protobuf::Message& from) { + GOOGLE_CHECK_NE(&from, this); + const MysqlResponse* source = + ::google::protobuf::internal::dynamic_cast_if_available(&from); + if (source == NULL) { + ::google::protobuf::internal::ReflectionOps::Merge(from, this); + } else { + MergeFrom(*source); + } +} + +void MysqlResponse::MergeFrom(const MysqlResponse& from) { + GOOGLE_CHECK_NE(&from, this); +} + +void MysqlResponse::CopyFrom(const ::google::protobuf::Message& from) { + if (&from == this) + return; + Clear(); + MergeFrom(from); +} + +void MysqlResponse::CopyFrom(const MysqlResponse& from) { + if (&from == this) + return; + Clear(); + MergeFrom(from); +} + +bool MysqlResponse::IsInitialized() const { + return true; +} + +void MysqlResponse::Swap(MysqlResponse* other) { + if (other != this) { + _first_reply.Swap(other->_first_reply); + std::swap(_other_replies, other->_other_replies); + _arena.swap(other->_arena); + std::swap(_nreply, other->_nreply); + std::swap(_cached_size_, other->_cached_size_); + } +} + +::google::protobuf::Metadata MysqlResponse::GetMetadata() const { + protobuf_AssignDescriptorsOnce(); + ::google::protobuf::Metadata metadata; + metadata.descriptor = MysqlResponse_descriptor_; + metadata.reflection = NULL; + return metadata; +} + +// =================================================================== + +ParseError MysqlResponse::ConsumePartialIOBuf(butil::IOBuf& buf, + bool is_auth, + MysqlStmtType stmt_type) { + bool more_results = true; + size_t oldsize = 0; + while (more_results) { + oldsize = buf.size(); + if (reply_size() == 0) { + ParseError err = + _first_reply.ConsumePartialIOBuf(buf, &_arena, is_auth, stmt_type, &more_results); + if (err != PARSE_OK) { + return err; + } + } else { + const int32_t replies_size = + FLAGS_mysql_multi_replies_size > 1 ? FLAGS_mysql_multi_replies_size : 10; + if (_other_replies.size() < reply_size()) { + MysqlReply* replies = + (MysqlReply*)_arena.allocate(sizeof(MysqlReply) * (replies_size - 1)); + if (replies == NULL) { + LOG(ERROR) << "Fail to allocate MysqlReply[" << replies_size - 1 << "]"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + _other_replies.reserve(replies_size - 1); + for (int i = 0; i < replies_size - 1; ++i) { + new (&replies[i]) MysqlReply; + _other_replies.push_back(&replies[i]); + } + } + ParseError err = _other_replies[_nreply - 1]->ConsumePartialIOBuf( + buf, &_arena, is_auth, stmt_type, &more_results); + if (err != PARSE_OK) { + return err; + } + } + + const size_t newsize = buf.size(); + _cached_size_ += oldsize - newsize; + oldsize = newsize; + ++_nreply; + } + + if (oldsize == 0) { + return PARSE_OK; + } else { + LOG(ERROR) << "Parse protocol finished, but IOBuf has more data"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } +} + +std::ostream& operator<<(std::ostream& os, const MysqlResponse& response) { + os << "\n-----MYSQL REPLY BEGIN-----\n"; + if (response.reply_size() == 0) { + os << ""; + } else if (response.reply_size() == 1) { + os << response.reply(0); + } else { + for (size_t i = 0; i < response.reply_size(); ++i) { + os << "\nreply(" << i << ")----------"; + os << response.reply(i); + } + } + os << "\n-----MYSQL REPLY END-----\n"; + + return os; +} + +} // namespace brpc diff --git a/src/brpc/mysql.h b/src/brpc/mysql.h new file mode 100644 index 0000000000..17fd5e6785 --- /dev/null +++ b/src/brpc/mysql.h @@ -0,0 +1,286 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_H +#define BRPC_MYSQL_H + +#include +#include +#include + +#include +#include +#include +#include +#include "google/protobuf/descriptor.pb.h" + +#include "butil/iobuf.h" +#include "butil/strings/string_piece.h" +#include "butil/arena.h" +#include "parse_result.h" +#include "mysql_command.h" +#include "mysql_reply.h" +#include "mysql_transaction.h" +#include "mysql_statement.h" + +namespace brpc { +// Request to mysql. +// Notice that you can pipeline multiple commands in one request and sent +// them to ONE mysql-server together. +// Example: +// MysqlRequest request; +// request.Query("select * from table"); +// MysqlResponse response; +// channel.CallMethod(NULL, &controller, &request, &response, NULL/*done*/); +// if (!cntl.Failed()) { +// LOG(INFO) << response.reply(0); +// } + +class MysqlStatementStub { +public: + MysqlStatementStub(MysqlStatement* stmt); + MysqlStatement* stmt(); + butil::IOBuf& execute_data(); + butil::Status PackExecuteCommand(butil::IOBuf* outbuf, uint32_t stmt_id); + // prepare statement null mask + struct NullMask { + NullMask() : area(butil::IOBuf::INVALID_AREA) {} + std::vector mask; + butil::IOBuf::Area area; + }; + // prepare statement param types + struct ParamTypes { + ParamTypes() : area(butil::IOBuf::INVALID_AREA) {} + std::vector types; + butil::IOBuf::Area area; + }; + // null mask and param types + NullMask& null_mask(); + ParamTypes& param_types(); + // save long data + void save_long_data(uint16_t param_id, const butil::StringPiece& value); + +private: + MysqlStatement* _stmt; + butil::IOBuf _execute_data; + NullMask _null_mask; + ParamTypes _param_types; + // long data + struct LongData { + uint16_t param_id; + butil::IOBuf long_data; + }; + std::vector _long_data; +}; + +inline MysqlStatementStub::MysqlStatementStub(MysqlStatement* stmt) : _stmt(stmt) {} + +inline MysqlStatement* MysqlStatementStub::stmt() { + return _stmt; +} + +inline butil::IOBuf& MysqlStatementStub::execute_data() { + return _execute_data; +} + +inline MysqlStatementStub::NullMask& MysqlStatementStub::null_mask() { + return _null_mask; +} + +inline MysqlStatementStub::ParamTypes& MysqlStatementStub::param_types() { + return _param_types; +} + +inline void MysqlStatementStub::save_long_data(uint16_t param_id, const butil::StringPiece& value) { + LongData d; + d.param_id = param_id; + d.long_data.append(value.data(), value.size()); + _long_data.push_back(d); +} + +class MysqlRequest : public ::google::protobuf::Message { +public: + MysqlRequest(); + MysqlRequest(const MysqlTransaction* tx); + MysqlRequest(MysqlStatement* stmt); + MysqlRequest(const MysqlTransaction* tx, MysqlStatement* stmt); + virtual ~MysqlRequest(); + MysqlRequest(const MysqlRequest& from); + inline MysqlRequest& operator=(const MysqlRequest& from) { + CopyFrom(from); + return *this; + } + void Swap(MysqlRequest* other); + + // Serialize the request into `buf'. Return true on success. + bool SerializeTo(butil::IOBuf* buf) const; + + // Protobuf methods. + MysqlRequest* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const MysqlRequest& from); + void MergeFrom(const MysqlRequest& from); + void Clear(); + + int ByteSize() const; + bool MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* output) const; + int GetCachedSize() const { + return _cached_size_; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const MysqlRequest& default_instance(); + ::google::protobuf::Metadata GetMetadata() const; + + // call query command + bool Query(const butil::StringPiece& command); + // add statement params + bool AddParam(int8_t p); + bool AddParam(uint8_t p); + bool AddParam(int16_t p); + bool AddParam(uint16_t p); + bool AddParam(int32_t p); + bool AddParam(uint32_t p); + bool AddParam(int64_t p); + bool AddParam(uint64_t p); + bool AddParam(float p); + bool AddParam(double p); + bool AddParam(const butil::StringPiece& p); + + // True if previous command failed. + bool has_error() const { + return _has_error; + } + + const MysqlTransaction* get_tx() const { + return _tx; + } + + MysqlStatementStub* get_stmt() const { + return _stmt; + } + + void Print(std::ostream&) const; + +private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + + bool _has_command; // request has command + bool _has_error; // previous AddCommand had error + butil::IOBuf _buf; // the serialized request. + mutable int _cached_size_; // ByteSize + const MysqlTransaction* _tx; // transaction + MysqlStatementStub* _stmt; // statement + uint16_t _param_index; // statement param index + + friend void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_impl(); + friend void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + friend void protobuf_AssignDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + friend void protobuf_ShutdownFile_baidu_2frpc_2fmysql_5fbase_2eproto(); + + void InitAsDefaultInstance(); + static MysqlRequest* default_instance_; +}; + +// Response from Mysql. +// Notice that a MysqlResponse instance may contain multiple replies +// due to pipelining. +class MysqlResponse : public ::google::protobuf::Message { +public: + MysqlResponse(); + virtual ~MysqlResponse(); + MysqlResponse(const MysqlResponse& from); + inline MysqlResponse& operator=(const MysqlResponse& from) { + CopyFrom(from); + return *this; + } + void Swap(MysqlResponse* other); + // Parse and consume intact replies from the buf, actual reply size may less then max_count, if + // some command execute failed + // Returns PARSE_OK on success. + // Returns PARSE_ERROR_NOT_ENOUGH_DATA if data in `buf' is not enough to parse. + // Returns PARSE_ERROR_ABSOLUTELY_WRONG if the parsing + // failed. + ParseError ConsumePartialIOBuf(butil::IOBuf& buf, bool is_auth, MysqlStmtType stmt_type); + + // Number of replies in this response. + // (May have more than one reply due to pipeline) + size_t reply_size() const { + return _nreply; + } + + const MysqlReply& reply(size_t index) const { + if (index < reply_size()) { + return (index == 0 ? _first_reply : *_other_replies[index - 1]); + } + static MysqlReply mysql_nil; + return mysql_nil; + } + // implements Message ---------------------------------------------- + + MysqlResponse* New() const; + void CopyFrom(const ::google::protobuf::Message& from); + void MergeFrom(const ::google::protobuf::Message& from); + void CopyFrom(const MysqlResponse& from); + void MergeFrom(const MysqlResponse& from); + void Clear(); + bool IsInitialized() const; + + int ByteSize() const; + bool MergePartialFromCodedStream(::google::protobuf::io::CodedInputStream* input); + void SerializeWithCachedSizes(::google::protobuf::io::CodedOutputStream* output) const; + ::google::protobuf::uint8* SerializeWithCachedSizesToArray( + ::google::protobuf::uint8* output) const; + int GetCachedSize() const { + return 0; + } + + static const ::google::protobuf::Descriptor* descriptor(); + static const MysqlResponse& default_instance(); + ::google::protobuf::Metadata GetMetadata() const; + +private: + void SharedCtor(); + void SharedDtor(); + void SetCachedSize(int size) const; + + MysqlReply _first_reply; + std::vector _other_replies; + butil::Arena _arena; + size_t _nreply; + mutable int _cached_size_; + + friend void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto_impl(); + friend void protobuf_AddDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + friend void protobuf_AssignDesc_baidu_2frpc_2fmysql_5fbase_2eproto(); + friend void protobuf_ShutdownFile_baidu_2frpc_2fmysql_5fbase_2eproto(); + + void InitAsDefaultInstance(); + static MysqlResponse* default_instance_; +}; + +std::ostream& operator<<(std::ostream& os, const MysqlRequest&); +std::ostream& operator<<(std::ostream& os, const MysqlResponse&); + +} // namespace brpc + +#endif // BRPC_MYSQL_H diff --git a/src/brpc/mysql_command.cpp b/src/brpc/mysql_command.cpp new file mode 100644 index 0000000000..c4f4debf6a --- /dev/null +++ b/src/brpc/mysql_command.cpp @@ -0,0 +1,260 @@ +// Copyright (c) 2015 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include "butil/sys_byteorder.h" +#include "butil/logging.h" // LOG() +#include "brpc/mysql_command.h" +#include "brpc/mysql_common.h" +#include "brpc/mysql.h" + +namespace brpc { + +namespace { +const uint32_t max_allowed_packet = 67108864; +const uint32_t max_packet_size = 16777215; + +template +butil::Status MakePacket(butil::IOBuf* outbuf, const H& head, const F& func, const D& data) { + long pkg_len = head.size() + data.size(); + if (pkg_len > max_allowed_packet) { + return butil::Status( + EINVAL, + "[MakePacket] statement size is too big, maxAllowedPacket = %d, pkg_len = %ld", + max_allowed_packet, + pkg_len); + } + uint32_t size, header; + uint8_t seq = 0; + size_t offset = 0; + for (; pkg_len > 0; pkg_len -= max_packet_size, ++seq) { + if (pkg_len > max_packet_size) { + size = max_packet_size; + } else { + size = pkg_len; + } + header = butil::ByteSwapToLE32(size); + ((uint8_t*)&header)[3] = seq; + outbuf->append(&header, 4); + if (seq == 0) { + const uint32_t old_size = outbuf->size(); + outbuf->append(head); + size -= outbuf->size() - old_size; + } + func(outbuf, data, size, offset); + offset += size; + } + + return butil::Status::OK(); +} + +} // namespace + +butil::Status MysqlMakeCommand(butil::IOBuf* outbuf, + const MysqlCommandType type, + const butil::StringPiece& command) { + if (outbuf == NULL || command.size() == 0) { + return butil::Status(EINVAL, "[MysqlMakeCommand] Param[outbuf] or [stmt] is NULL"); + } + auto func = + [](butil::IOBuf* outbuf, const butil::StringPiece& command, size_t size, size_t offset) { + outbuf->append(command.data() + offset, size); + }; + butil::IOBuf head; + head.push_back(type); + return MakePacket(outbuf, head, func, command); +} + +butil::Status MysqlMakeExecutePacket(butil::IOBuf* outbuf, + uint32_t stmt_id, + const butil::IOBuf& edata) { + butil::IOBuf head; // cmd_type + stmt_id + flag + reserved + body_size + head.push_back(MYSQL_COM_STMT_EXECUTE); + const uint32_t si = butil::ByteSwapToLE32(stmt_id); + head.append(&si, 4); + head.push_back('\0'); + head.push_back((char)0x01); + head.push_back('\0'); + head.push_back('\0'); + head.push_back('\0'); + auto func = [](butil::IOBuf* outbuf, const butil::IOBuf& data, size_t size, size_t offset) { + data.append_to(outbuf, size, offset); + }; + return MakePacket(outbuf, head, func, edata); +} + +butil::Status MysqlMakeExecuteData(MysqlStatementStub* stmt, + uint16_t index, + const void* value, + MysqlFieldType type, + bool is_unsigned) { + const uint16_t n = stmt->stmt()->param_count(); + uint32_t long_data_size = max_allowed_packet / (n + 1); + if (long_data_size < 64) { + long_data_size = 64; + } + // if param count is zero finished. + if (n == 0) { + return butil::Status::OK(); + } + butil::IOBuf& buf = stmt->execute_data(); + MysqlStatementStub::NullMask& null_mask = stmt->null_mask(); + MysqlStatementStub::ParamTypes& param_types = stmt->param_types(); + // else param number larger than zero. + if (index >= n) { + LOG(ERROR) << "too many params"; + return butil::Status(EINVAL, "[MysqlMakeExecuteData] too many params"); + } + // reserve null mask and param types packing at first param + if (index == 0) { + const size_t mask_len = (n + 7) / 8; + const size_t types_len = 2 * n; + null_mask.mask.resize(mask_len, 0); + null_mask.area = buf.reserve(mask_len); + buf.push_back((char)0x01); + param_types.types.reserve(types_len); + param_types.area = buf.reserve(types_len); + } + // pack param value + switch (type) { + case MYSQL_FIELD_TYPE_TINY: + if (is_unsigned) { + param_types.types[index + index] = MYSQL_FIELD_TYPE_TINY; + param_types.types[index + index + 1] = 0x80; + } else { + param_types.types[index + index] = MYSQL_FIELD_TYPE_TINY; + param_types.types[index + index + 1] = 0x00; + } + buf.append(value, 1); + break; + case MYSQL_FIELD_TYPE_SHORT: + if (is_unsigned) { + param_types.types[index + index] = MYSQL_FIELD_TYPE_SHORT; + param_types.types[index + index + 1] = 0x80; + } else { + param_types.types[index + index] = MYSQL_FIELD_TYPE_SHORT; + param_types.types[index + index + 1] = 0x00; + } + { + uint16_t v = butil::ByteSwapToLE16(*(uint16_t*)value); + buf.append(&v, 2); + } + break; + case MYSQL_FIELD_TYPE_LONG: + if (is_unsigned) { + param_types.types[index + index] = MYSQL_FIELD_TYPE_LONG; + param_types.types[index + index + 1] = 0x80; + + } else { + param_types.types[index + index] = MYSQL_FIELD_TYPE_LONG; + param_types.types[index + index + 1] = 0x00; + } + { + uint32_t v = butil::ByteSwapToLE32(*(uint32_t*)value); + buf.append(&v, 4); + } + break; + case MYSQL_FIELD_TYPE_LONGLONG: + if (is_unsigned) { + param_types.types[index + index] = MYSQL_FIELD_TYPE_LONGLONG; + param_types.types[index + index + 1] = 0x80; + } else { + param_types.types[index + index] = MYSQL_FIELD_TYPE_LONGLONG; + param_types.types[index + index + 1] = 0x00; + } + { + uint64_t v = butil::ByteSwapToLE64(*(uint64_t*)value); + buf.append(&v, 8); + } + break; + case MYSQL_FIELD_TYPE_FLOAT: + param_types.types[index + index] = MYSQL_FIELD_TYPE_FLOAT; + param_types.types[index + index + 1] = 0x00; + buf.append(value, 4); + break; + case MYSQL_FIELD_TYPE_DOUBLE: + param_types.types[index + index] = MYSQL_FIELD_TYPE_DOUBLE; + param_types.types[index + index + 1] = 0x00; + buf.append(value, 8); + break; + case MYSQL_FIELD_TYPE_STRING: { + const butil::StringPiece* p = (butil::StringPiece*)value; + if (p == NULL || p->data() == NULL) { + param_types.types[index + index] = MYSQL_FIELD_TYPE_NULL; + param_types.types[index + index + 1] = 0x00; + null_mask.mask[index / 8] |= 1 << (index & 7); + } else { + param_types.types[index + index] = MYSQL_FIELD_TYPE_STRING; + param_types.types[index + index + 1] = 0x00; + if (p->size() < long_data_size) { + std::string len = pack_encode_length(p->size()); + buf.append(len); + buf.append(p->data(), p->size()); + } else { + stmt->save_long_data(index, *p); + } + } + } break; + case MYSQL_FIELD_TYPE_NULL: { + param_types.types[index + index] = MYSQL_FIELD_TYPE_NULL; + param_types.types[index + index + 1] = 0x00; + null_mask.mask[index / 8] |= 1 << (index & 7); + } break; + default: + LOG(ERROR) << "wrong param type"; + return butil::Status(EINVAL, "[MysqlMakeExecuteData] wrong param type"); + } + + // all args have been building + if (index + 1 == n) { + buf.unsafe_assign(null_mask.area, null_mask.mask.data()); + buf.unsafe_assign(param_types.area, param_types.types.data()); + } + + return butil::Status::OK(); +} + +butil::Status MysqlMakeLongDataPacket(butil::IOBuf* outbuf, + uint32_t stmt_id, + uint16_t param_id, + const butil::IOBuf& ldata) { + butil::IOBuf head; + head.push_back(MYSQL_COM_STMT_SEND_LONG_DATA); + const uint32_t si = butil::ByteSwapToLE32(stmt_id); + outbuf->append(&si, 4); + const uint16_t pi = butil::ByteSwapToLE16(param_id); + outbuf->append(&pi, 2); + size_t len, pos = 0; + for (size_t pkg_len = ldata.size(); pkg_len > 0; pkg_len -= max_allowed_packet) { + if (pkg_len < max_allowed_packet) { + len = pkg_len; + } else { + len = max_allowed_packet; + } + butil::IOBuf data; + ldata.append_to(&data, len, pos); + pos += pkg_len; + auto func = [](butil::IOBuf* outbuf, const butil::IOBuf& data, size_t size, size_t offset) { + data.append_to(outbuf, size, offset); + }; + auto rc = MakePacket(outbuf, head, func, data); + if (!rc.ok()) { + return rc; + } + } + return butil::Status::OK(); +} + +} // namespace brpc diff --git a/src/brpc/mysql_command.h b/src/brpc/mysql_command.h new file mode 100644 index 0000000000..9d6c65604b --- /dev/null +++ b/src/brpc/mysql_command.h @@ -0,0 +1,92 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_COMMAND_H +#define BRPC_MYSQL_COMMAND_H + +#include +#include "butil/iobuf.h" +#include "butil/status.h" +#include "brpc/mysql_common.h" + +namespace brpc { +// mysql command types +enum MysqlCommandType : unsigned char { + MYSQL_COM_SLEEP, + MYSQL_COM_QUIT, + MYSQL_COM_INIT_DB, + MYSQL_COM_QUERY, + MYSQL_COM_FIELD_LIST, + MYSQL_COM_CREATE_DB, + MYSQL_COM_DROP_DB, + MYSQL_COM_REFRESH, + MYSQL_COM_SHUTDOWN, + MYSQL_COM_STATISTICS, + MYSQL_COM_PROCESS_INFO, + MYSQL_COM_CONNECT, + MYSQL_COM_PROCESS_KILL, + MYSQL_COM_DEBUG, + MYSQL_COM_PING, + MYSQL_COM_TIME, + MYSQL_COM_DELAYED_INSERT, + MYSQL_COM_CHANGE_USER, + MYSQL_COM_BINLOG_DUMP, + MYSQL_COM_TABLE_DUMP, + MYSQL_COM_CONNECT_OUT, + MYSQL_COM_REGISTER_SLAVE, + MYSQL_COM_STMT_PREPARE, + MYSQL_COM_STMT_EXECUTE, + MYSQL_COM_STMT_SEND_LONG_DATA, + MYSQL_COM_STMT_CLOSE, + MYSQL_COM_STMT_RESET, + MYSQL_COM_SET_OPTION, + MYSQL_COM_STMT_FETCH, + MYSQL_COM_DAEMON, + MYSQL_COM_BINLOG_DUMP_GTID, + MYSQL_COM_RESET_CONNECTION, +}; + +butil::Status MysqlMakeCommand(butil::IOBuf* outbuf, + const MysqlCommandType type, + const butil::StringPiece& stmt); + +// Prepared Statement Protocol +// an prepared statement has a unique statement id in one connection (in brpc SocketId), an prepared +// statement can be executed in many connections, so ever connection has a different statement id. +// In bprc, we can only get a connection in the stage of PackXXXRequest which is behind our building +// mysql protocol stage, but building prepared statement need the statement id of a connection, so +// we will need to building this fragment at PackXXXRequest stage. + +// maybe we can Add a wrapper function, call CallMethod many times use bind_sock +class MysqlStatementStub; +// prepared statement execute command header, will be called at PackXXXRequest stage. +butil::Status MysqlMakeExecutePacket(butil::IOBuf* outbuf, + uint32_t stmt_id, + const butil::IOBuf& body); +// prepared statement execute command body, will be called at building mysql protocol stage. +butil::Status MysqlMakeExecuteData(MysqlStatementStub* stmt, + uint16_t index, + const void* value, + MysqlFieldType type, + bool is_unsigned = false); +// prepared statement long data header +butil::Status MysqlMakeLongDataPacket(butil::IOBuf* outbuf, + uint32_t stmt_id, + uint16_t param_id, + const butil::IOBuf& body); + +} // namespace brpc +#endif diff --git a/src/brpc/mysql_common.cpp b/src/brpc/mysql_common.cpp new file mode 100644 index 0000000000..368c0e245f --- /dev/null +++ b/src/brpc/mysql_common.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include "mysql_common.h" + +namespace brpc { + +const char* MysqlDefaultCollation = "utf8mb4_general_ci"; +const char* MysqlBinaryCollation = "binary"; + +const char* MysqlFieldTypeToString(MysqlFieldType type) { + switch (type) { + case MYSQL_FIELD_TYPE_DECIMAL: + case MYSQL_FIELD_TYPE_TINY: + return "tiny"; + case MYSQL_FIELD_TYPE_SHORT: + return "short"; + case MYSQL_FIELD_TYPE_LONG: + return "long"; + case MYSQL_FIELD_TYPE_FLOAT: + return "float"; + case MYSQL_FIELD_TYPE_DOUBLE: + return "double"; + case MYSQL_FIELD_TYPE_NULL: + return "null"; + case MYSQL_FIELD_TYPE_TIMESTAMP: + return "timestamp"; + case MYSQL_FIELD_TYPE_LONGLONG: + return "longlong"; + case MYSQL_FIELD_TYPE_INT24: + return "int24"; + case MYSQL_FIELD_TYPE_DATE: + return "date"; + case MYSQL_FIELD_TYPE_TIME: + return "time"; + case MYSQL_FIELD_TYPE_DATETIME: + return "datetime"; + case MYSQL_FIELD_TYPE_YEAR: + return "year"; + case MYSQL_FIELD_TYPE_NEWDATE: + return "new date"; + case MYSQL_FIELD_TYPE_VARCHAR: + return "varchar"; + case MYSQL_FIELD_TYPE_BIT: + return "bit"; + case MYSQL_FIELD_TYPE_JSON: + return "json"; + case MYSQL_FIELD_TYPE_NEWDECIMAL: + return "new decimal"; + case MYSQL_FIELD_TYPE_ENUM: + return "enum"; + case MYSQL_FIELD_TYPE_SET: + return "set"; + case MYSQL_FIELD_TYPE_TINY_BLOB: + return "tiny blob"; + case MYSQL_FIELD_TYPE_MEDIUM_BLOB: + return "blob"; + case MYSQL_FIELD_TYPE_LONG_BLOB: + return "long blob"; + case MYSQL_FIELD_TYPE_BLOB: + return "blob"; + case MYSQL_FIELD_TYPE_VAR_STRING: + return "var string"; + case MYSQL_FIELD_TYPE_STRING: + return "string"; + case MYSQL_FIELD_TYPE_GEOMETRY: + return "geometry"; + default: + return "Unknown Field Type"; + } +} + +} // namespace brpc diff --git a/src/brpc/mysql_common.h b/src/brpc/mysql_common.h new file mode 100644 index 0000000000..621388137b --- /dev/null +++ b/src/brpc/mysql_common.h @@ -0,0 +1,419 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_COMMON_H +#define BRPC_MYSQL_COMMON_H + +#include +#include +#include "butil/logging.h" // LOG() + +namespace brpc { +// Msql Collation +extern const char* MysqlDefaultCollation; +extern const char* MysqlBinaryCollation; +const std::map MysqlCollations = { + {"big5_chinese_ci", 1}, + {"latin2_czech_cs", 2}, + {"dec8_swedish_ci", 3}, + {"cp850_general_ci", 4}, + {"latin1_german1_ci", 5}, + {"hp8_english_ci", 6}, + {"koi8r_general_ci", 7}, + {"latin1_swedish_ci", 8}, + {"latin2_general_ci", 9}, + {"swe7_swedish_ci", 10}, + {"ascii_general_ci", 11}, + {"ujis_japanese_ci", 12}, + {"sjis_japanese_ci", 13}, + {"cp1251_bulgarian_ci", 14}, + {"latin1_danish_ci", 15}, + {"hebrew_general_ci", 16}, + {"tis620_thai_ci", 18}, + {"euckr_korean_ci", 19}, + {"latin7_estonian_cs", 20}, + {"latin2_hungarian_ci", 21}, + {"koi8u_general_ci", 22}, + {"cp1251_ukrainian_ci", 23}, + {"gb2312_chinese_ci", 24}, + {"greek_general_ci", 25}, + {"cp1250_general_ci", 26}, + {"latin2_croatian_ci", 27}, + {"gbk_chinese_ci", 28}, + {"cp1257_lithuanian_ci", 29}, + {"latin5_turkish_ci", 30}, + {"latin1_german2_ci", 31}, + {"armscii8_general_ci", 32}, + {"utf8_general_ci", 33}, + {"cp1250_czech_cs", 34}, + //{"ucs2_general_ci", 35}, + {"cp866_general_ci", 36}, + {"keybcs2_general_ci", 37}, + {"macce_general_ci", 38}, + {"macroman_general_ci", 39}, + {"cp852_general_ci", 40}, + {"latin7_general_ci", 41}, + {"latin7_general_cs", 42}, + {"macce_bin", 43}, + {"cp1250_croatian_ci", 44}, + {"utf8mb4_general_ci", 45}, + {"utf8mb4_bin", 46}, + {"latin1_bin", 47}, + {"latin1_general_ci", 48}, + {"latin1_general_cs", 49}, + {"cp1251_bin", 50}, + {"cp1251_general_ci", 51}, + {"cp1251_general_cs", 52}, + {"macroman_bin", 53}, + //{"utf16_general_ci", 54}, + //{"utf16_bin", 55}, + //{"utf16le_general_ci", 56}, + {"cp1256_general_ci", 57}, + {"cp1257_bin", 58}, + {"cp1257_general_ci", 59}, + //{"utf32_general_ci", 60}, + //{"utf32_bin", 61}, + //{"utf16le_bin", 62}, + {"binary", 63}, + {"armscii8_bin", 64}, + {"ascii_bin", 65}, + {"cp1250_bin", 66}, + {"cp1256_bin", 67}, + {"cp866_bin", 68}, + {"dec8_bin", 69}, + {"greek_bin", 70}, + {"hebrew_bin", 71}, + {"hp8_bin", 72}, + {"keybcs2_bin", 73}, + {"koi8r_bin", 74}, + {"koi8u_bin", 75}, + {"utf8_tolower_ci", 76}, + {"latin2_bin", 77}, + {"latin5_bin", 78}, + {"latin7_bin", 79}, + {"cp850_bin", 80}, + {"cp852_bin", 81}, + {"swe7_bin", 82}, + {"utf8_bin", 83}, + {"big5_bin", 84}, + {"euckr_bin", 85}, + {"gb2312_bin", 86}, + {"gbk_bin", 87}, + {"sjis_bin", 88}, + {"tis620_bin", 89}, + //"{ucs2_bin", 90}, + {"ujis_bin", 91}, + {"geostd8_general_ci", 92}, + {"geostd8_bin", 93}, + {"latin1_spanish_ci", 94}, + {"cp932_japanese_ci", 95}, + {"cp932_bin", 96}, + {"eucjpms_japanese_ci", 97}, + {"eucjpms_bin", 98}, + {"cp1250_polish_ci", 99}, + // {"utf16_unicode_ci", 101}, + // {"utf16_icelandic_ci", 102}, + // {"utf16_latvian_ci", 103}, + // {"utf16_romanian_ci", 104}, + // {"utf16_slovenian_ci", 105}, + // {"utf16_polish_ci", 106}, + // {"utf16_estonian_ci", 107}, + // {"utf16_spanish_ci", 108}, + // {"utf16_swedish_ci", 109}, + // {"utf16_turkish_ci", 110}, + // {"utf16_czech_ci", 111}, + // {"utf16_danish_ci", 112}, + // {"utf16_lithuanian_ci", 113}, + // {"utf16_slovak_ci", 114}, + // {"utf16_spanish2_ci", 115}, + // {"utf16_roman_ci", 116}, + // {"utf16_persian_ci", 117}, + // {"utf16_esperanto_ci", 118}, + // {"utf16_hungarian_ci", 119}, + // {"utf16_sinhala_ci", 120}, + // {"utf16_german2_ci", 121}, + // {"utf16_croatian_ci", 122}, + // {"utf16_unicode_520_ci", 123}, + // {"utf16_vietnamese_ci", 124}, + // {"ucs2_unicode_ci", 128}, + // {"ucs2_icelandic_ci", 129}, + // {"ucs2_latvian_ci", 130}, + // {"ucs2_romanian_ci", 131}, + // {"ucs2_slovenian_ci", 132}, + // {"ucs2_polish_ci", 133}, + // {"ucs2_estonian_ci", 134}, + // {"ucs2_spanish_ci", 135}, + // {"ucs2_swedish_ci", 136}, + // {"ucs2_turkish_ci", 137}, + // {"ucs2_czech_ci", 138}, + // {"ucs2_danish_ci", 139}, + // {"ucs2_lithuanian_ci", 140}, + // {"ucs2_slovak_ci", 141}, + // {"ucs2_spanish2_ci", 142}, + // {"ucs2_roman_ci", 143}, + // {"ucs2_persian_ci", 144}, + // {"ucs2_esperanto_ci", 145}, + // {"ucs2_hungarian_ci", 146}, + // {"ucs2_sinhala_ci", 147}, + // {"ucs2_german2_ci", 148}, + // {"ucs2_croatian_ci", 149}, + // {"ucs2_unicode_520_ci", 150}, + // {"ucs2_vietnamese_ci", 151}, + // {"ucs2_general_mysql500_ci", 159}, + // {"utf32_unicode_ci", 160}, + // {"utf32_icelandic_ci", 161}, + // {"utf32_latvian_ci", 162}, + // {"utf32_romanian_ci", 163}, + // {"utf32_slovenian_ci", 164}, + // {"utf32_polish_ci", 165}, + // {"utf32_estonian_ci", 166}, + // {"utf32_spanish_ci", 167}, + // {"utf32_swedish_ci", 168}, + // {"utf32_turkish_ci", 169}, + // {"utf32_czech_ci", 170}, + // {"utf32_danish_ci", 171}, + // {"utf32_lithuanian_ci", 172}, + // {"utf32_slovak_ci", 173}, + // {"utf32_spanish2_ci", 174}, + // {"utf32_roman_ci", 175}, + // {"utf32_persian_ci", 176}, + // {"utf32_esperanto_ci", 177}, + // {"utf32_hungarian_ci", 178}, + // {"utf32_sinhala_ci", 179}, + // {"utf32_german2_ci", 180}, + // {"utf32_croatian_ci", 181}, + // {"utf32_unicode_520_ci", 182}, + // {"utf32_vietnamese_ci", 183}, + {"utf8_unicode_ci", 192}, + {"utf8_icelandic_ci", 193}, + {"utf8_latvian_ci", 194}, + {"utf8_romanian_ci", 195}, + {"utf8_slovenian_ci", 196}, + {"utf8_polish_ci", 197}, + {"utf8_estonian_ci", 198}, + {"utf8_spanish_ci", 199}, + {"utf8_swedish_ci", 200}, + {"utf8_turkish_ci", 201}, + {"utf8_czech_ci", 202}, + {"utf8_danish_ci", 203}, + {"utf8_lithuanian_ci", 204}, + {"utf8_slovak_ci", 205}, + {"utf8_spanish2_ci", 206}, + {"utf8_roman_ci", 207}, + {"utf8_persian_ci", 208}, + {"utf8_esperanto_ci", 209}, + {"utf8_hungarian_ci", 210}, + {"utf8_sinhala_ci", 211}, + {"utf8_german2_ci", 212}, + {"utf8_croatian_ci", 213}, + {"utf8_unicode_520_ci", 214}, + {"utf8_vietnamese_ci", 215}, + {"utf8_general_mysql500_ci", 223}, + {"utf8mb4_unicode_ci", 224}, + {"utf8mb4_icelandic_ci", 225}, + {"utf8mb4_latvian_ci", 226}, + {"utf8mb4_romanian_ci", 227}, + {"utf8mb4_slovenian_ci", 228}, + {"utf8mb4_polish_ci", 229}, + {"utf8mb4_estonian_ci", 230}, + {"utf8mb4_spanish_ci", 231}, + {"utf8mb4_swedish_ci", 232}, + {"utf8mb4_turkish_ci", 233}, + {"utf8mb4_czech_ci", 234}, + {"utf8mb4_danish_ci", 235}, + {"utf8mb4_lithuanian_ci", 236}, + {"utf8mb4_slovak_ci", 237}, + {"utf8mb4_spanish2_ci", 238}, + {"utf8mb4_roman_ci", 239}, + {"utf8mb4_persian_ci", 240}, + {"utf8mb4_esperanto_ci", 241}, + {"utf8mb4_hungarian_ci", 242}, + {"utf8mb4_sinhala_ci", 243}, + {"utf8mb4_german2_ci", 244}, + {"utf8mb4_croatian_ci", 245}, + {"utf8mb4_unicode_520_ci", 246}, + {"utf8mb4_vietnamese_ci", 247}, + {"gb18030_chinese_ci", 248}, + {"gb18030_bin", 249}, + {"gb18030_unicode_520_ci", 250}, + {"utf8mb4_0900_ai_ci", 255}, +}; + +enum MysqlFieldType : uint8_t { + MYSQL_FIELD_TYPE_DECIMAL = 0x00, + MYSQL_FIELD_TYPE_TINY = 0x01, + MYSQL_FIELD_TYPE_SHORT = 0x02, + MYSQL_FIELD_TYPE_LONG = 0x03, + MYSQL_FIELD_TYPE_FLOAT = 0x04, + MYSQL_FIELD_TYPE_DOUBLE = 0x05, + MYSQL_FIELD_TYPE_NULL = 0x06, + MYSQL_FIELD_TYPE_TIMESTAMP = 0x07, + MYSQL_FIELD_TYPE_LONGLONG = 0x08, + MYSQL_FIELD_TYPE_INT24 = 0x09, + MYSQL_FIELD_TYPE_DATE = 0x0A, + MYSQL_FIELD_TYPE_TIME = 0x0B, + MYSQL_FIELD_TYPE_DATETIME = 0x0C, + MYSQL_FIELD_TYPE_YEAR = 0x0D, + MYSQL_FIELD_TYPE_NEWDATE = 0x0E, + MYSQL_FIELD_TYPE_VARCHAR = 0x0F, + MYSQL_FIELD_TYPE_BIT = 0x10, + MYSQL_FIELD_TYPE_JSON = 0xF5, + MYSQL_FIELD_TYPE_NEWDECIMAL = 0xF6, + MYSQL_FIELD_TYPE_ENUM = 0xF7, + MYSQL_FIELD_TYPE_SET = 0xF8, + MYSQL_FIELD_TYPE_TINY_BLOB = 0xF9, + MYSQL_FIELD_TYPE_MEDIUM_BLOB = 0xFA, + MYSQL_FIELD_TYPE_LONG_BLOB = 0xFB, + MYSQL_FIELD_TYPE_BLOB = 0xFC, + MYSQL_FIELD_TYPE_VAR_STRING = 0xFD, + MYSQL_FIELD_TYPE_STRING = 0xFE, + MYSQL_FIELD_TYPE_GEOMETRY = 0xFF, +}; + +enum MysqlFieldFlag : uint16_t { + MYSQL_NOT_NULL_FLAG = 0x0001, + MYSQL_PRI_KEY_FLAG = 0x0002, + MYSQL_UNIQUE_KEY_FLAG = 0x0004, + MYSQL_MULTIPLE_KEY_FLAG = 0x0008, + MYSQL_BLOB_FLAG = 0x0010, + MYSQL_UNSIGNED_FLAG = 0x0020, + MYSQL_ZEROFILL_FLAG = 0x0040, + MYSQL_BINARY_FLAG = 0x0080, + MYSQL_ENUM_FLAG = 0x0100, + MYSQL_AUTO_INCREMENT_FLAG = 0x0200, + MYSQL_TIMESTAMP_FLAG = 0x0400, + MYSQL_SET_FLAG = 0x0800, +}; + +enum MysqlServerStatus : uint16_t { + MYSQL_SERVER_STATUS_IN_TRANS = 1, + MYSQL_SERVER_STATUS_AUTOCOMMIT = 2, /* Server in auto_commit mode */ + MYSQL_SERVER_MORE_RESULTS_EXISTS = 8, /* Multi query - next query exists */ + MYSQL_SERVER_QUERY_NO_GOOD_INDEX_USED = 16, + MYSQL_SERVER_QUERY_NO_INDEX_USED = 32, + /** + The server was able to fulfill the clients request and opened a + read-only non-scrollable cursor for a query. This flag comes + in reply to COM_STMT_EXECUTE and COM_STMT_FETCH commands. + */ + MYSQL_SERVER_STATUS_CURSOR_EXISTS = 64, + /** + This flag is sent when a read-only cursor is exhausted, in reply to + COM_STMT_FETCH command. + */ + MYSQL_SERVER_STATUS_LAST_ROW_SENT = 128, + MYSQL_SERVER_STATUS_DB_DROPPED = 256, /* A database was dropped */ + MYSQL_SERVER_STATUS_NO_BACKSLASH_ESCAPES = 512, + /** + Sent to the client if after a prepared statement reprepare + we discovered that the new statement returns a different + number of result set columns. + */ + MYSQL_SERVER_STATUS_METADATA_CHANGED = 1024, + MYSQL_SERVER_QUERY_WAS_SLOW = 2048, + + /** + To mark ResultSet containing output parameter values. + */ + MYSQL_SERVER_PS_OUT_PARAMS = 4096, + + /** + Set at the same time as MYSQL_SERVER_STATUS_IN_TRANS if the started + multi-statement transaction is a read-only transaction. Cleared + when the transaction commits or aborts. Since this flag is sent + to clients in OK and EOF packets, the flag indicates the + transaction status at the end of command execution. + */ + MYSQL_SERVER_STATUS_IN_TRANS_READONLY = 8192, + MYSQL_SERVER_SESSION_STATE_CHANGED = 1UL << 14, +}; + +// 1. normal statement 2. prepared statement 3. need prepare statement +enum MysqlStmtType : uint32_t { + MYSQL_NORMAL_STATEMENT = 1, + MYSQL_PREPARED_STATEMENT = 2, + MYSQL_NEED_PREPARE = 3, +}; + +const char* MysqlFieldTypeToString(MysqlFieldType); + +inline std::string pack_encode_length(const uint64_t value) { + std::stringstream ss; + if (value <= 250) { + ss.put((char)value); + } else if (value <= 0xffff) { + ss.put((char)0xfc).put((char)value).put((char)(value >> 8)); + } else if (value <= 0xffffff) { + ss.put((char)0xfd).put((char)value).put((char)(value >> 8)).put((char)(value >> 16)); + } else { + ss.put((char)0xfd) + .put((char)value) + .put((char)(value >> 8)) + .put((char)(value >> 16)) + .put((char)(value >> 24)) + .put((char)(value >> 32)) + .put((char)(value >> 40)) + .put((char)(value >> 48)) + .put((char)(value >> 56)); + } + return ss.str(); +} + +// little endian order to host order +#if !defined(ARCH_CPU_LITTLE_ENDIAN) + +inline uint16_t mysql_uint2korr(const uint8_t* A) { + return (uint16_t)(((uint16_t)(A[0])) + ((uint16_t)(A[1]) << 8)); +} + +inline uint32_t mysql_uint3korr(const uint8_t* A) { + return (uint32_t)(((uint32_t)(A[0])) + (((uint32_t)(A[1])) << 8) + (((uint32_t)(A[2])) << 16)); +} + +inline uint32_t mysql_uint4korr(const uint8_t* A) { + return (uint32_t)(((uint32_t)(A[0])) + (((uint32_t)(A[1])) << 8) + (((uint32_t)(A[2])) << 16) + + (((uint32_t)(A[3])) << 24)); +} + +inline uint64_t mysql_uint8korr(const uint8_t* A) { + return (uint64_t)(((uint64_t)(A[0])) + (((uint64_t)(A[1])) << 8) + (((uint64_t)(A[2])) << 16) + + (((uint64_t)(A[3])) << 24) + (((uint64_t)(A[4])) << 32) + + (((uint64_t)(A[5])) << 40) + (((uint64_t)(A[6])) << 48) + + (((uint64_t)(A[7])) << 56)); +} + +#else + +inline uint16_t mysql_uint2korr(const uint8_t* A) { + return *(uint16_t*)A; +} + +inline uint32_t mysql_uint3korr(const uint8_t* A) { + return (uint32_t)(((uint32_t)(A[0])) + (((uint32_t)(A[1])) << 8) + (((uint32_t)(A[2])) << 16)); +} + +inline uint32_t mysql_uint4korr(const uint8_t* A) { + return *(uint32_t*)A; +} + +inline uint64_t mysql_uint8korr(const uint8_t* A) { + return *(uint64_t*)A; +} + +#endif + +} // namespace brpc +#endif diff --git a/src/brpc/mysql_reply.cpp b/src/brpc/mysql_reply.cpp new file mode 100644 index 0000000000..78b96d0c22 --- /dev/null +++ b/src/brpc/mysql_reply.cpp @@ -0,0 +1,1189 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include "brpc/mysql_common.h" +#include "brpc/mysql_reply.h" + +namespace brpc { + +#define MY_ALLOC_CHECK(expr) \ + do { \ + if ((expr) == false) { \ + return PARSE_ERROR_ABSOLUTELY_WRONG; \ + } \ + } while (0) + +#define MY_PARSE_CHECK(expr) \ + do { \ + ParseError rc = (expr); \ + if (rc != PARSE_OK) { \ + return rc; \ + } \ + } while (0) + +template +inline bool my_alloc_check(butil::Arena* arena, const size_t n, Type*& pointer) { + if (pointer == NULL) { + pointer = (Type*)arena->allocate(sizeof(Type) * n); + if (pointer == NULL) { + return false; + } + for (size_t i = 0; i < n; ++i) { + new (pointer + i) Type; + } + } + return true; +} + +template <> +inline bool my_alloc_check(butil::Arena* arena, const size_t n, char*& pointer) { + if (pointer == NULL) { + pointer = (char*)arena->allocate(sizeof(char) * n); + if (pointer == NULL) { + return false; + } + } + return true; +} + +namespace { +struct MysqlHeader { + uint32_t payload_size; + uint32_t seq; +}; +const char* digits01 = + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123" + "456789"; +const char* digits10 = + "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999" + "999999"; +} // namespace + +const char* MysqlRspTypeToString(MysqlRspType type) { + switch (type) { + case MYSQL_RSP_OK: + return "ok"; + case MYSQL_RSP_ERROR: + return "error"; + case MYSQL_RSP_RESULTSET: + return "resultset"; + case MYSQL_RSP_EOF: + return "eof"; + case MYSQL_RSP_AUTH: + return "auth"; + case MYSQL_RSP_PREPARE_OK: + return "prepare_ok"; + default: + return "Unknown Response Type"; + } +} + +// check if the buf is contain a full package +inline bool is_full_package(const butil::IOBuf& buf) { + uint8_t header[4]; + const uint8_t* p = (const uint8_t*)buf.fetch(header, sizeof(header)); + if (p == NULL) { + return false; + } + uint32_t payload_size = mysql_uint3korr(p); + if (buf.size() < payload_size + 4) { + return false; + } + return true; +} +// if is eof package +inline bool is_an_eof(const butil::IOBuf& buf) { + uint8_t tmp[5]; + const uint8_t* p = (const uint8_t*)buf.fetch(tmp, sizeof(tmp)); + if (p == NULL) { + return false; + } + uint8_t type = p[4]; + if (type == MYSQL_RSP_EOF) { + return true; + } else { + return false; + } +} +// parse header +inline bool parse_header(butil::IOBuf& buf, MysqlHeader* value) { + if (!is_full_package(buf)) { + return false; + } + { + uint8_t tmp[3]; + buf.cutn(tmp, sizeof(tmp)); + value->payload_size = mysql_uint3korr(tmp); + } + { + uint8_t tmp; + buf.cut1((char*)&tmp); + value->seq = tmp; + } + return true; +} +// use this carefully, we depending on parse_header for checking IOBuf contain full package +inline uint64_t parse_encode_length(butil::IOBuf& buf) { + if (buf.size() == 0) { + return 0; + } + + uint64_t value = 0; + uint8_t f = 0; + buf.cut1((char*)&f); + if (f <= 250) { + value = f; + } else if (f == 251) { + value = 0; + } else if (f == 252) { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + value = mysql_uint2korr(tmp); + } else if (f == 253) { + uint8_t tmp[3]; + buf.cutn(tmp, sizeof(tmp)); + value = mysql_uint3korr(tmp); + } else if (f == 254) { + uint8_t tmp[8]; + buf.cutn(tmp, sizeof(tmp)); + value = mysql_uint8korr(tmp); + } + return value; +} + +ParseError MysqlReply::ConsumePartialIOBuf(butil::IOBuf& buf, + butil::Arena* arena, + bool is_auth, + MysqlStmtType stmt_type, + bool* more_results) { + *more_results = false; + if (!is_full_package(buf)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + uint8_t header[4 + 1]; // use the extra byte to judge message type + const uint8_t* p = (const uint8_t*)buf.fetch(header, sizeof(header)); + uint8_t type = (_type == MYSQL_RSP_UNKNOWN) ? p[4] : (uint8_t)_type; + if (is_auth && type != 0x00 && type != 0xFF) { + _type = MYSQL_RSP_AUTH; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.auth)); + MY_PARSE_CHECK(_data.auth->Parse(buf, arena)); + return PARSE_OK; + } + if (type == 0x00 && (is_auth || stmt_type != MYSQL_NEED_PREPARE)) { + _type = MYSQL_RSP_OK; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.ok)); + MY_PARSE_CHECK(_data.ok->Parse(buf, arena)); + *more_results = _data.ok->status() & MYSQL_SERVER_MORE_RESULTS_EXISTS; + } else if ((type == 0x00 && stmt_type == MYSQL_NEED_PREPARE) || type == MYSQL_RSP_PREPARE_OK) { + _type = MYSQL_RSP_PREPARE_OK; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.prepare_ok)); + MY_PARSE_CHECK(_data.prepare_ok->Parse(buf, arena)); + } else if (type == 0xFF) { + _type = MYSQL_RSP_ERROR; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.error)); + MY_PARSE_CHECK(_data.error->Parse(buf, arena)); + } else if (type == 0xFE) { + _type = MYSQL_RSP_EOF; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.eof)); + MY_PARSE_CHECK(_data.eof->Parse(buf)); + *more_results = _data.eof->status() & MYSQL_SERVER_MORE_RESULTS_EXISTS; + } else if (type >= 0x01 && type <= 0xFA) { + _type = MYSQL_RSP_RESULTSET; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, _data.result_set)); + MY_PARSE_CHECK(_data.result_set->Parse(buf, arena, !(stmt_type == MYSQL_NORMAL_STATEMENT))); + *more_results = _data.result_set->_eof2.status() & MYSQL_SERVER_MORE_RESULTS_EXISTS; + } else { + LOG(ERROR) << "Unknown Response Type " + << "type=" << unsigned(type) << " buf_size=" << buf.size(); + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + return PARSE_OK; +} + +void MysqlReply::Print(std::ostream& os) const { + if (_type == MYSQL_RSP_AUTH) { + const Auth& auth = *_data.auth; + os << "\nprotocol:" << (unsigned)auth._protocol << "\nversion:" << auth._version + << "\nthread_id:" << auth._thread_id << "\nsalt:" << auth._salt + << "\ncapacity:" << auth._capability << "\nlanguage:" << (unsigned)auth._collation + << "\nstatus:" << auth._status << "\nextended_capacity:" << auth._extended_capability + << "\nauth_plugin_length:" << auth._auth_plugin_length << "\nsalt2:" << auth._salt2 + << "\nauth_plugin:" << auth._auth_plugin; + } else if (_type == MYSQL_RSP_OK) { + const Ok& ok = *_data.ok; + os << "\naffect_row:" << ok._affect_row << "\nindex:" << ok._index + << "\nstatus:" << ok._status << "\nwarning:" << ok._warning << "\nmessage:" << ok._msg; + } else if (_type == MYSQL_RSP_ERROR) { + const Error& err = *_data.error; + os << "\nerrcode:" << err._errcode << "\nstatus:" << err._status + << "\nmessage:" << err._msg; + } else if (_type == MYSQL_RSP_RESULTSET) { + const ResultSet& r = *_data.result_set; + os << "\nheader.column_count:" << r._header._column_count; + for (uint64_t i = 0; i < r._header._column_count; ++i) { + os << "\ncolumn[" << i << "].catalog:" << r._columns[i]._catalog << "\ncolumn[" << i + << "].database:" << r._columns[i]._database << "\ncolumn[" << i + << "].table:" << r._columns[i]._table << "\ncolumn[" << i + << "].origin_table:" << r._columns[i]._origin_table << "\ncolumn[" << i + << "].name:" << r._columns[i]._name << "\ncolumn[" << i + << "].origin_name:" << r._columns[i]._origin_name << "\ncolumn[" << i + << "].charset:" << (uint16_t)r._columns[i]._charset << "\ncolumn[" << i + << "].length:" << r._columns[i]._length << "\ncolumn[" << i + << "].type:" << (unsigned)r._columns[i]._type << "\ncolumn[" << i + << "].flag:" << (unsigned)r._columns[i]._flag << "\ncolumn[" << i + << "].decimal:" << (unsigned)r._columns[i]._decimal; + } + os << "\neof1.warning:" << r._eof1._warning; + os << "\neof1.status:" << r._eof1._status; + int n = 0; + for (const Row* row = r._first->_next; row != r._last->_next; row = row->_next) { + os << "\nrow(" << n++ << "):"; + for (uint64_t j = 0; j < r._header._column_count; ++j) { + if (row->field(j).is_nil()) { + os << "NULL\t"; + continue; + } + switch (row->field(j)._type) { + case MYSQL_FIELD_TYPE_NULL: + os << "NULL"; + break; + case MYSQL_FIELD_TYPE_TINY: + if (r._columns[j]._flag & MYSQL_UNSIGNED_FLAG) { + os << unsigned(row->field(j).tiny()); + } else { + os << signed(row->field(j).stiny()); + } + break; + case MYSQL_FIELD_TYPE_SHORT: + case MYSQL_FIELD_TYPE_YEAR: + if (r._columns[j]._flag & MYSQL_UNSIGNED_FLAG) { + os << unsigned(row->field(j).small()); + } else { + os << signed(row->field(j).ssmall()); + } + break; + case MYSQL_FIELD_TYPE_INT24: + case MYSQL_FIELD_TYPE_LONG: + if (r._columns[j]._flag & MYSQL_UNSIGNED_FLAG) { + os << row->field(j).integer(); + } else { + os << row->field(j).sinteger(); + } + break; + case MYSQL_FIELD_TYPE_LONGLONG: + if (r._columns[j]._flag & MYSQL_UNSIGNED_FLAG) { + os << row->field(j).bigint(); + } else { + os << row->field(j).sbigint(); + } + break; + case MYSQL_FIELD_TYPE_FLOAT: + os << row->field(j).float32(); + break; + case MYSQL_FIELD_TYPE_DOUBLE: + os << row->field(j).float64(); + break; + case MYSQL_FIELD_TYPE_DECIMAL: + case MYSQL_FIELD_TYPE_NEWDECIMAL: + case MYSQL_FIELD_TYPE_VARCHAR: + case MYSQL_FIELD_TYPE_BIT: + case MYSQL_FIELD_TYPE_ENUM: + case MYSQL_FIELD_TYPE_SET: + case MYSQL_FIELD_TYPE_TINY_BLOB: + case MYSQL_FIELD_TYPE_MEDIUM_BLOB: + case MYSQL_FIELD_TYPE_LONG_BLOB: + case MYSQL_FIELD_TYPE_BLOB: + case MYSQL_FIELD_TYPE_VAR_STRING: + case MYSQL_FIELD_TYPE_STRING: + case MYSQL_FIELD_TYPE_GEOMETRY: + case MYSQL_FIELD_TYPE_JSON: + case MYSQL_FIELD_TYPE_TIME: + case MYSQL_FIELD_TYPE_DATE: + case MYSQL_FIELD_TYPE_NEWDATE: + case MYSQL_FIELD_TYPE_TIMESTAMP: + case MYSQL_FIELD_TYPE_DATETIME: + os << row->field(j).string(); + break; + default: + os << "Unknown field type"; + } + os << "\t"; + } + } + os << "\neof2.warning:" << r._eof2._warning; + os << "\neof2.status:" << r._eof2._status; + } else if (_type == MYSQL_RSP_EOF) { + const Eof& e = *_data.eof; + os << "\nwarning:" << e._warning << "\nstatus:" << e._status; + } else if (_type == MYSQL_RSP_PREPARE_OK) { + const PrepareOk& prep = *_data.prepare_ok; + os << "\nstmt_id:" << prep._header._stmt_id + << "\ncolumn_count:" << prep._header._column_count + << "\nparam_count:" << prep._header._param_count; + for (uint16_t i = 0; i < prep._header._param_count; ++i) { + os << "\nparam[" << i << "].catalog:" << prep._params[i]._catalog << "\nparam[" << i + << "].database:" << prep._params[i]._database << "\nparam[" << i + << "].table:" << prep._params[i]._table << "\nparam[" << i + << "].origin_table:" << prep._params[i]._origin_table << "\nparam[" << i + << "].name:" << prep._params[i]._name << "\nparam[" << i + << "].origin_name:" << prep._params[i]._origin_name << "\nparam[" << i + << "].charset:" << (uint16_t)prep._params[i]._charset << "\nparam[" << i + << "].length:" << prep._params[i]._length << "\nparam[" << i + << "].type:" << (unsigned)prep._params[i]._type << "\nparam[" << i + << "].flag:" << (unsigned)prep._params[i]._flag << "\nparam[" << i + << "].decimal:" << (unsigned)prep._params[i]._decimal; + } + for (uint16_t i = 0; i < prep._header._column_count; ++i) { + os << "\ncolumn[" << i << "].catalog:" << prep._columns[i]._catalog << "\ncolumn[" << i + << "].database:" << prep._columns[i]._database << "\ncolumn[" << i + << "].table:" << prep._columns[i]._table << "\ncolumn[" << i + << "].origin_table:" << prep._columns[i]._origin_table << "\ncolumn[" << i + << "].name:" << prep._columns[i]._name << "\ncolumn[" << i + << "].origin_name:" << prep._columns[i]._origin_name << "\ncolumn[" << i + << "].charset:" << (uint16_t)prep._columns[i]._charset << "\ncolumn[" << i + << "].length:" << prep._columns[i]._length << "\ncolumn[" << i + << "].type:" << (unsigned)prep._columns[i]._type << "\ncolumn[" << i + << "].flag:" << (unsigned)prep._columns[i]._flag << "\ncolumn[" << i + << "].decimal:" << (unsigned)prep._columns[i]._decimal; + } + } else { + os << "Unknown response type"; + } +} + +ParseError MysqlReply::Auth::Parse(butil::IOBuf& buf, butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + const std::string delim(1, 0x00); + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + buf.cut1((char*)&_protocol); + { + butil::IOBuf version; + buf.cut_until(&version, delim); + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, version.size(), d)); + version.copy_to(d); + _version.set(d, version.size()); + } + { + uint8_t tmp[4]; + buf.cutn(tmp, sizeof(tmp)); + _thread_id = mysql_uint4korr(tmp); + } + { + butil::IOBuf salt; + buf.cut_until(&salt, delim); + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, salt.size(), d)); + salt.copy_to(d); + _salt.set(d, salt.size()); + } + { + uint8_t tmp[2]; + buf.cutn(&tmp, sizeof(tmp)); + _capability = mysql_uint2korr(tmp); + } + buf.cut1((char*)&_collation); + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _status = mysql_uint2korr(tmp); + } + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _extended_capability = mysql_uint2korr(tmp); + } + buf.cut1((char*)&_auth_plugin_length); + buf.pop_front(10); + { + butil::IOBuf salt2; + buf.cut_until(&salt2, delim); + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, salt2.size(), d)); + salt2.copy_to(d); + _salt2.set(d, salt2.size()); + } + { + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, _auth_plugin_length, d)); + buf.cutn(d, _auth_plugin_length); + _auth_plugin.set(d, _auth_plugin_length); + } + buf.clear(); // consume all buf + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::ResultSetHeader::Parse(butil::IOBuf& buf) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + uint64_t old_size, new_size; + old_size = buf.size(); + _column_count = parse_encode_length(buf); + new_size = buf.size(); + if (old_size - new_size < header.payload_size) { + _extra_msg = parse_encode_length(buf); + } else { + _extra_msg = 0; + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Column::Parse(butil::IOBuf& buf, butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + + uint64_t len = parse_encode_length(buf); + char* catalog = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, catalog)); + buf.cutn(catalog, len); + _catalog.set(catalog, len); + + len = parse_encode_length(buf); + char* database = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, database)); + buf.cutn(database, len); + _database.set(database, len); + + len = parse_encode_length(buf); + char* table = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, table)); + buf.cutn(table, len); + _table.set(table, len); + + len = parse_encode_length(buf); + char* origin_table = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, origin_table)); + buf.cutn(origin_table, len); + _origin_table.set(origin_table, len); + + len = parse_encode_length(buf); + char* name = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, name)); + buf.cutn(name, len); + _name.set(name, len); + + len = parse_encode_length(buf); + char* origin_name = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, origin_name)); + buf.cutn(origin_name, len); + _origin_name.set(origin_name, len); + buf.pop_front(1); + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _charset = mysql_uint2korr(tmp); + } + { + uint8_t tmp[4]; + buf.cutn(tmp, sizeof(tmp)); + _length = mysql_uint4korr(tmp); + } + buf.cut1((char*)&_type); + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _flag = (MysqlFieldFlag)mysql_uint2korr(tmp); + } + buf.cut1((char*)&_decimal); + buf.pop_front(2); + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Ok::Parse(butil::IOBuf& buf, butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + + uint64_t old_size, new_size; + old_size = buf.size(); + buf.pop_front(1); + + _affect_row = parse_encode_length(buf); + _index = parse_encode_length(buf); + buf.cutn(&_status, 2); + buf.cutn(&_warning, 2); + + new_size = buf.size(); + if (old_size - new_size < header.payload_size) { + const int64_t len = header.payload_size - (old_size - new_size); + char* msg = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, msg)); + buf.cutn(msg, len); + _msg.set(msg, len); + // buf.pop_front(1); // Null + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Eof::Parse(butil::IOBuf& buf) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + buf.pop_front(1); + buf.cutn(&_warning, 2); + buf.cutn(&_status, 2); + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Error::Parse(butil::IOBuf& buf, butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + buf.pop_front(1); // 0xFF + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _errcode = mysql_uint2korr(tmp); + } + buf.pop_front(1); // '#' + // 5 byte server status + char* status = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, 5, status)); + buf.cutn(status, 5); + _status.set(status, 5); + // error message, Null-Terminated string + uint64_t len = header.payload_size - 9; + char* msg = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, msg)); + buf.cutn(msg, len); + _msg.set(msg, len); + // buf.pop_front(1); // Null + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Row::Parse(butil::IOBuf& buf, + const MysqlReply::Column* columns, + uint64_t column_count, + MysqlReply::Field* fields, + bool binary, + butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + if (!binary) { // mysql text protocol + for (uint64_t i = 0; i < column_count; ++i) { + MY_PARSE_CHECK(fields[i].Parse(buf, columns + i, arena)); + } + } else { // mysql binary protocol + uint8_t hdr = 0; + buf.cut1((char*)&hdr); + if (hdr != 0x00) { + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + // NULL-bitmap, [(column-count + 7 + 2) / 8 bytes] + const uint64_t size = ((column_count + 7 + 2) >> 3); + uint8_t null_mask[size]; + for (size_t i = 0; i < sizeof(null_mask); ++i) { + null_mask[i] = 0; + } + buf.cutn(null_mask, size); + for (uint64_t i = 0; i < column_count; ++i) { + MY_PARSE_CHECK(fields[i].Parse(buf, columns + i, i, column_count, null_mask, arena)); + } + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Field::Parse(butil::IOBuf& buf, + const MysqlReply::Column* column, + butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + // field type + _type = column->_type; + // is unsigned flag set + _unsigned = column->_flag & MYSQL_UNSIGNED_FLAG; + // parse encode length + const uint64_t len = parse_encode_length(buf); + // is it null? + if (len == 0 && !(column->_flag & MYSQL_NOT_NULL_FLAG)) { + _is_nil = true; + set_parsed(); + return PARSE_OK; + } + // field is not null + butil::IOBuf str; + buf.cutn(&str, len); + switch (_type) { + case MYSQL_FIELD_TYPE_NULL: + _is_nil = true; + break; + case MYSQL_FIELD_TYPE_TINY: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + _data.tiny = strtoul(str.to_string().c_str(), NULL, 10); + } else { + _data.stiny = strtol(str.to_string().c_str(), NULL, 10); + } + break; + case MYSQL_FIELD_TYPE_SHORT: + case MYSQL_FIELD_TYPE_YEAR: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + _data.small = strtoul(str.to_string().c_str(), NULL, 10); + } else { + _data.ssmall = strtol(str.to_string().c_str(), NULL, 10); + } + break; + case MYSQL_FIELD_TYPE_INT24: + case MYSQL_FIELD_TYPE_LONG: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + _data.integer = strtoul(str.to_string().c_str(), NULL, 10); + } else { + _data.sinteger = strtol(str.to_string().c_str(), NULL, 10); + } + break; + case MYSQL_FIELD_TYPE_LONGLONG: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + _data.bigint = strtoul(str.to_string().c_str(), NULL, 10); + } else { + _data.sbigint = strtol(str.to_string().c_str(), NULL, 10); + } + break; + case MYSQL_FIELD_TYPE_FLOAT: + _data.float32 = strtof(str.to_string().c_str(), NULL); + break; + case MYSQL_FIELD_TYPE_DOUBLE: + _data.float64 = strtod(str.to_string().c_str(), NULL); + break; + case MYSQL_FIELD_TYPE_DECIMAL: + case MYSQL_FIELD_TYPE_NEWDECIMAL: + case MYSQL_FIELD_TYPE_VARCHAR: + case MYSQL_FIELD_TYPE_BIT: + case MYSQL_FIELD_TYPE_ENUM: + case MYSQL_FIELD_TYPE_SET: + case MYSQL_FIELD_TYPE_TINY_BLOB: + case MYSQL_FIELD_TYPE_MEDIUM_BLOB: + case MYSQL_FIELD_TYPE_LONG_BLOB: + case MYSQL_FIELD_TYPE_BLOB: + case MYSQL_FIELD_TYPE_VAR_STRING: + case MYSQL_FIELD_TYPE_STRING: + case MYSQL_FIELD_TYPE_GEOMETRY: + case MYSQL_FIELD_TYPE_JSON: + case MYSQL_FIELD_TYPE_TIME: + case MYSQL_FIELD_TYPE_DATE: + case MYSQL_FIELD_TYPE_NEWDATE: + case MYSQL_FIELD_TYPE_TIMESTAMP: + case MYSQL_FIELD_TYPE_DATETIME: { + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, d)); + str.copy_to(d); + _data.str.set(d, len); + } break; + default: + LOG(ERROR) << "Unknown field type"; + set_parsed(); + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Field::Parse(butil::IOBuf& buf, + const MysqlReply::Column* column, + uint64_t column_index, + uint64_t column_count, + const uint8_t* null_mask, + butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + // field type + _type = column->_type; + // is unsigned flag set + _unsigned = column->_flag & MYSQL_UNSIGNED_FLAG; + // (byte >> bit-pos) % 2 == 1 + if (((null_mask[(column_index + 2) >> 3] >> ((column_index + 2) & 7)) & 1) == 1) { + _is_nil = true; + set_parsed(); + return PARSE_OK; + } + + switch (_type) { + case MYSQL_FIELD_TYPE_NULL: + _is_nil = true; + break; + case MYSQL_FIELD_TYPE_TINY: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + buf.cut1((char*)&_data.tiny); + } else { + buf.cut1((char*)&_data.stiny); + } + break; + case MYSQL_FIELD_TYPE_SHORT: + case MYSQL_FIELD_TYPE_YEAR: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + uint8_t* p = (uint8_t*)&_data.small; + buf.cutn(p, 2); + _data.small = mysql_uint2korr(p); + } else { + uint8_t* p = (uint8_t*)&_data.ssmall; + buf.cutn(p, 2); + _data.ssmall = (int16_t)mysql_uint2korr(p); + } + break; + case MYSQL_FIELD_TYPE_INT24: + case MYSQL_FIELD_TYPE_LONG: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + uint8_t* p = (uint8_t*)&_data.integer; + buf.cutn(p, 4); + _data.integer = mysql_uint4korr(p); + } else { + uint8_t* p = (uint8_t*)&_data.sinteger; + buf.cutn(p, 4); + _data.sinteger = (int32_t)mysql_uint4korr(p); + } + break; + case MYSQL_FIELD_TYPE_LONGLONG: + if (column->_flag & MYSQL_UNSIGNED_FLAG) { + uint8_t* p = (uint8_t*)&_data.bigint; + buf.cutn(p, 8); + _data.bigint = mysql_uint8korr(p); + } else { + uint8_t* p = (uint8_t*)&_data.sbigint; + buf.cutn(p, 8); + _data.sbigint = (int64_t)mysql_uint8korr(p); + } + break; + case MYSQL_FIELD_TYPE_FLOAT: { + uint8_t* p = (uint8_t*)&_data.float32; + buf.cutn(p, 4); + } break; + case MYSQL_FIELD_TYPE_DOUBLE: { + uint8_t* p = (uint8_t*)&_data.float64; + buf.cutn(p, 8); + } break; + case MYSQL_FIELD_TYPE_DECIMAL: + case MYSQL_FIELD_TYPE_NEWDECIMAL: + case MYSQL_FIELD_TYPE_VARCHAR: + case MYSQL_FIELD_TYPE_BIT: + case MYSQL_FIELD_TYPE_ENUM: + case MYSQL_FIELD_TYPE_SET: + case MYSQL_FIELD_TYPE_TINY_BLOB: + case MYSQL_FIELD_TYPE_MEDIUM_BLOB: + case MYSQL_FIELD_TYPE_LONG_BLOB: + case MYSQL_FIELD_TYPE_BLOB: + case MYSQL_FIELD_TYPE_VAR_STRING: + case MYSQL_FIELD_TYPE_STRING: + case MYSQL_FIELD_TYPE_GEOMETRY: + case MYSQL_FIELD_TYPE_JSON: { + const uint64_t len = parse_encode_length(buf); + // is it null? + if (len == 0 && !(column->_flag & MYSQL_NOT_NULL_FLAG)) { + _is_nil = true; + set_parsed(); + return PARSE_OK; + } + // field is not null + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, len, d)); + buf.cutn(d, len); + _data.str.set(d, len); + } break; + case MYSQL_FIELD_TYPE_NEWDATE: // Date YYYY-MM-DD + case MYSQL_FIELD_TYPE_DATE: // Date YYYY-MM-DD + case MYSQL_FIELD_TYPE_DATETIME: // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] + case MYSQL_FIELD_TYPE_TIMESTAMP: { // Timestamp YYYY-MM-DD HH:MM:SS[.fractal] + ParseError rc = ParseBinaryDataTime(buf, column, _data.str, arena); + if (rc != PARSE_OK) { + return rc; + } + } break; + case MYSQL_FIELD_TYPE_TIME: { // Time [-][H]HH:MM:SS[.fractal] + ParseError rc = ParseBinaryTime(buf, column, _data.str, arena); + if (rc != PARSE_OK) { + return rc; + } + } break; + default: + LOG(ERROR) << "Unknown field type"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::Field::ParseBinaryTime(butil::IOBuf& buf, + const MysqlReply::Column* column, + butil::StringPiece& str, + butil::Arena* arena) { + + const uint64_t len = parse_encode_length(buf); + if (len == 0) { + _is_nil = true; + return PARSE_OK; + } + + if (len != 8 && len != 12) { + LOG(ERROR) << "invalid TIME packet length " << len; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + + uint8_t dstlen; + switch (column->_decimal) { + case 0x00: + case 0x1f: + dstlen = 8; + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + dstlen = 8 + 1 + column->_decimal; + break; + default: + LOG(ERROR) << "protocol error, illegal decimals value " << column->_decimal; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + + size_t i = 0; + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, dstlen + 2, d)); + d[dstlen] = '\0'; + d[dstlen + 1] = '\0'; + uint32_t day; + uint8_t neg, hour, min, sec; + + buf.cut1((char*)&neg); + if (neg == 1) { + d[i++] = '-'; + } + + buf.cutn(&day, 4); + day = mysql_uint4korr((uint8_t*)&day); + buf.cut1((char*)&hour); + hour += day * 24; + if (hour >= 100) { + std::ostringstream os; + os << hour; + std::string s = os.str(); + for (const auto& v : s) { + d[i++] = v; + } + } else { + d[i++] = digits10[hour]; + d[i++] = digits01[hour]; + } + + buf.cut1((char*)&min); + buf.cut1((char*)&sec); + + d[i++] = ':'; + d[i++] = digits10[min]; + d[i++] = digits01[min]; + d[i++] = ':'; + d[i++] = digits10[sec]; + d[i++] = digits01[sec]; + + ParseError rc = ParseMicrosecs(buf, column->_decimal, d + i); + if (rc == PARSE_OK) { + str.set(d, dstlen + 2); + } + return rc; +} + +ParseError MysqlReply::Field::ParseBinaryDataTime(butil::IOBuf& buf, + const MysqlReply::Column* column, + butil::StringPiece& str, + butil::Arena* arena) { + const uint64_t len = parse_encode_length(buf); + if (len == 0) { + _is_nil = true; + return PARSE_OK; + } + + if (len != 4 && len != 7 && len != 11) { + LOG(ERROR) << "illegal date time length " << len; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + + uint8_t dstlen; + if (column->_type == MYSQL_FIELD_TYPE_DATE) { + dstlen = 10; + } else { + switch (column->_decimal) { + case 0x00: + case 0x1f: + dstlen = 19; + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + dstlen = 19 + 1 + column->_decimal; + break; + default: + LOG(ERROR) << "protocol error, illegal decimal value " << column->_decimal; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + } + + size_t i = 0; + char* d = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, dstlen, d)); + uint16_t year; + uint8_t pt, p1, p2, p3; + buf.cutn(&year, 2); // year + year = mysql_uint2korr((uint8_t*)&year); + pt = year / 100; + p1 = year - (100 * pt); + buf.cut1((char*)&p2); + buf.cut1((char*)&p3); + + d[i++] = digits10[pt]; + d[i++] = digits01[pt]; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = '-'; + d[i++] = digits10[p2]; + d[i++] = digits01[p2]; + d[i++] = '-'; + d[i++] = digits10[p3]; + d[i++] = digits01[p3]; + + if (len == 4) { + str.set(d, dstlen); + return PARSE_OK; + } + + d[i++] = ' '; + buf.cut1((char*)&p1); // hour + buf.cut1((char*)&p2); // min + buf.cut1((char*)&p3); // sec + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = ':'; + d[i++] = digits10[p2]; + d[i++] = digits01[p2]; + d[i++] = ':'; + d[i++] = digits10[p3]; + d[i++] = digits01[p3]; + + ParseError rc = ParseMicrosecs(buf, column->_decimal, d + i); + if (rc == PARSE_OK) { + str.set(d, dstlen); + } + return rc; +} + +ParseError MysqlReply::Field::ParseMicrosecs(butil::IOBuf& buf, uint8_t decimal, char* d) { + if (decimal <= 0) { + return PARSE_OK; + } + + size_t i = 0; + uint32_t microsecs; + uint8_t p1, p2, p3; + buf.cutn((char*)µsecs, 4); + microsecs = mysql_uint4korr((uint8_t*)µsecs); + p1 = microsecs / 10000; + microsecs -= 10000 * p1; + p2 = microsecs / 100; + microsecs -= 100 * p2; + p3 = microsecs; + + switch (decimal) { + case 1: + d[i++] = '.'; + d[i++] = digits10[p1]; + break; + case 2: + d[i++] = '.'; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + break; + case 3: + d[i++] = '.'; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = digits10[p2]; + break; + case 4: + d[i++] = '.'; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = digits10[p2]; + d[i++] = digits01[p2]; + break; + case 5: + d[i++] = '.'; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = digits10[p2]; + d[i++] = digits01[p2]; + d[i++] = digits10[p3]; + break; + default: + d[i++] = '.'; + d[i++] = digits10[p1]; + d[i++] = digits01[p1]; + d[i++] = digits10[p2]; + d[i++] = digits01[p2]; + d[i++] = digits10[p3]; + d[i++] = digits01[p3]; + } + return PARSE_OK; +} + +ParseError MysqlReply::ResultSet::Parse(butil::IOBuf& buf, butil::Arena* arena, bool binary) { + if (is_parsed()) { + return PARSE_OK; + } + // parse header + MY_PARSE_CHECK(_header.Parse(buf)); + // parse colunms + MY_ALLOC_CHECK(my_alloc_check(arena, _header._column_count, _columns)); + for (uint64_t i = 0; i < _header._column_count; ++i) { + MY_PARSE_CHECK(_columns[i].Parse(buf, arena)); + } + // parse eof1 + MY_PARSE_CHECK(_eof1.Parse(buf)); + // parse row + std::vector rows; + for (;;) { + // if not full package reread + if (!is_full_package(buf)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + // if eof break loops for row + if (is_an_eof(buf)) { + break; + } + // allocate memory for row and fields + Row* row = NULL; + Field* fields = NULL; + MY_ALLOC_CHECK(my_alloc_check(arena, 1, row)); + MY_ALLOC_CHECK(my_alloc_check(arena, _header._column_count, fields)); + row->_fields = fields; + row->_field_count = _header._column_count; + _last->_next = row; + _last = row; + // parse row and fields + MY_PARSE_CHECK(row->Parse(buf, _columns, _header._column_count, fields, binary, arena)); + // add row count + ++_row_count; + } + // parse eof2 + MY_PARSE_CHECK(_eof2.Parse(buf)); + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::PrepareOk::Parse(butil::IOBuf& buf, butil::Arena* arena) { + if (is_parsed()) { + return PARSE_OK; + } + + MY_PARSE_CHECK(_header.Parse(buf)); + + if (_header._param_count > 0) { + MY_ALLOC_CHECK(my_alloc_check(arena, _header._param_count, _params)); + for (uint16_t i = 0; i < _header._param_count; ++i) { + MY_PARSE_CHECK(_params[i].Parse(buf, arena)); + } + MY_PARSE_CHECK(_eof1.Parse(buf)); + } + + if (_header._column_count > 0) { + MY_ALLOC_CHECK(my_alloc_check(arena, _header._column_count, _columns)); + for (uint16_t i = 0; i < _header._column_count; ++i) { + MY_PARSE_CHECK(_columns[i].Parse(buf, arena)); + } + MY_PARSE_CHECK(_eof2.Parse(buf)); + } + set_parsed(); + return PARSE_OK; +} + +ParseError MysqlReply::PrepareOk::Header::Parse(butil::IOBuf& buf) { + if (is_parsed()) { + return PARSE_OK; + } + + MysqlHeader header; + if (!parse_header(buf, &header)) { + return PARSE_ERROR_NOT_ENOUGH_DATA; + } + + buf.pop_front(1); + { + uint8_t tmp[4]; + buf.cutn(tmp, sizeof(tmp)); + _stmt_id = mysql_uint4korr(tmp); + } + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _column_count = mysql_uint2korr(tmp); + } + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _param_count = mysql_uint2korr(tmp); + } + buf.pop_front(1); + { + uint8_t tmp[2]; + buf.cutn(tmp, sizeof(tmp)); + _warning = mysql_uint2korr(tmp); + } + + set_parsed(); + return PARSE_OK; +} + +} // namespace brpc diff --git a/src/brpc/mysql_reply.h b/src/brpc/mysql_reply.h new file mode 100644 index 0000000000..4a52537743 --- /dev/null +++ b/src/brpc/mysql_reply.h @@ -0,0 +1,801 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_REPLY_H +#define BRPC_MYSQL_REPLY_H + +#include "butil/iobuf.h" // butil::IOBuf +#include "butil/arena.h" +#include "butil/sys_byteorder.h" +#include "butil/logging.h" // LOG() +#include "brpc/parse_result.h" +#include "brpc/mysql_common.h" + +namespace brpc { + +class CheckParsed { +public: + CheckParsed() : _is_parsed(false) {} + bool is_parsed() const { + return _is_parsed; + } + void set_parsed() { + _is_parsed = true; + } + +private: + bool _is_parsed; +}; + +enum MysqlRspType : uint8_t { + MYSQL_RSP_OK = 0x00, + MYSQL_RSP_ERROR = 0xFF, + MYSQL_RSP_RESULTSET = 0x01, + MYSQL_RSP_EOF = 0xFE, + MYSQL_RSP_AUTH = 0xFB, // add for mysql auth + MYSQL_RSP_PREPARE_OK = 0xFC, // add for prepared statement + MYSQL_RSP_UNKNOWN = 0xFD, // add for other case +}; + +const char* MysqlRspTypeToString(MysqlRspType); + +class MysqlReply { +public: + // Mysql Auth package + class Auth : private CheckParsed { + public: + Auth(); + uint8_t protocol() const; + butil::StringPiece version() const; + uint32_t thread_id() const; + butil::StringPiece salt() const; + uint16_t capability() const; + uint8_t collation() const; + uint16_t status() const; + uint16_t extended_capability() const; + uint8_t auth_plugin_length() const; + butil::StringPiece salt2() const; + butil::StringPiece auth_plugin() const; + + private: + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(Auth); + friend class MysqlReply; + + uint8_t _protocol; + butil::StringPiece _version; + uint32_t _thread_id; + butil::StringPiece _salt; + uint16_t _capability; + uint8_t _collation; + uint16_t _status; + uint16_t _extended_capability; + uint8_t _auth_plugin_length; + butil::StringPiece _salt2; + butil::StringPiece _auth_plugin; + }; + // Mysql Prepared Statement Ok + class Column; + // Mysql Eof package + class Eof : private CheckParsed { + public: + Eof(); + uint16_t warning() const; + uint16_t status() const; + + private: + ParseError Parse(butil::IOBuf& buf); + + DISALLOW_COPY_AND_ASSIGN(Eof); + friend class MysqlReply; + + uint16_t _warning; + uint16_t _status; + }; + // Mysql PrepareOk package + class PrepareOk : private CheckParsed { + public: + PrepareOk(); + uint32_t stmt_id() const; + uint16_t column_count() const; + uint16_t param_count() const; + uint16_t warning() const; + const Column& param(uint16_t index) const; + const Column& column(uint16_t index) const; + + private: + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(PrepareOk); + friend class MysqlReply; + + class Header : private CheckParsed { + public: + Header() : _stmt_id(0), _column_count(0), _param_count(0), _warning(0) {} + uint32_t _stmt_id; + uint16_t _column_count; + uint16_t _param_count; + uint16_t _warning; + ParseError Parse(butil::IOBuf& buf); + }; + Header _header; + Column* _params; + Eof _eof1; + Column* _columns; + Eof _eof2; + }; + // Mysql Ok package + class Ok : private CheckParsed { + public: + Ok(); + uint64_t affect_row() const; + uint64_t index() const; + uint16_t status() const; + uint16_t warning() const; + butil::StringPiece msg() const; + + private: + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(Ok); + friend class MysqlReply; + + uint64_t _affect_row; + uint64_t _index; + uint16_t _status; + uint16_t _warning; + butil::StringPiece _msg; + }; + // Mysql Error package + class Error : private CheckParsed { + public: + Error(); + uint16_t errcode() const; + butil::StringPiece status() const; + butil::StringPiece msg() const; + + private: + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(Error); + friend class MysqlReply; + + uint16_t _errcode; + butil::StringPiece _status; + butil::StringPiece _msg; + }; + // Mysql Column + class Column : private CheckParsed { + public: + Column(); + butil::StringPiece catalog() const; + butil::StringPiece database() const; + butil::StringPiece table() const; + butil::StringPiece origin_table() const; + butil::StringPiece name() const; + butil::StringPiece origin_name() const; + uint16_t charset() const; + uint32_t length() const; + MysqlFieldType type() const; + MysqlFieldFlag flag() const; + uint8_t decimal() const; + + private: + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(Column); + friend class MysqlReply; + + butil::StringPiece _catalog; + butil::StringPiece _database; + butil::StringPiece _table; + butil::StringPiece _origin_table; + butil::StringPiece _name; + butil::StringPiece _origin_name; + uint16_t _charset; + uint32_t _length; + MysqlFieldType _type; + MysqlFieldFlag _flag; + uint8_t _decimal; + }; + // Mysql Field + class Field : private CheckParsed { + public: + Field(); + int8_t stiny() const; + uint8_t tiny() const; + int16_t ssmall() const; + uint16_t small() const; + int32_t sinteger() const; + uint32_t integer() const; + int64_t sbigint() const; + uint64_t bigint() const; + float float32() const; + double float64() const; + butil::StringPiece string() const; + bool is_stiny() const; + bool is_tiny() const; + bool is_ssmall() const; + bool is_small() const; + bool is_sinteger() const; + bool is_integer() const; + bool is_sbigint() const; + bool is_bigint() const; + bool is_float32() const; + bool is_float64() const; + bool is_string() const; + bool is_nil() const; + + private: + ParseError Parse(butil::IOBuf& buf, const MysqlReply::Column* column, butil::Arena* arena); + ParseError Parse(butil::IOBuf& buf, + const MysqlReply::Column* column, + uint64_t column_index, + uint64_t column_number, + const uint8_t* null_mask, + butil::Arena* arena); + ParseError ParseBinaryTime(butil::IOBuf& buf, + const MysqlReply::Column* column, + butil::StringPiece& str, + butil::Arena* arena); + ParseError ParseBinaryDataTime(butil::IOBuf& buf, + const MysqlReply::Column* column, + butil::StringPiece& str, + butil::Arena* arena); + ParseError ParseMicrosecs(butil::IOBuf& buf, uint8_t decimal, char* d); + DISALLOW_COPY_AND_ASSIGN(Field); + friend class MysqlReply; + + union { + int8_t stiny; + uint8_t tiny; + int16_t ssmall; + uint16_t small; + int32_t sinteger; + uint32_t integer; + int64_t sbigint; + uint64_t bigint; + float float32; + double float64; + butil::StringPiece str; + } _data = {.str = NULL}; + MysqlFieldType _type; + bool _unsigned; + bool _is_nil; + }; + // Mysql Row + class Row : private CheckParsed { + public: + Row(); + uint64_t field_count() const; + const Field& field(const uint64_t index) const; + + private: + ParseError Parse(butil::IOBuf& buf, + const Column* columns, + uint64_t column_number, + Field* fields, + bool binary, + butil::Arena* arena); + + DISALLOW_COPY_AND_ASSIGN(Row); + friend class MysqlReply; + + Field* _fields; + uint64_t _field_count; + Row* _next; + }; + +public: + MysqlReply(); + ParseError ConsumePartialIOBuf(butil::IOBuf& buf, + butil::Arena* arena, + bool is_auth, + MysqlStmtType stmt_type, + bool* more_results); + void Swap(MysqlReply& other); + void Print(std::ostream& os) const; + // response type + MysqlRspType type() const; + // get auth + const Auth& auth() const; + const Ok& ok() const; + const PrepareOk& prepare_ok() const; + const Error& error() const; + const Eof& eof() const; + // get column number + uint64_t column_count() const; + // get one column + const Column& column(const uint64_t index) const; + // get row number + uint64_t row_count() const; + // get one row + const Row& next() const; + bool is_auth() const; + bool is_ok() const; + bool is_prepare_ok() const; + bool is_error() const; + bool is_eof() const; + bool is_resultset() const; + +private: + // Mysql result set header + struct ResultSetHeader : private CheckParsed { + ResultSetHeader() : _column_count(0), _extra_msg(0) {} + ParseError Parse(butil::IOBuf& buf); + uint64_t _column_count; + uint64_t _extra_msg; + + private: + DISALLOW_COPY_AND_ASSIGN(ResultSetHeader); + }; + // Mysql result set + struct ResultSet : private CheckParsed { + ResultSet() : _columns(NULL), _row_count(0) { + _cur = _first = _last = &_dummy; + } + ParseError Parse(butil::IOBuf& buf, butil::Arena* arena, bool binary); + ResultSetHeader _header; + Column* _columns; + Eof _eof1; + // row list begin + Row* _first; + Row* _last; + Row* _cur; + uint64_t _row_count; + // row list end + Eof _eof2; + + private: + DISALLOW_COPY_AND_ASSIGN(ResultSet); + Row _dummy; + }; + // member values + MysqlRspType _type; + union { + Auth* auth; + ResultSet* result_set; + Ok* ok; + PrepareOk* prepare_ok; + Error* error; + Eof* eof; + uint64_t padding; // For swapping, must cover all bytes. + } _data; + + DISALLOW_COPY_AND_ASSIGN(MysqlReply); +}; + +// mysql reply +inline MysqlReply::MysqlReply() { + _type = MYSQL_RSP_UNKNOWN; + _data.padding = 0; +} +inline void MysqlReply::Swap(MysqlReply& other) { + std::swap(_type, other._type); + std::swap(_data.padding, other._data.padding); +} +inline std::ostream& operator<<(std::ostream& os, const MysqlReply& r) { + r.Print(os); + return os; +} +inline MysqlRspType MysqlReply::type() const { + return _type; +} +inline const MysqlReply::Auth& MysqlReply::auth() const { + if (is_auth()) { + return *_data.auth; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an auth"; + static Auth auth_nil; + return auth_nil; +} +inline const MysqlReply::PrepareOk& MysqlReply::prepare_ok() const { + if (is_prepare_ok()) { + return *_data.prepare_ok; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an ok"; + static PrepareOk prepare_ok_nil; + return prepare_ok_nil; +} +inline const MysqlReply::Ok& MysqlReply::ok() const { + if (is_ok()) { + return *_data.ok; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an ok"; + static Ok ok_nil; + return ok_nil; +} +inline const MysqlReply::Error& MysqlReply::error() const { + if (is_error()) { + return *_data.error; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an error"; + static Error error_nil; + return error_nil; +} +inline const MysqlReply::Eof& MysqlReply::eof() const { + if (is_eof()) { + return *_data.eof; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an eof"; + static Eof eof_nil; + return eof_nil; +} +inline uint64_t MysqlReply::column_count() const { + if (is_resultset()) { + return _data.result_set->_header._column_count; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an resultset"; + return 0; +} +inline const MysqlReply::Column& MysqlReply::column(const uint64_t index) const { + static Column column_nil; + if (is_resultset()) { + if (index < _data.result_set->_header._column_count) { + return _data.result_set->_columns[index]; + } + CHECK(false) << "index " << index << " out of bound [0," + << _data.result_set->_header._column_count << ")"; + return column_nil; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an resultset"; + return column_nil; +} +inline uint64_t MysqlReply::row_count() const { + if (is_resultset()) { + return _data.result_set->_row_count; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an resultset"; + return 0; +} +inline const MysqlReply::Row& MysqlReply::next() const { + static Row row_nil; + if (is_resultset()) { + if (_data.result_set->_row_count == 0) { + CHECK(false) << "there are 0 rows returned"; + return row_nil; + } + if (_data.result_set->_cur == _data.result_set->_last->_next) { + _data.result_set->_cur = _data.result_set->_first->_next; + } else { + _data.result_set->_cur = _data.result_set->_cur->_next; + } + return *_data.result_set->_cur; + } + CHECK(false) << "The reply is " << MysqlRspTypeToString(_type) << ", not an resultset"; + return row_nil; +} +inline bool MysqlReply::is_auth() const { + return _type == MYSQL_RSP_AUTH; +} +inline bool MysqlReply::is_prepare_ok() const { + return _type == MYSQL_RSP_PREPARE_OK; +} +inline bool MysqlReply::is_ok() const { + return _type == MYSQL_RSP_OK; +} +inline bool MysqlReply::is_error() const { + return _type == MYSQL_RSP_ERROR; +} +inline bool MysqlReply::is_eof() const { + return _type == MYSQL_RSP_EOF; +} +inline bool MysqlReply::is_resultset() const { + return _type == MYSQL_RSP_RESULTSET; +} +// mysql auth +inline MysqlReply::Auth::Auth() + : _protocol(0), + _thread_id(0), + _capability(0), + _collation(0), + _status(0), + _extended_capability(0), + _auth_plugin_length(0) {} +inline uint8_t MysqlReply::Auth::protocol() const { + return _protocol; +} +inline butil::StringPiece MysqlReply::Auth::version() const { + return _version; +} +inline uint32_t MysqlReply::Auth::thread_id() const { + return _thread_id; +} +inline butil::StringPiece MysqlReply::Auth::salt() const { + return _salt; +} +inline uint16_t MysqlReply::Auth::capability() const { + return _capability; +} +inline uint8_t MysqlReply::Auth::collation() const { + return _collation; +} +inline uint16_t MysqlReply::Auth::status() const { + return _status; +} +inline uint16_t MysqlReply::Auth::extended_capability() const { + return _extended_capability; +} +inline uint8_t MysqlReply::Auth::auth_plugin_length() const { + return _auth_plugin_length; +} +inline butil::StringPiece MysqlReply::Auth::salt2() const { + return _salt2; +} +inline butil::StringPiece MysqlReply::Auth::auth_plugin() const { + return _auth_plugin; +} +// mysql prepared statement ok +inline MysqlReply::PrepareOk::PrepareOk() : _params(NULL), _columns(NULL) {} +inline uint32_t MysqlReply::PrepareOk::stmt_id() const { + CHECK(_header._stmt_id > 0) << "stmt id is wrong"; + return _header._stmt_id; +} +inline uint16_t MysqlReply::PrepareOk::column_count() const { + return _header._column_count; +} +inline uint16_t MysqlReply::PrepareOk::param_count() const { + return _header._param_count; +} +inline uint16_t MysqlReply::PrepareOk::warning() const { + return _header._warning; +} +inline const MysqlReply::Column& MysqlReply::PrepareOk::param(uint16_t index) const { + if (index < _header._param_count) { + return _params[index]; + } + static Column column_nil; + CHECK(false) << "index " << index << " out of bound [0," << _header._param_count << ")"; + return column_nil; +} +inline const MysqlReply::Column& MysqlReply::PrepareOk::column(uint16_t index) const { + if (index < _header._column_count) { + return _columns[index]; + } + CHECK(false) << "index " << index << " out of bound [0," << _header._column_count << ")"; + static Column column_nil; + return column_nil; +} +// mysql reply ok +inline MysqlReply::Ok::Ok() : _affect_row(0), _index(0), _status(0), _warning(0) {} +inline uint64_t MysqlReply::Ok::affect_row() const { + return _affect_row; +} +inline uint64_t MysqlReply::Ok::index() const { + return _index; +} +inline uint16_t MysqlReply::Ok::status() const { + return _status; +} +inline uint16_t MysqlReply::Ok::warning() const { + return _warning; +} +inline butil::StringPiece MysqlReply::Ok::msg() const { + return _msg; +} +// mysql reply error +inline MysqlReply::Error::Error() : _errcode(0) {} +inline uint16_t MysqlReply::Error::errcode() const { + return _errcode; +} +inline butil::StringPiece MysqlReply::Error::status() const { + return _status; +} +inline butil::StringPiece MysqlReply::Error::msg() const { + return _msg; +} +// mysql reply eof +inline MysqlReply::Eof::Eof() : _warning(0), _status(0) {} +inline uint16_t MysqlReply::Eof::warning() const { + return _warning; +} +inline uint16_t MysqlReply::Eof::status() const { + return _status; +} +// mysql reply column +inline MysqlReply::Column::Column() : _length(0), _type(MYSQL_FIELD_TYPE_NULL), _decimal(0) {} +inline butil::StringPiece MysqlReply::Column::catalog() const { + return _catalog; +} +inline butil::StringPiece MysqlReply::Column::database() const { + return _database; +} +inline butil::StringPiece MysqlReply::Column::table() const { + return _table; +} +inline butil::StringPiece MysqlReply::Column::origin_table() const { + return _origin_table; +} +inline butil::StringPiece MysqlReply::Column::name() const { + return _name; +} +inline butil::StringPiece MysqlReply::Column::origin_name() const { + return _origin_name; +} +inline uint16_t MysqlReply::Column::charset() const { + return _charset; +} +inline uint32_t MysqlReply::Column::length() const { + return _length; +} +inline MysqlFieldType MysqlReply::Column::type() const { + return _type; +} +inline MysqlFieldFlag MysqlReply::Column::flag() const { + return _flag; +} +inline uint8_t MysqlReply::Column::decimal() const { + return _decimal; +} +// mysql reply row +inline MysqlReply::Row::Row() : _fields(NULL), _field_count(0), _next(NULL) {} +inline uint64_t MysqlReply::Row::field_count() const { + return _field_count; +} +inline const MysqlReply::Field& MysqlReply::Row::field(const uint64_t index) const { + if (index < _field_count) { + return _fields[index]; + } + CHECK(false) << "index " << index << " out of bound [0," << _field_count << ")"; + static Field field_nil; + return field_nil; +} +// mysql reply field +inline MysqlReply::Field::Field() + : _type(MYSQL_FIELD_TYPE_NULL), _unsigned(false), _is_nil(false) {} +inline int8_t MysqlReply::Field::stiny() const { + if (is_stiny()) { + return _data.stiny; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an stiny"; + return 0; +} +inline uint8_t MysqlReply::Field::tiny() const { + if (is_tiny()) { + return _data.tiny; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an tiny"; + return 0; +} +inline int16_t MysqlReply::Field::ssmall() const { + if (is_ssmall()) { + return _data.ssmall; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an ssmall"; + return 0; +} +inline uint16_t MysqlReply::Field::small() const { + if (is_small()) { + return _data.small; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an small"; + return 0; +} +inline int32_t MysqlReply::Field::sinteger() const { + if (is_sinteger()) { + return _data.sinteger; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an sinteger"; + return 0; +} +inline uint32_t MysqlReply::Field::integer() const { + if (is_integer()) { + return _data.integer; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an integer"; + return 0; +} +inline int64_t MysqlReply::Field::sbigint() const { + if (is_sbigint()) { + return _data.sbigint; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an sbigint"; + return 0; +} +inline uint64_t MysqlReply::Field::bigint() const { + if (is_bigint()) { + return _data.bigint; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an bigint"; + return 0; +} +inline float MysqlReply::Field::float32() const { + if (is_float32()) { + return _data.float32; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an float32"; + return 0; +} +inline double MysqlReply::Field::float64() const { + if (is_float64()) { + return _data.float64; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an float64"; + return 0; +} +inline butil::StringPiece MysqlReply::Field::string() const { + if (is_string()) { + return _data.str; + } + CHECK(false) << "The reply is " << MysqlFieldTypeToString(_type) << " and " + << (_is_nil ? "NULL" : "NOT NULL") << ", not an string"; + return butil::StringPiece(); +} +inline bool MysqlReply::Field::is_stiny() const { + return _type == MYSQL_FIELD_TYPE_TINY && !_unsigned && !_is_nil; +} +inline bool MysqlReply::Field::is_tiny() const { + return _type == MYSQL_FIELD_TYPE_TINY && _unsigned && !_is_nil; +} +inline bool MysqlReply::Field::is_ssmall() const { + return (_type == MYSQL_FIELD_TYPE_SHORT || _type == MYSQL_FIELD_TYPE_YEAR) && !_unsigned && + !_is_nil; +} +inline bool MysqlReply::Field::is_small() const { + return (_type == MYSQL_FIELD_TYPE_SHORT || _type == MYSQL_FIELD_TYPE_YEAR) && _unsigned && + !_is_nil; +} +inline bool MysqlReply::Field::is_sinteger() const { + return (_type == MYSQL_FIELD_TYPE_INT24 || _type == MYSQL_FIELD_TYPE_LONG) && !_unsigned && + !_is_nil; +} +inline bool MysqlReply::Field::is_integer() const { + return (_type == MYSQL_FIELD_TYPE_INT24 || _type == MYSQL_FIELD_TYPE_LONG) && _unsigned && + !_is_nil; +} +inline bool MysqlReply::Field::is_sbigint() const { + return _type == MYSQL_FIELD_TYPE_LONGLONG && !_unsigned && !_is_nil; +} +inline bool MysqlReply::Field::is_bigint() const { + return _type == MYSQL_FIELD_TYPE_LONGLONG && _unsigned && !_is_nil; +} +inline bool MysqlReply::Field::is_float32() const { + return _type == MYSQL_FIELD_TYPE_FLOAT && !_is_nil; +} +inline bool MysqlReply::Field::is_float64() const { + return _type == MYSQL_FIELD_TYPE_DOUBLE && !_is_nil; +} +inline bool MysqlReply::Field::is_string() const { + return (_type == MYSQL_FIELD_TYPE_DECIMAL || _type == MYSQL_FIELD_TYPE_NEWDECIMAL || + _type == MYSQL_FIELD_TYPE_VARCHAR || _type == MYSQL_FIELD_TYPE_BIT || + _type == MYSQL_FIELD_TYPE_ENUM || _type == MYSQL_FIELD_TYPE_SET || + _type == MYSQL_FIELD_TYPE_TINY_BLOB || _type == MYSQL_FIELD_TYPE_MEDIUM_BLOB || + _type == MYSQL_FIELD_TYPE_LONG_BLOB || _type == MYSQL_FIELD_TYPE_BLOB || + _type == MYSQL_FIELD_TYPE_VAR_STRING || _type == MYSQL_FIELD_TYPE_STRING || + _type == MYSQL_FIELD_TYPE_GEOMETRY || _type == MYSQL_FIELD_TYPE_JSON || + _type == MYSQL_FIELD_TYPE_TIME || _type == MYSQL_FIELD_TYPE_DATE || + _type == MYSQL_FIELD_TYPE_NEWDATE || _type == MYSQL_FIELD_TYPE_TIMESTAMP || + _type == MYSQL_FIELD_TYPE_DATETIME) && + !_is_nil; +} +inline bool MysqlReply::Field::is_nil() const { + return _is_nil; +} + +} // namespace brpc + +#endif // BRPC_MYSQL_REPLY_H diff --git a/src/brpc/mysql_statement.cpp b/src/brpc/mysql_statement.cpp new file mode 100644 index 0000000000..449792e753 --- /dev/null +++ b/src/brpc/mysql_statement.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include +#include +#include "brpc/socket.h" +#include "brpc/mysql_statement.h" + +namespace brpc { +DEFINE_int32(mysql_statment_map_size, + 100, + "Mysql statement map size, usually equal to max bthread number"); + +MysqlStatementUniquePtr NewMysqlStatement(const Channel& channel, const butil::StringPiece& str) { + MysqlStatementUniquePtr ptr(new MysqlStatement(channel, str)); + return ptr; +} + +uint32_t MysqlStatement::StatementId(SocketId socket_id) const { + if (_connection_type == CONNECTION_TYPE_SHORT) { + return 0; + } + MysqlStatementDBD::ScopedPtr ptr; + if (_id_map.Read(&ptr) != 0) { + return 0; + } + const MysqlStatementId* p = ptr->seek(socket_id); + if (p == NULL) { + return 0; + } + SocketUniquePtr socket; + if (Socket::Address(socket_id, &socket) == 0) { + uint64_t fd_version = socket->fd_version(); + if (fd_version == p->version) { + return p->stmt_id; + } + } + return 0; +} + +void MysqlStatement::SetStatementId(SocketId socket_id, uint32_t stmt_id) { + if (_connection_type == CONNECTION_TYPE_SHORT) { + return; + } + SocketUniquePtr socket; + if (Socket::Address(socket_id, &socket) == 0) { + uint64_t fd_version = socket->fd_version(); + MysqlStatementId value{stmt_id, fd_version}; + _id_map.Modify(my_update_kv, socket_id, value); + } +} + +void MysqlStatement::Init(const Channel& channel) { + _param_count = std::count(_str.begin(), _str.end(), '?'); + ChannelOptions opts = channel.options(); + _connection_type = ConnectionType(opts.connection_type); + if (_connection_type != CONNECTION_TYPE_SHORT) { + _id_map.Modify(my_init_kv); + } +} + +} // namespace brpc diff --git a/src/brpc/mysql_statement.h b/src/brpc/mysql_statement.h new file mode 100644 index 0000000000..49dc7ca318 --- /dev/null +++ b/src/brpc/mysql_statement.h @@ -0,0 +1,66 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_STATEMENT_H +#define BRPC_MYSQL_STATEMENT_H +#include +#include +#include "brpc/channel.h" +#include "brpc/mysql_statement_inl.h" + +namespace brpc { +// mysql prepared statement Unique Ptr +class MysqlStatement; +typedef std::unique_ptr MysqlStatementUniquePtr; +// mysql prepared statement +class MysqlStatement { +public: + const butil::StringPiece str() const; + uint16_t param_count() const; + uint32_t StatementId(SocketId sock_id) const; + void SetStatementId(SocketId sock_id, uint32_t stmt_id); + +private: + MysqlStatement(const Channel& channel, const butil::StringPiece& str); + void Init(const Channel& channel); + DISALLOW_COPY_AND_ASSIGN(MysqlStatement); + + friend MysqlStatementUniquePtr NewMysqlStatement(const Channel& channel, + const butil::StringPiece& str); + + const std::string _str; // prepare statement string + uint16_t _param_count; + mutable MysqlStatementDBD _id_map; // SocketId and statement id + ConnectionType _connection_type; +}; + +inline MysqlStatement::MysqlStatement(const Channel& channel, const butil::StringPiece& str) + : _str(str.data(), str.size()), _param_count(0) { + Init(channel); +} + +inline const butil::StringPiece MysqlStatement::str() const { + return butil::StringPiece(_str); +} + +inline uint16_t MysqlStatement::param_count() const { + return _param_count; +} + +MysqlStatementUniquePtr NewMysqlStatement(const Channel& channel, const butil::StringPiece& str); + +} // namespace brpc +#endif diff --git a/src/brpc/mysql_statement_inl.h b/src/brpc/mysql_statement_inl.h new file mode 100644 index 0000000000..97a814c1ee --- /dev/null +++ b/src/brpc/mysql_statement_inl.h @@ -0,0 +1,58 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_STATEMENT_INL_H +#define BRPC_MYSQL_STATEMENT_INL_H +#include +#include "butil/containers/flat_map.h" // FlatMap +#include "butil/containers/doubly_buffered_data.h" +#include "brpc/socket_id.h" + +namespace brpc { +DECLARE_int32(mysql_statment_map_size); + +struct MysqlStatementId { + uint32_t stmt_id; // statement id + uint64_t version; // socket's fd version +}; + +typedef butil::FlatMap MysqlStatementKVMap; +typedef butil::DoublyBufferedData MysqlStatementDBD; + +inline size_t my_init_kv(MysqlStatementKVMap& m) { + if (FLAGS_mysql_statment_map_size < 100) { + FLAGS_mysql_statment_map_size = 100; + } + m.init(FLAGS_mysql_statment_map_size); + return 1; +} + +inline size_t my_update_kv(MysqlStatementKVMap& m, SocketId key, MysqlStatementId value) { + MysqlStatementId* p = m.seek(key); + if (p == NULL) { + m.insert(key, value); + } else { + *p = value; + } + return 1; +} + +inline size_t my_delete_k(MysqlStatementKVMap& m, SocketId key) { + return m.erase(key); +} + +} // namespace brpc +#endif diff --git a/src/brpc/mysql_transaction.cpp b/src/brpc/mysql_transaction.cpp new file mode 100644 index 0000000000..3704f0ad5d --- /dev/null +++ b/src/brpc/mysql_transaction.cpp @@ -0,0 +1,113 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include +#include "butil/logging.h" // LOG() +#include "brpc/mysql_transaction.h" +#include "brpc/mysql.h" +#include "brpc/socket.h" +#include "brpc/details/controller_private_accessor.h" + +namespace brpc { +// mysql transaction isolation level string +const char* mysql_isolation_level[] = { + "REPEATABLE READ", "READ COMMITTED", "READ UNCOMMITTED", "SERIALIZABLE"}; + +SocketId MysqlTransaction::GetSocketId() const { + return _socket->id(); +} + +bool MysqlTransaction::DoneTransaction(const char* command) { + bool rc = false; + MysqlRequest request(this); + if (_socket == NULL) { // must already commit or rollback, return true. + return true; + } else if (!request.Query(command)) { + LOG(ERROR) << "Fail to query command" << command; + } else { + MysqlResponse response; + Controller cntl; + _channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (!cntl.Failed()) { + if (response.reply(0).is_ok()) { + rc = true; + } else { + LOG(ERROR) << "Fail " << command << " transaction, " << response; + } + } else { + LOG(ERROR) << "Fail " << command << " transaction, " << cntl.ErrorText(); + } + } + if (rc && _connection_type == CONNECTION_TYPE_POOLED) { + _socket->ReturnToPool(); + } + _socket.reset(); + return rc; +} + +MysqlTransactionUniquePtr NewMysqlTransaction(Channel& channel, + const MysqlTransactionOptions& opts) { + const char* command[2] = {"START TRANSACTION READ ONLY", "START TRANSACTION"}; + + if (channel.options().connection_type == CONNECTION_TYPE_SINGLE) { + LOG(ERROR) << "mysql transaction can't use connection type 'single'"; + return NULL; + } + std::stringstream ss; + // repeatable read is mysql default isolation level, so ignore it. + if (opts.isolation_level != MysqlIsoRepeatableRead) { + ss << "SET TRANSACTION ISOLATION LEVEL " << mysql_isolation_level[opts.isolation_level] + << ";"; + } + + if (opts.readonly) { + ss << command[0]; + } else { + ss << command[1]; + } + + MysqlRequest request; + if (!request.Query(ss.str())) { + LOG(ERROR) << "Fail to query command" << ss.str(); + return NULL; + } + + MysqlTransactionUniquePtr tx; + MysqlResponse response; + Controller cntl; + ControllerPrivateAccessor(&cntl).set_bind_sock_action(BIND_SOCK_ACTIVE); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + if (!cntl.Failed()) { + // repeatable read isolation send one reply, other isolation has two reply + if ((opts.isolation_level == MysqlIsoRepeatableRead && response.reply(0).is_ok()) || + (response.reply(0).is_ok() && response.reply(1).is_ok())) { + SocketUniquePtr socket; + ControllerPrivateAccessor(&cntl).get_bind_sock(&socket); + if (socket == NULL) { + LOG(ERROR) << "Fail create mysql transaction, get bind socket failed"; + } else { + tx.reset(new MysqlTransaction(channel, socket, cntl.connection_type())); + } + } else { + LOG(ERROR) << "Fail create mysql transaction, " << response; + } + } else { + LOG(ERROR) << "Fail create mysql transaction, " << cntl.ErrorText(); + } + return tx; +} + +} // namespace brpc diff --git a/src/brpc/mysql_transaction.h b/src/brpc/mysql_transaction.h new file mode 100644 index 0000000000..8bfae54909 --- /dev/null +++ b/src/brpc/mysql_transaction.h @@ -0,0 +1,89 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_MYSQL_TRANSACTION_H +#define BRPC_MYSQL_TRANSACTION_H + +#include "brpc/socket_id.h" +#include "brpc/channel.h" + +namespace brpc { +// mysql isolation level enum +enum MysqlIsolationLevel { + MysqlIsoRepeatableRead = 0, + MysqlIsoReadCommitted = 1, + MysqlIsoReadUnCommitted = 2, + MysqlIsoSerializable = 3, +}; +// mysql transaction options +struct MysqlTransactionOptions { + // if is readonly transaction + MysqlTransactionOptions() : readonly(false), isolation_level(MysqlIsoRepeatableRead) {} + bool readonly; + MysqlIsolationLevel isolation_level; +}; +// MysqlTransaction Unique Ptr +class MysqlTransaction; +typedef std::unique_ptr MysqlTransactionUniquePtr; +// mysql transaction type +class MysqlTransaction { +public: + ~MysqlTransaction(); + SocketId GetSocketId() const; + // commit transaction + bool commit(); + // rollback transaction + bool rollback(); + +private: + MysqlTransaction(Channel& channel, SocketUniquePtr& socket, ConnectionType connection_type); + bool DoneTransaction(const char* command); + DISALLOW_COPY_AND_ASSIGN(MysqlTransaction); + + friend MysqlTransactionUniquePtr NewMysqlTransaction(Channel& channel, + const MysqlTransactionOptions& opts); + +private: + Channel& _channel; + SocketUniquePtr _socket; + ConnectionType _connection_type; +}; + +inline MysqlTransaction::MysqlTransaction(Channel& channel, + SocketUniquePtr& socket, + ConnectionType connection_type) + : _channel(channel), _connection_type(connection_type) { + _socket.reset(socket.release()); +} + +inline MysqlTransaction::~MysqlTransaction() { + CHECK(rollback()) << "rollback failed"; +} + +inline bool MysqlTransaction::commit() { + return DoneTransaction("COMMIT"); +} + +inline bool MysqlTransaction::rollback() { + return DoneTransaction("ROLLBACK"); +} + +MysqlTransactionUniquePtr NewMysqlTransaction( + Channel& channel, const MysqlTransactionOptions& opts = MysqlTransactionOptions()); + +} // namespace brpc + +#endif diff --git a/src/brpc/options.proto b/src/brpc/options.proto index 3e34b5f6f6..4df7cff13d 100644 --- a/src/brpc/options.proto +++ b/src/brpc/options.proto @@ -64,6 +64,7 @@ enum ProtocolType { PROTOCOL_CDS_AGENT = 24; // Client side only PROTOCOL_ESP = 25; // Client side only PROTOCOL_H2 = 26; + PROTOCOL_MYSQL = 27; // Client side only } enum CompressType { diff --git a/src/brpc/policy/mysql_auth_hash.cpp b/src/brpc/policy/mysql_auth_hash.cpp new file mode 100644 index 0000000000..52e7acd96b --- /dev/null +++ b/src/brpc/policy/mysql_auth_hash.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2.0, as + * published by the Free Software Foundation. + * + * This program is also distributed with certain software (including + * but not limited to OpenSSL) that is licensed under separate terms, + * as designated in a particular file or component or in included license + * documentation. The authors of MySQL hereby grant you an + * additional permission to link the program and your derivative works + * with the separately licensed software that they have included with + * MySQL. + * + * Without limiting anything contained in the foregoing, this file, + * which is part of MySQL Connector/C++, is also subject to the + * Universal FOSS Exception, version 1.0, a copy of which can be found at + * http://oss.oracle.com/licenses/universal-foss-exception. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License, version 2.0, for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mysql_auth_hash.h" +#include +#include +#include // memset +#include "butil/logging.h" // LOG() + +// Avoid warnings from protobuf +#if defined __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#elif defined _MSC_VER +#pragma warning(push) +#endif + +#include + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#elif defined _MSC_VER +#pragma warning(pop) +#endif + + +#define PVERSION41_CHAR '*' +#define SCRAMBLE_LENGTH 20 +#define SHA1_HASH_SIZE 20 +#define SHA256_HASH_SIZE 32 + +typedef unsigned char byte; +typedef size_t length_t; + +class SHA { + SHA_CTX m_sha; + + void init() { + SHA1_Init(&m_sha); + } + +public: + enum { DIGEST_SIZE = SHA1_HASH_SIZE }; // in Bytes + + SHA() { + init(); + } + + void Update(byte* data, length_t length) { + SHA1_Update(&m_sha, + data, +#ifdef WITH_SSL_WOLFSSL + (unsigned long) +#endif + length); + } + + void Final(byte* hash) { + SHA1_Final(hash, &m_sha); + init(); + } + + size_t getDigestSize() const { + return SHA1_HASH_SIZE; + } +}; + +class SHA256 { + SHA256_CTX m_sha; + + void init() { + SHA256_Init(&m_sha); + } + +public: + enum { DIGEST_SIZE = SHA256_HASH_SIZE }; // in Bytes + + SHA256() { + init(); + } + + void Update(byte* data, length_t length) { + SHA256_Update(&m_sha, data, length); + } + + void Final(byte* hash) { + SHA256_Final(hash, &m_sha); + init(); + } + + size_t getDigestSize() const { + return SHA256_HASH_SIZE; + } +}; + + +static void my_crypt(uint8_t* to, const uint8_t* s1, const uint8_t* s2, size_t len) { + const uint8_t* s1_end = s1 + len; + + while (s1 < s1_end) + *to++ = *s1++ ^ *s2++; +} + + +template +static std::string scramble(const std::string& scramble_data, const std::string& password) { + SHA_Crypt sha; + + if (scramble_data.length() != SCRAMBLE_LENGTH) + throw std::invalid_argument("Password scramble data is invalid"); + + byte hash_stage1[SHA_Crypt::DIGEST_SIZE]; + byte hash_stage2[SHA_Crypt::DIGEST_SIZE]; + byte result_buf[SHA_Crypt::DIGEST_SIZE + 1]; + + memset(result_buf, 0, sizeof(result_buf)); + + /* Two stage SHA1 hash of the pwd */ + /* Stage 1: hash pwd */ + sha.Update((byte*)password.data(), (length_t)password.length()); + sha.Final(hash_stage1); + + /* Stage 2 : hash first stage's output. */ + sha.Update(hash_stage1, sha.getDigestSize()); + sha.Final(hash_stage2); + + /* create crypt string as sha1(message, hash_stage2) */; + /* MYSQL41 and SHA256_PASSWORD have different behaviors here! Bug? */ + if (sha.getDigestSize() == SHA1_HASH_SIZE) { + sha.Update((byte*)scramble_data.data(), (length_t)scramble_data.length()); + sha.Update(hash_stage2, sha.getDigestSize()); + } else { + sha.Update(hash_stage2, sha.getDigestSize()); + sha.Update((byte*)scramble_data.data(), (length_t)scramble_data.length()); + } + sha.Final(result_buf); + result_buf[sha.getDigestSize()] = '\0'; + + my_crypt(result_buf, result_buf, hash_stage1, sha.getDigestSize()); + + return std::string((char*)result_buf, sha.getDigestSize()); +} + +static char* octet2hex(char* to, const char* str, size_t len) { + const char* _dig_vec_upper = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + const char* str_end = str + len; + for (; str != str_end; ++str) { + *to++ = _dig_vec_upper[((uint8_t)*str) >> 4]; + *to++ = _dig_vec_upper[((uint8_t)*str) & 0x0F]; + } + *to = '\0'; + return to; +} + +// MYSQL41 specific + +std::string brpc::policy::mysql_build_mysql41_authentication_response(const std::string& salt_data, + const std::string& password) { + std::string data; + std::string password_hash; + if (password.length()) { + password_hash = scramble(salt_data, password); + } + data.append(password_hash); // pass + + return data; +} + +// SHA256_MEMORY specific + +static std::string get_password_from_salt_sha256(const std::string& hash_stage2) { + std::string result(2 * SHA256_HASH_SIZE + 1, '\0'); + + if (hash_stage2.length() != SHA256_HASH_SIZE) + throw std::invalid_argument("Wrong size of binary hash password"); + + octet2hex(&result[0], &hash_stage2[0], SHA256_HASH_SIZE); + + return result; +} + +std::string brpc::policy::mysql_build_sha256_authentication_response(const std::string& salt_data, + const std::string& user, + const std::string& password, + const std::string& schema) { + std::string password_hash; + std::string data; + + password_hash = scramble(salt_data, password); + password_hash = get_password_from_salt_sha256(password_hash); + // REMOVE ending \0 + password_hash.pop_back(); + + data.append(schema).push_back('\0'); // authz + data.append(user).push_back('\0'); // authc + data.append(password_hash); // pass + + return data; +} diff --git a/src/brpc/policy/mysql_auth_hash.h b/src/brpc/policy/mysql_auth_hash.h new file mode 100644 index 0000000000..b037474637 --- /dev/null +++ b/src/brpc/policy/mysql_auth_hash.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, version 2.0, as + * published by the Free Software Foundation. + * + * This program is also distributed with certain software (including + * but not limited to OpenSSL) that is licensed under separate terms, + * as designated in a particular file or component or in included license + * documentation. The authors of MySQL hereby grant you an + * additional permission to link the program and your derivative works + * with the separately licensed software that they have included with + * MySQL. + * + * Without limiting anything contained in the foregoing, this file, + * which is part of MySQL Connector/C++, is also subject to the + * Universal FOSS Exception, version 1.0, a copy of which can be found at + * http://oss.oracle.com/licenses/universal-foss-exception. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License, version 2.0, for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef BRPC_POLICY_MYSQL_AUTH_HASH_H +#define BRPC_POLICY_MYSQL_AUTH_HASH_H + +// #include +#include + +namespace brpc { +namespace policy { + +std::string mysql_build_mysql41_authentication_response(const std::string& salt_data, + const std::string& password); + +std::string mysql_build_sha256_authentication_response(const std::string& salt_data, + const std::string& user, + const std::string& password, + const std::string& schema); +} // namespace policy +} // namespace brpc + +#endif diff --git a/src/brpc/policy/mysql_authenticator.cpp b/src/brpc/policy/mysql_authenticator.cpp new file mode 100644 index 0000000000..1e1fd5fa1f --- /dev/null +++ b/src/brpc/policy/mysql_authenticator.cpp @@ -0,0 +1,176 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author(s): Yang,Liming + +#include +#include "brpc/policy/mysql_authenticator.h" +#include "brpc/policy/mysql_auth_hash.h" +#include "brpc/mysql_command.h" +#include "brpc/mysql_reply.h" +#include "brpc/mysql_common.h" +#include "butil/base64.h" +#include "butil/iobuf.h" +#include "butil/logging.h" // LOG() +#include "butil/sys_byteorder.h" + +namespace brpc { +namespace policy { + +namespace { +const butil::StringPiece mysql_native_password("mysql_native_password"); +const char* auth_param_delim = "\t"; +bool MysqlHandleParams(const butil::StringPiece& params, std::string* param_cmd) { + if (params.empty()) { + return true; + } + const char* delim1 = "&"; + std::vector idx; + for (size_t p = params.find(delim1); p != butil::StringPiece::npos; + p = params.find(delim1, p + 1)) { + idx.push_back(p); + } + + const char* delim2 = "="; + std::stringstream ss; + for (size_t i = 0; i < idx.size() + 1; ++i) { + size_t pos = (i > 0) ? idx[i - 1] + 1 : 0; + size_t len = (i < idx.size()) ? idx[i] - pos : params.size() - pos; + butil::StringPiece raw(params.data() + pos, len); + const size_t p = raw.find(delim2); + if (p != butil::StringPiece::npos) { + butil::StringPiece k(raw.data(), p); + butil::StringPiece v(raw.data() + p + 1, raw.size() - p - 1); + if (k == "charset") { + ss << "SET NAMES " << v << ";"; + } else { + ss << "SET " << k << "=" << v << ";"; + } + } + } + *param_cmd = ss.str(); + return true; +} +}; // namespace + +// user + "\t" + password + "\t" + schema + "\t" + collation + "\t" + param +bool MysqlAuthenticator::SerializeToString(std::string* str) const { + std::stringstream ss; + ss << _user << auth_param_delim; + ss << _passwd << auth_param_delim; + ss << _schema << auth_param_delim; + ss << _collation << auth_param_delim; + std::string param_cmd; + if (MysqlHandleParams(_params, ¶m_cmd)) { + ss << param_cmd; + } else { + LOG(ERROR) << "handle mysql authentication params failed, ignore it"; + return false; + } + *str = ss.str(); + return true; +} + +void MysqlParseAuthenticator(const butil::StringPiece& raw, + std::string* user, + std::string* password, + std::string* schema, + std::string* collation) { + std::vector idx; + idx.reserve(4); + for (size_t p = raw.find(auth_param_delim); p != butil::StringPiece::npos; + p = raw.find(auth_param_delim, p + 1)) { + idx.push_back(p); + } + user->assign(raw.data(), 0, idx[0]); + password->assign(raw.data(), idx[0] + 1, idx[1] - idx[0] - 1); + schema->assign(raw.data(), idx[1] + 1, idx[2] - idx[1] - 1); + collation->assign(raw.data(), idx[2] + 1, idx[3] - idx[2] - 1); +} + +void MysqlParseParams(const butil::StringPiece& raw, std::string* params) { + size_t idx = raw.rfind(auth_param_delim); + params->assign(raw.data(), idx + 1, raw.size() - idx - 1); +} + +int MysqlPackAuthenticator(const MysqlReply::Auth& auth, + const butil::StringPiece& user, + const butil::StringPiece& password, + const butil::StringPiece& schema, + const butil::StringPiece& collation, + std::string* auth_cmd) { + const uint16_t capability = + butil::ByteSwapToLE16((schema == "" ? 0x8285 : 0x828d) & auth.capability()); + const uint16_t extended_capability = butil::ByteSwapToLE16(0x000b & auth.extended_capability()); + butil::IOBuf salt; + salt.append(auth.salt().data(), auth.salt().size()); + salt.append(auth.salt2().data(), auth.salt2().size()); + if (auth.auth_plugin() == mysql_native_password) { + salt = mysql_build_mysql41_authentication_response(salt.to_string(), password.data()); + } else { + LOG(ERROR) << "no support auth plugin [" << auth.auth_plugin() << "]"; + return 1; + } + + butil::IOBuf payload; + payload.append(&capability, 2); + payload.append(&extended_capability, 2); + payload.push_back(0x00); + payload.push_back(0x00); + payload.push_back(0x00); + payload.push_back(0x00); + auto iter = MysqlCollations.find(collation.data()); + if (iter == MysqlCollations.end()) { + LOG(ERROR) << "wrong collation [" << collation << "]"; + return 1; + } + payload.append(&iter->second, 1); + const std::string stuff(23, '\0'); + payload.append(stuff); + payload.append(user.data()); + payload.push_back('\0'); + payload.append(pack_encode_length(salt.size())); + payload.append(salt); + if (schema != "") { + payload.append(schema.data()); + payload.push_back('\0'); + } + if (auth.auth_plugin() == mysql_native_password) { + payload.append(mysql_native_password.data(), mysql_native_password.size()); + payload.push_back('\0'); + } + butil::IOBuf message; + const uint32_t payload_size = butil::ByteSwapToLE32(payload.size()); + // header + message.append(&payload_size, 3); + message.push_back(0x01); + // payload + message.append(payload); + *auth_cmd = message.to_string(); + return 0; +} + +int MysqlPackParams(const butil::StringPiece& params, std::string* param_cmd) { + if (!params.empty()) { + butil::IOBuf buf; + MysqlMakeCommand(&buf, MYSQL_COM_QUERY, params); + buf.copy_to(param_cmd); + return 0; + } + LOG(ERROR) << "empty connection params"; + return 1; +} + +} // namespace policy +} // namespace brpc diff --git a/src/brpc/policy/mysql_authenticator.h b/src/brpc/policy/mysql_authenticator.h new file mode 100644 index 0000000000..e3494b61ac --- /dev/null +++ b/src/brpc/policy/mysql_authenticator.h @@ -0,0 +1,88 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author(s): Yang,Liming + +#ifndef BRPC_POLICY_MYSQL_AUTHENTICATOR_H +#define BRPC_POLICY_MYSQL_AUTHENTICATOR_H + +#include "butil/iobuf.h" +#include "brpc/authenticator.h" +#include "brpc/mysql_reply.h" + +namespace brpc { +namespace policy { +// Request to mysql for authentication. +class MysqlAuthenticator : public Authenticator { +public: + MysqlAuthenticator(const butil::StringPiece& user, + const butil::StringPiece& passwd, + const butil::StringPiece& schema, + const butil::StringPiece& params = "", + const butil::StringPiece& collation = MysqlDefaultCollation) + : _user(user.data(), user.size()), + _passwd(passwd.data(), passwd.size()), + _schema(schema.data(), schema.size()), + _params(params.data(), params.size()), + _collation(collation.data(), collation.size()) {} + + int GenerateCredential(std::string* auth_str) const { + return 0; + } + + int VerifyCredential(const std::string&, const butil::EndPoint&, brpc::AuthContext*) const { + return 0; + } + + const butil::StringPiece user() const; + const butil::StringPiece passwd() const; + const butil::StringPiece schema() const; + const butil::StringPiece params() const; + const butil::StringPiece collation() const; + bool SerializeToString(std::string* str) const; + +private: + DISALLOW_COPY_AND_ASSIGN(MysqlAuthenticator); + + const std::string _user; + const std::string _passwd; + const std::string _schema; + const std::string _params; + const std::string _collation; +}; + +inline const butil::StringPiece MysqlAuthenticator::user() const { + return _user; +} + +inline const butil::StringPiece MysqlAuthenticator::passwd() const { + return _passwd; +} + +inline const butil::StringPiece MysqlAuthenticator::schema() const { + return _schema; +} + +inline const butil::StringPiece MysqlAuthenticator::params() const { + return _params; +} + +inline const butil::StringPiece MysqlAuthenticator::collation() const { + return _collation; +} + +} // namespace policy +} // namespace brpc + +#endif // BRPC_POLICY_COUCHBASE_AUTHENTICATOR_H diff --git a/src/brpc/policy/mysql_protocol.cpp b/src/brpc/policy/mysql_protocol.cpp new file mode 100644 index 0000000000..dd1cf8a1b6 --- /dev/null +++ b/src/brpc/policy/mysql_protocol.cpp @@ -0,0 +1,416 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#include // MethodDescriptor +#include // Message +#include +#include +#include "butil/logging.h" // LOG() +#include "butil/time.h" +#include "butil/iobuf.h" // butil::IOBuf +#include "butil/sys_byteorder.h" +#include "brpc/controller.h" // Controller +#include "brpc/details/controller_private_accessor.h" +#include "brpc/socket.h" // Socket +#include "brpc/server.h" // Server +#include "brpc/details/server_private_accessor.h" +#include "brpc/span.h" +#include "brpc/mysql.h" +#include "brpc/policy/mysql_authenticator.h" +#include "brpc/policy/mysql_protocol.h" + +namespace brpc { + +DECLARE_bool(enable_rpcz); + +namespace policy { + +DEFINE_bool(mysql_verbose, false, "[DEBUG] Print EVERY mysql request/response"); + +void MysqlParseAuthenticator(const butil::StringPiece& raw, + std::string* user, + std::string* password, + std::string* schema, + std::string* collation); +void MysqlParseParams(const butil::StringPiece& raw, std::string* params); +// pack mysql authentication_data +int MysqlPackAuthenticator(const MysqlReply::Auth& auth, + const butil::StringPiece& user, + const butil::StringPiece& password, + const butil::StringPiece& schema, + const butil::StringPiece& collation, + std::string* auth_cmd); +int MysqlPackParams(const butil::StringPiece& params, std::string* param_cmd); + +namespace { +// I really don't want to add a variable in controller, so I use AuthContext group to mark auth +// step. +const char* auth_step[] = {"AUTH_OK", "PARAMS_OK"}; + +struct InputResponse : public InputMessageBase { + bthread_id_t id_wait; + MysqlResponse response; + + // @InputMessageBase + void DestroyImpl() { + delete this; + } +}; + +bool PackRequest(butil::IOBuf* buf, + ControllerPrivateAccessor& accessor, + const butil::IOBuf& request) { + if (accessor.pipelined_count() == MYSQL_PREPARED_STATEMENT) { + Socket* sock = accessor.get_sending_socket(); + if (sock == NULL) { + LOG(ERROR) << "[MYSQL PACK] get sending socket with NULL"; + return false; + } + auto stub = accessor.get_stmt(); + if (stub == NULL) { + LOG(ERROR) << "[MYSQL PACK] get prepare statement with NULL"; + return false; + } + uint32_t stmt_id; + // if can't found stmt_id in this socket, create prepared statement on it, store user + // request. + if ((stmt_id = stub->stmt()->StatementId(sock->id())) == 0) { + butil::IOBuf b; + butil::Status st = MysqlMakeCommand(&b, MYSQL_COM_STMT_PREPARE, stub->stmt()->str()); + if (!st.ok()) { + LOG(ERROR) << "[MYSQL PACK] make prepare statement error " << st; + return false; + } + accessor.set_pipelined_count(MYSQL_NEED_PREPARE); + buf->append(b); + return true; + } + // else pack execute header with stmt_id + butil::Status st = stub->PackExecuteCommand(buf, stmt_id); + if (!st.ok()) { + LOG(ERROR) << "write execute data error " << st; + return false; + } + return true; + } + buf->append(request); + return true; +} + +ParseError HandleAuthentication(const InputResponse* msg, const Socket* socket, PipelinedInfo* pi) { + const bthread_id_t cid = pi->id_wait; + Controller* cntl = NULL; + if (bthread_id_lock(cid, (void**)&cntl) != 0) { + LOG(ERROR) << "[MYSQL PARSE] fail to lock controller"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + + ParseError parseCode = PARSE_OK; + const AuthContext* ctx = socket->auth_context(); + if (ctx == NULL) { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] auth context is null"; + goto END_OF_AUTH; + } + if (msg->response.reply(0).is_auth()) { + std::string user, password, schema, collation, auth_cmd; + const MysqlReply& reply = msg->response.reply(0); + MysqlParseAuthenticator(ctx->user(), &user, &password, &schema, &collation); + if (MysqlPackAuthenticator(reply.auth(), user, password, schema, collation, &auth_cmd) == + 0) { + butil::IOBuf buf; + buf.append(auth_cmd); + buf.cut_into_file_descriptor(socket->fd()); + const_cast(ctx)->set_group(auth_step[0]); + } else { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] wrong pack authentication data"; + } + } else if (msg->response.reply_size() > 0) { + for (size_t i = 0; i < msg->response.reply_size(); ++i) { + if (!msg->response.reply(i).is_ok()) { + LOG(ERROR) << "[MYSQL PARSE] auth failed " << msg->response; + parseCode = PARSE_ERROR_NO_RESOURCE; + goto END_OF_AUTH; + } + } + std::string params, params_cmd; + MysqlParseParams(ctx->user(), ¶ms); + if (ctx->group() == auth_step[0] && !params.empty()) { + if (MysqlPackParams(params, ¶ms_cmd) == 0) { + butil::IOBuf buf; + buf.append(params_cmd); + buf.cut_into_file_descriptor(socket->fd()); + const_cast(ctx)->set_group(auth_step[1]); + } else { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] wrong pack params data"; + } + } else { + butil::IOBuf raw_req; + raw_req.append(ctx->starter()); + raw_req.cut_into_file_descriptor(socket->fd()); + pi->with_auth = false; + } + } else { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] wrong authentication step"; + } + +END_OF_AUTH: + if (bthread_id_unlock(cid) != 0) { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] fail to unlock controller"; + } + return parseCode; +} + +ParseError HandlePrepareStatement(const InputResponse* msg, + const Socket* socket, + PipelinedInfo* pi) { + if (!msg->response.reply(0).is_prepare_ok()) { + LOG(ERROR) << "[MYSQL PARSE] response is not prepare ok, " << msg->response; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + const MysqlReply::PrepareOk& ok = msg->response.reply(0).prepare_ok(); + const bthread_id_t cid = pi->id_wait; + Controller* cntl = NULL; + if (bthread_id_lock(cid, (void**)&cntl) != 0) { + LOG(ERROR) << "[MYSQL PARSE] fail to lock controller"; + return PARSE_ERROR_ABSOLUTELY_WRONG; + } + ParseError parseCode = PARSE_OK; + butil::IOBuf buf; + butil::Status st; + auto stub = ControllerPrivateAccessor(cntl).get_stmt(); + auto stmt = stub->stmt(); + if (stmt == NULL || stmt->param_count() != ok.param_count()) { + LOG(ERROR) << "[MYSQL PACK] stmt can't be NULL"; + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + goto END_OF_PREPARE; + } + if (stmt->param_count() != ok.param_count()) { + LOG(ERROR) << "[MYSQL PACK] stmt param number " << stmt->param_count() + << " not equal to prepareOk.param_number " << ok.param_count(); + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + goto END_OF_PREPARE; + } + stmt->SetStatementId(socket->id(), ok.stmt_id()); + st = stub->PackExecuteCommand(&buf, ok.stmt_id()); + if (!st.ok()) { + LOG(ERROR) << "[MYSQL PACK] make execute header error " << st; + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + goto END_OF_PREPARE; + } + buf.cut_into_file_descriptor(socket->fd()); + pi->count = MYSQL_PREPARED_STATEMENT; +END_OF_PREPARE: + if (bthread_id_unlock(cid) != 0) { + parseCode = PARSE_ERROR_ABSOLUTELY_WRONG; + LOG(ERROR) << "[MYSQL PARSE] fail to unlock controller"; + } + return parseCode; +} + +} // namespace + +// "Message" = "Response" as we only implement the client for mysql. +ParseResult ParseMysqlMessage(butil::IOBuf* source, + Socket* socket, + bool /*read_eof*/, + const void* /*arg*/) { + if (source->empty()) { + return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); + } + + PipelinedInfo pi; + if (!socket->PopPipelinedInfo(&pi)) { + LOG(WARNING) << "No corresponding PipelinedInfo in socket"; + return MakeParseError(PARSE_ERROR_TRY_OTHERS); + } + + InputResponse* msg = static_cast(socket->parsing_context()); + if (msg == NULL) { + msg = new InputResponse; + socket->reset_parsing_context(msg); + } + + MysqlStmtType stmt_type = static_cast(pi.count); + ParseError err = msg->response.ConsumePartialIOBuf(*source, pi.with_auth, stmt_type); + if (FLAGS_mysql_verbose) { + LOG(INFO) << "[MYSQL PARSE] " << msg->response; + } + if (err != PARSE_OK) { + if (err == PARSE_ERROR_NOT_ENOUGH_DATA) { + socket->GivebackPipelinedInfo(pi); + } + return MakeParseError(err); + } + if (pi.with_auth) { + ParseError err = HandleAuthentication(msg, socket, &pi); + if (err != PARSE_OK) { + return MakeParseError(err, "Fail to authenticate with Mysql"); + } + DestroyingPtr auth_msg = + static_cast(socket->release_parsing_context()); + socket->GivebackPipelinedInfo(pi); + return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); + } + if (stmt_type == MYSQL_NEED_PREPARE) { + // store stmt_id, make execute header. + ParseError err = HandlePrepareStatement(msg, socket, &pi); + if (err != PARSE_OK) { + return MakeParseError(err, "Fail to make parepared statement with Mysql"); + } + DestroyingPtr prepare_msg = + static_cast(socket->release_parsing_context()); + socket->GivebackPipelinedInfo(pi); + return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); + } + msg->id_wait = pi.id_wait; + socket->release_parsing_context(); + return MakeMessage(msg); +} + +void ProcessMysqlResponse(InputMessageBase* msg_base) { + const int64_t start_parse_us = butil::cpuwide_time_us(); + DestroyingPtr msg(static_cast(msg_base)); + + const bthread_id_t cid = msg->id_wait; + Controller* cntl = NULL; + const int rc = bthread_id_lock(cid, (void**)&cntl); + if (rc != 0) { + LOG_IF(ERROR, rc != EINVAL && rc != EPERM) + << "Fail to lock correlation_id=" << cid << ": " << berror(rc); + return; + } + + ControllerPrivateAccessor accessor(cntl); + Span* span = accessor.span(); + if (span) { + span->set_base_real_us(msg->base_real_us()); + span->set_received_us(msg->received_us()); + span->set_response_size(msg->response.ByteSize()); + span->set_start_parse_us(start_parse_us); + } + const int saved_error = cntl->ErrorCode(); + if (cntl->response() != NULL) { + if (cntl->response()->GetDescriptor() != MysqlResponse::descriptor()) { + cntl->SetFailed(ERESPONSE, "Must be MysqlResponse"); + } else { + // We work around ParseFrom of pb which is just a placeholder. + ((MysqlResponse*)cntl->response())->Swap(&msg->response); + } + } // silently ignore the response. + + // Unlocks correlation_id inside. Revert controller's + // error code if it version check of `cid' fails + msg.reset(); // optional, just release resourse ASAP + accessor.OnResponse(cid, saved_error); +} + +void SerializeMysqlRequest(butil::IOBuf* buf, + Controller* cntl, + const google::protobuf::Message* request) { + if (request == NULL) { + return cntl->SetFailed(EREQUEST, "request is NULL"); + } + if (request->GetDescriptor() != MysqlRequest::descriptor()) { + return cntl->SetFailed(EREQUEST, "The request is not a MysqlRequest"); + } + const MysqlRequest* rr = (const MysqlRequest*)request; + // We work around SerializeTo of pb which is just a placeholder. + if (!rr->SerializeTo(buf)) { + return cntl->SetFailed(EREQUEST, "Fail to serialize MysqlRequest"); + } + // mysql protocol don't use pipelined count to verify the end of a response, so pipelined count + // is meanless, but we can use it help us to distinguish mysql reply type. In mysql protocol, we + // can't distinguish OK and PreparedOk, so we set pipelined count to 2 to let parse function to + // parse PreparedOk reply + ControllerPrivateAccessor accessor(cntl); + accessor.set_pipelined_count(MYSQL_NORMAL_STATEMENT); + + auto tx = rr->get_tx(); + if (tx != NULL) { + accessor.use_bind_sock(tx->GetSocketId()); + } + auto st = rr->get_stmt(); + if (st != NULL) { + accessor.set_stmt(st); + accessor.set_pipelined_count(MYSQL_PREPARED_STATEMENT); + } + if (FLAGS_mysql_verbose) { + LOG(INFO) << "\n[MYSQL REQUEST] " << *rr; + } +} + +void PackMysqlRequest(butil::IOBuf* buf, + SocketMessage**, + uint64_t /*correlation_id*/, + const google::protobuf::MethodDescriptor*, + Controller* cntl, + const butil::IOBuf& request, + const Authenticator* auth) { + ControllerPrivateAccessor accessor(cntl); + if (auth) { + const MysqlAuthenticator* my_auth(dynamic_cast(auth)); + if (my_auth == NULL) { + LOG(ERROR) << "[MYSQL PACK] there is not MysqlAuthenticator"; + return; + } + Socket* sock = accessor.get_sending_socket(); + if (sock == NULL) { + LOG(ERROR) << "[MYSQL PACK] get sending socket with NULL"; + return; + } + AuthContext* ctx = sock->mutable_auth_context(); + // std::string params; + // if (!MysqlHandleParams(my_auth->params(), ¶ms)) { + // LOG(ERROR) << "[MYSQL PACK] handle params error"; + // return; + // } + // std::stringstream ss; + // ss << my_auth->user() << "\t" << my_auth->passwd() << "\t" << my_auth->schema() << "\t" + // << my_auth->collation() << "\t" << params; + std::string str; + if (!my_auth->SerializeToString(&str)) { + LOG(ERROR) << "[MYSQL PACK] auth param serialize to string failed"; + return; + } + ctx->set_user(str); + butil::IOBuf b; + if (!PackRequest(&b, accessor, request)) { + LOG(ERROR) << "[MYSQL PACK] pack request error"; + return; + } + ctx->set_starter(b.to_string()); + accessor.add_with_auth(); + } else { + if (!PackRequest(buf, accessor, request)) { + LOG(ERROR) << "[MYSQL PACK] pack request error"; + return; + } + } +} + +const std::string& GetMysqlMethodName(const google::protobuf::MethodDescriptor*, + const Controller*) { + const static std::string MYSQL_SERVER_STR = "mysql-server"; + return MYSQL_SERVER_STR; +} + +} // namespace policy +} // namespace brpc diff --git a/src/brpc/policy/mysql_protocol.h b/src/brpc/policy/mysql_protocol.h new file mode 100644 index 0000000000..163629c6cf --- /dev/null +++ b/src/brpc/policy/mysql_protocol.h @@ -0,0 +1,52 @@ +// Copyright (c) 2019 Baidu, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Authors: Yang,Liming (yangliming01@baidu.com) + +#ifndef BRPC_POLICY_MYSQL_PROTOCOL_H +#define BRPC_POLICY_MYSQL_PROTOCOL_H + +#include "brpc/protocol.h" + + +namespace brpc { +namespace policy { + +// Parse mysql response. +ParseResult ParseMysqlMessage(butil::IOBuf* source, Socket* socket, bool read_eof, const void* arg); + +// Actions to a mysql response. +void ProcessMysqlResponse(InputMessageBase* msg); + +// Serialize a mysql request. +void SerializeMysqlRequest(butil::IOBuf* buf, + Controller* cntl, + const google::protobuf::Message* request); + +// Pack `request' to `method' into `buf'. +void PackMysqlRequest(butil::IOBuf* buf, + SocketMessage**, + uint64_t correlation_id, + const google::protobuf::MethodDescriptor* method, + Controller* controller, + const butil::IOBuf& request, + const Authenticator* auth); + +const std::string& GetMysqlMethodName(const google::protobuf::MethodDescriptor*, const Controller*); + +} // namespace policy +} // namespace brpc + + +#endif // BRPC_POLICY_MYSQL_PROTOCOL_H diff --git a/src/brpc/socket.cpp b/src/brpc/socket.cpp index e3878c198e..f4c6b1da50 100644 --- a/src/brpc/socket.cpp +++ b/src/brpc/socket.cpp @@ -428,6 +428,7 @@ Socket::Socket(Forbidden) , _fd(-1) , _tos(0) , _reset_fd_real_us(-1) + , _fd_version(0) , _on_edge_triggered_events(NULL) , _user(NULL) , _conn(NULL) @@ -533,6 +534,8 @@ int Socket::ResetFileDescriptor(int fd) { _avg_msg_size = 0; // MUST store `_fd' before adding itself into epoll device to avoid // race conditions with the callback function inside epoll + static butil::atomic BAIDU_CACHELINE_ALIGNMENT fd_version(0); + _fd_version = fd_version.fetch_add(1, butil::memory_order_relaxed); _fd.store(fd, butil::memory_order_release); _reset_fd_real_us = butil::gettimeofday_us(); if (!ValidFileDescriptor(fd)) { @@ -1512,7 +1515,7 @@ int Socket::Write(butil::IOBuf* data, const WriteOptions* options_in) { if (options_in) { opt = *options_in; } - if (data->empty()) { + if (data->empty() && !opt.with_auth) { return SetError(opt.id_wait, EINVAL); } if (opt.pipelined_count > MAX_PIPELINED_COUNT) { diff --git a/src/brpc/socket.h b/src/brpc/socket.h index 6f710ee2a9..e839efbe22 100644 --- a/src/brpc/socket.h +++ b/src/brpc/socket.h @@ -283,6 +283,9 @@ friend class policy::H2GlobalStreamCreator; // The file descriptor int fd() const { return _fd.load(butil::memory_order_relaxed); } + // The file descriptor version, used to avoid ABA problem. + uint64_t fd_version() const { return _fd_version; } + // ip/port of the local end of the connection butil::EndPoint local_side() const { return _local_side; } @@ -720,6 +723,7 @@ friend void DereferenceSocket(Socket*); butil::atomic _fd; // -1 when not connected. int _tos; // Type of service which is actually only 8bits. int64_t _reset_fd_real_us; // When _fd was reset, in microseconds. + uint64_t _fd_version; // _fd_version, use only for mysql now. // Address of peer. Initialized by SocketOptions.remote_side. butil::EndPoint _remote_side; diff --git a/test/brpc_mysql_unittest.cpp b/test/brpc_mysql_unittest.cpp new file mode 100644 index 0000000000..7668749a8d --- /dev/null +++ b/test/brpc_mysql_unittest.cpp @@ -0,0 +1,852 @@ +// Copyright (c) 2019 Baidu, Inc. +// Date: Thu Jun 11 14:30:07 CST 2019 + +#include +#include +#include +#include "butil/time.h" +#include +#include +#include "butil/logging.h" // LOG() +#include "butil/strings/string_piece.h" +#include +#include + +namespace brpc { +const std::string MYSQL_connection_type = "pooled"; +const int MYSQL_timeout_ms = 80000; +const int MYSQL_connect_timeout_ms = 80000; + +// const std::string MYSQL_host = "127.0.0.1"; +const std::string MYSQL_host = "db4free.net"; +const std::string MYSQL_port = "3306"; +const std::string MYSQL_user = "brpcuser"; +const std::string MYSQL_password = "12345678"; +const std::string MYSQL_schema = "brpc_test"; +int64_t MYSQL_table_suffix; +} // namespace brpc + +int main(int argc, char* argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +namespace { +static pthread_once_t check_mysql_server_once = PTHREAD_ONCE_INIT; + +static void CheckMysqlServer() { + brpc::MYSQL_table_suffix = butil::gettimeofday_us(); + puts("Checking mysql-server..."); + std::stringstream ss; + ss << "mysql" + << " -h" << brpc::MYSQL_host << " -P" << brpc::MYSQL_port << " -u" << brpc::MYSQL_user + << " -p" << brpc::MYSQL_password << " -D" << brpc::MYSQL_schema << " -e 'show databases'"; + puts(ss.str().c_str()); + if (system(ss.str().c_str()) != 0) { + std::stringstream ss; + ss << "please startup your mysql-server, then create \nschema:" << brpc::MYSQL_schema + << "\nuser:" << brpc::MYSQL_user << "\npassword:" << brpc::MYSQL_password; + puts(ss.str().c_str()); + return; + } +} + +class MysqlTest : public testing::Test { +protected: + MysqlTest() {} + void SetUp() { + pthread_once(&check_mysql_server_once, CheckMysqlServer); + } + void TearDown() {} +}; + +TEST_F(MysqlTest, auth) { + // config auth + { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + request.Query("show databases"); + + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_RESULTSET, response.reply(0).type()); + } + + // Auth failed + { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = + new brpc::policy::MysqlAuthenticator(brpc::MYSQL_user, "123456789", brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + request.Query("show databases"); + + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_TRUE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(brpc::MYSQL_RSP_UNKNOWN, response.reply(0).type()); + } + + // check noauth. + { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + request.Query("show databases"); + + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_TRUE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(brpc::MYSQL_RSP_UNKNOWN, response.reply(0).type()); + } +} + +TEST_F(MysqlTest, ok) { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "drop table brpc_table_" << brpc::MYSQL_table_suffix; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + } + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "CREATE TABLE IF NOT EXISTS `brpc_table_" << brpc::MYSQL_table_suffix + << "` (`col1` int(11) NOT NULL AUTO_INCREMENT, " + "`col2` varchar(45) DEFAULT NULL, " + "`col3` decimal(6,3) DEFAULT NULL, `col4` datetime DEFAULT NULL, `col5` blob, `col6` " + "binary(6) DEFAULT NULL, `col7` tinyblob, `col8` longblob, `col9` mediumblob, " + "`col10` " + "tinyblob, `col11` varbinary(10) DEFAULT NULL, `col12` date DEFAULT NULL, `col13` " + "datetime(6) DEFAULT NULL, `col14` time DEFAULT NULL, `col15` timestamp(4) NULL " + "DEFAULT NULL, `col16` year(4) DEFAULT NULL, `col17` geometry DEFAULT NULL, `col18` " + "geometrycollection DEFAULT NULL, `col19` linestring DEFAULT NULL, `col20` point " + "DEFAULT NULL, `col21` polygon DEFAULT NULL, `col22` bigint(64) DEFAULT NULL, " + "`col23` " + "decimal(10,0) DEFAULT NULL, `col24` double DEFAULT NULL, `col25` float DEFAULT " + "NULL, " + "`col26` int(7) DEFAULT NULL, `col27` mediumint(18) DEFAULT NULL, `col28` double " + "DEFAULT NULL, `col29` smallint(2) DEFAULT NULL, `col30` tinyint(1) DEFAULT NULL, " + "`col31` char(6) DEFAULT NULL, `col32` varchar(6) DEFAULT NULL, `col33` longtext, " + "`col34` mediumtext, `col35` tinytext, `col36` tinytext, `col37` bit(7) DEFAULT " + "NULL, " + "`col38` tinyint(4) DEFAULT NULL, `col39` varchar(45) DEFAULT NULL, `col40` " + "varchar(45) CHARACTER SET utf8 DEFAULT NULL, `col41` char(4) CHARACTER SET utf8 " + "DEFAULT NULL, `col42` varchar(6) CHARACTER SET utf8 DEFAULT NULL, PRIMARY KEY " + "(`col1`)) ENGINE=InnoDB AUTO_INCREMENT=1157 DEFAULT CHARSET=utf8"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + std::stringstream ss1; + ss1 << "INSERT INTO `brpc_table_" << brpc::MYSQL_table_suffix + << "` " + "(`col2`,`col3`,`col4`,`col5`,`col6`,`col7`,`col8`,`col9`,`col10`,`col11`,`" + "col12`,`col13`,`col14`,`col15`,`col16`,`col17`,`col18`,`col19`,`col20`,`col21`, " + "`col22` " + ",`col23`,`col24`,`col25`,`col26`,`col27`,`col28`,`col29`,`col30`,`col31`,`col32`,`" + "col33`,`col34`,`col35`,`col36`,`col37`,`col38`,`col39`,`col40`,`col41`,`col42`) " + "VALUES ('col2',0.015,'2018-12-01 " + "12:13:14','aaa','bbb','ccc','ddd','eee','fff','ggg','2014-09-18', '2010-12-10 " + "14:12:09.019473' ,'01:06:09','1970-12-08 00:00:00.0001' " + ",2014,NULL,NULL,NULL,NULL,NULL,69,'12.5',16.9,6.7,24,37,69.56,234,6, '" + "col31','col32','col33','col34','col35','col36',NULL,9,'col39','col40','col4' ,'" + "col42')"; + request.Query(ss1.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } +} + +TEST_F(MysqlTest, error) { + { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + request.Query("select nocol from notable"); + + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_ERROR, response.reply(0).type()); + } +} + +TEST_F(MysqlTest, resultset) { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema, "charset=utf8"); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + { + for (int i = 0; i < 50; ++i) { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + + std::stringstream ss1; + ss1 << "INSERT INTO `brpc_table_" << brpc::MYSQL_table_suffix + << "` " + "(`col2`,`col3`,`col4`,`col5`,`col6`,`col7`,`col8`,`col9`,`col10`,`col11`" + ",`" + "col12`,`col13`,`col14`,`col15`,`col16`,`col17`,`col18`,`col19`,`col20`,`col21`," + " " + "`col22` " + ",`col23`,`col24`,`col25`,`col26`,`col27`,`col28`,`col29`,`col30`,`col31`,`" + "col32`,`" + "col33`,`col34`,`col35`,`col36`,`col37`,`col38`,`col39`,`col40`,`col41`,`col42`)" + " VALUES ('col2',0.015,'2018-12-01 " + "12:13:14','aaa','bbb','ccc','ddd','eee','fff','ggg','2014-09-18', '2010-12-10 " + "14:12:09.019473' ,'01:06:09','1970-12-08 00:00:00.0001' " + ",2014,NULL,NULL,NULL,NULL,NULL,69,'12.5',16.9,6.7,24,37,69.56,234,6, '" + "col31','col32','col33','col34','col35','col36',NULL,9,'col39','col40','col4' " + ",'" + "col42')"; + request.Query(ss1.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } + } + + { + std::stringstream ss1; + for (int i = 0; i < 30; ++i) { + ss1 << "INSERT INTO `brpc_table_" << brpc::MYSQL_table_suffix + << "` " + "(`col2`,`col3`,`col4`,`col5`,`col6`,`col7`,`col8`,`col9`,`col10`,`col11`" + ",`" + "col12`,`col13`,`col14`,`col15`,`col16`,`col17`,`col18`,`col19`,`col20`,`col21`," + " " + "`col22` " + ",`col23`,`col24`,`col25`,`col26`,`col27`,`col28`,`col29`,`col30`,`col31`,`" + "col32`,`" + "col33`,`col34`,`col35`,`col36`,`col37`,`col38`,`col39`,`col40`,`col41`,`col42`)" + "VALUES ('col2',0.015,'2018-12-01 " + "12:13:14','aaa','bbb','ccc','ddd','eee','fff','ggg','2014-09-18', '2010-12-10 " + "14:12:09.019473' ,'01:06:09','1970-12-08 00:00:00.0001' " + ",2014,NULL,NULL,NULL,NULL,NULL,69,'12.5',16.9,6.7,24,37,69.56,234,6, '" + "col31','col32','col33','col34','col35','col36',NULL,9,'col39','col40','col4' " + ",'" + "col42');"; + } + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + request.Query(ss1.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(30ul, response.reply_size()); + for (int i = 0; i < 30; ++i) { + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(i).type()); + } + } + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "select count(0) from brpc_table_" << brpc::MYSQL_table_suffix; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + // ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_RESULTSET, response.reply(0).type()); + } + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "select * from brpc_table_" << brpc::MYSQL_table_suffix << " where 1 = 2"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_RESULTSET, response.reply(0).type()); + } + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "select * from brpc_table_" << brpc::MYSQL_table_suffix << " limit 10"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_RESULTSET, response.reply(0).type()); + ASSERT_EQ(42ull, response.reply(0).column_count()); + const brpc::MysqlReply& reply = response.reply(0); + ASSERT_EQ(reply.column(0).name(), "col1"); + ASSERT_EQ(reply.column(0).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(0).type(), brpc::MYSQL_FIELD_TYPE_LONG); + + ASSERT_EQ(reply.column(1).name(), "col2"); + ASSERT_EQ(reply.column(1).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(1).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + ASSERT_EQ(reply.column(2).name(), "col3"); + ASSERT_EQ(reply.column(2).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(2).type(), brpc::MYSQL_FIELD_TYPE_NEWDECIMAL); + + ASSERT_EQ(reply.column(3).name(), "col4"); + ASSERT_EQ(reply.column(3).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(3).type(), brpc::MYSQL_FIELD_TYPE_DATETIME); + + ASSERT_EQ(reply.column(4).name(), "col5"); + ASSERT_EQ(reply.column(4).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(4).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(5).name(), "col6"); + ASSERT_EQ(reply.column(5).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(5).type(), brpc::MYSQL_FIELD_TYPE_STRING); + + ASSERT_EQ(reply.column(6).name(), "col7"); + ASSERT_EQ(reply.column(6).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(6).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(7).name(), "col8"); + ASSERT_EQ(reply.column(7).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(7).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(8).name(), "col9"); + ASSERT_EQ(reply.column(8).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(8).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(9).name(), "col10"); + ASSERT_EQ(reply.column(9).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(9).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(10).name(), "col11"); + ASSERT_EQ(reply.column(10).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(10).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + ASSERT_EQ(reply.column(11).name(), "col12"); + ASSERT_EQ(reply.column(11).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(11).type(), brpc::MYSQL_FIELD_TYPE_DATE); + + ASSERT_EQ(reply.column(12).name(), "col13"); + ASSERT_EQ(reply.column(12).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(12).type(), brpc::MYSQL_FIELD_TYPE_DATETIME); + + ASSERT_EQ(reply.column(13).name(), "col14"); + ASSERT_EQ(reply.column(13).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(13).type(), brpc::MYSQL_FIELD_TYPE_TIME); + + ASSERT_EQ(reply.column(14).name(), "col15"); + ASSERT_EQ(reply.column(14).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(14).type(), brpc::MYSQL_FIELD_TYPE_TIMESTAMP); + + ASSERT_EQ(reply.column(15).name(), "col16"); + ASSERT_EQ(reply.column(15).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(15).type(), brpc::MYSQL_FIELD_TYPE_YEAR); + + ASSERT_EQ(reply.column(16).name(), "col17"); + ASSERT_EQ(reply.column(16).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(16).type(), brpc::MYSQL_FIELD_TYPE_GEOMETRY); + + ASSERT_EQ(reply.column(17).name(), "col18"); + ASSERT_EQ(reply.column(17).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(17).type(), brpc::MYSQL_FIELD_TYPE_GEOMETRY); + + ASSERT_EQ(reply.column(18).name(), "col19"); + ASSERT_EQ(reply.column(18).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(18).type(), brpc::MYSQL_FIELD_TYPE_GEOMETRY); + + ASSERT_EQ(reply.column(19).name(), "col20"); + ASSERT_EQ(reply.column(19).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(19).type(), brpc::MYSQL_FIELD_TYPE_GEOMETRY); + + ASSERT_EQ(reply.column(20).name(), "col21"); + ASSERT_EQ(reply.column(20).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(20).type(), brpc::MYSQL_FIELD_TYPE_GEOMETRY); + + ASSERT_EQ(reply.column(21).name(), "col22"); + ASSERT_EQ(reply.column(21).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(21).type(), brpc::MYSQL_FIELD_TYPE_LONGLONG); + + ASSERT_EQ(reply.column(22).name(), "col23"); + ASSERT_EQ(reply.column(22).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(22).type(), brpc::MYSQL_FIELD_TYPE_NEWDECIMAL); + + ASSERT_EQ(reply.column(23).name(), "col24"); + ASSERT_EQ(reply.column(23).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(23).type(), brpc::MYSQL_FIELD_TYPE_DOUBLE); + + ASSERT_EQ(reply.column(24).name(), "col25"); + ASSERT_EQ(reply.column(24).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(24).type(), brpc::MYSQL_FIELD_TYPE_FLOAT); + + ASSERT_EQ(reply.column(25).name(), "col26"); + ASSERT_EQ(reply.column(25).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(25).type(), brpc::MYSQL_FIELD_TYPE_LONG); + + ASSERT_EQ(reply.column(26).name(), "col27"); + ASSERT_EQ(reply.column(26).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(26).type(), brpc::MYSQL_FIELD_TYPE_INT24); + + ASSERT_EQ(reply.column(27).name(), "col28"); + ASSERT_EQ(reply.column(27).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(27).type(), brpc::MYSQL_FIELD_TYPE_DOUBLE); + + ASSERT_EQ(reply.column(28).name(), "col29"); + ASSERT_EQ(reply.column(28).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(28).type(), brpc::MYSQL_FIELD_TYPE_SHORT); + + ASSERT_EQ(reply.column(29).name(), "col30"); + ASSERT_EQ(reply.column(29).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(29).type(), brpc::MYSQL_FIELD_TYPE_TINY); + + ASSERT_EQ(reply.column(30).name(), "col31"); + ASSERT_EQ(reply.column(30).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(30).type(), brpc::MYSQL_FIELD_TYPE_STRING); + + ASSERT_EQ(reply.column(31).name(), "col32"); + ASSERT_EQ(reply.column(31).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(31).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + ASSERT_EQ(reply.column(32).name(), "col33"); + ASSERT_EQ(reply.column(32).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(32).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(33).name(), "col34"); + ASSERT_EQ(reply.column(33).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(33).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(34).name(), "col35"); + ASSERT_EQ(reply.column(34).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(34).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(35).name(), "col36"); + ASSERT_EQ(reply.column(35).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(35).type(), brpc::MYSQL_FIELD_TYPE_BLOB); + + ASSERT_EQ(reply.column(36).name(), "col37"); + ASSERT_EQ(reply.column(36).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(36).type(), brpc::MYSQL_FIELD_TYPE_BIT); + + ASSERT_EQ(reply.column(37).name(), "col38"); + ASSERT_EQ(reply.column(37).charset(), brpc::MysqlCollations.at("binary")); + ASSERT_EQ(reply.column(37).type(), brpc::MYSQL_FIELD_TYPE_TINY); + + ASSERT_EQ(reply.column(38).name(), "col39"); + ASSERT_EQ(reply.column(38).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(38).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + ASSERT_EQ(reply.column(39).name(), "col40"); + ASSERT_EQ(reply.column(39).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(39).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + ASSERT_EQ(reply.column(40).name(), "col41"); + ASSERT_EQ(reply.column(40).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(40).type(), brpc::MYSQL_FIELD_TYPE_STRING); + + ASSERT_EQ(reply.column(41).name(), "col42"); + ASSERT_EQ(reply.column(41).charset(), brpc::MysqlCollations.at("utf8_general_ci")); + ASSERT_EQ(reply.column(41).type(), brpc::MYSQL_FIELD_TYPE_VAR_STRING); + + for (uint64_t idx = 0; idx < reply.row_count(); ++idx) { + const brpc::MysqlReply::Row& row = reply.next(); + ASSERT_EQ(row.field(1).string(), "col2"); + ASSERT_EQ(row.field(2).string(), "0.015"); + ASSERT_EQ(row.field(3).string(), "2018-12-01 12:13:14"); + ASSERT_EQ(row.field(4).string(), "aaa"); + butil::StringPiece field5 = row.field(5).string(); + ASSERT_EQ(field5.size(), size_t(6)); + ASSERT_EQ(field5[0], 'b'); + ASSERT_EQ(field5[1], 'b'); + ASSERT_EQ(field5[2], 'b'); + ASSERT_EQ(field5[3], '\0'); + ASSERT_EQ(field5[4], '\0'); + ASSERT_EQ(field5[5], '\0'); + ASSERT_EQ(row.field(6).string(), "ccc"); + ASSERT_EQ(row.field(7).string(), "ddd"); + ASSERT_EQ(row.field(8).string(), "eee"); + ASSERT_EQ(row.field(9).string(), "fff"); + ASSERT_EQ(row.field(10).string(), "ggg"); + ASSERT_EQ(row.field(11).string(), "2014-09-18"); + ASSERT_EQ(row.field(12).string(), "2010-12-10 14:12:09.019473"); + ASSERT_EQ(row.field(13).string(), "01:06:09"); + ASSERT_EQ(row.field(14).string(), "1970-12-08 00:00:00.0001"); + ASSERT_EQ(row.field(15).small(), uint16_t(2014)); + ASSERT_EQ(row.field(16).is_nil(), true); + ASSERT_EQ(row.field(17).is_nil(), true); + ASSERT_EQ(row.field(18).is_nil(), true); + ASSERT_EQ(row.field(19).is_nil(), true); + ASSERT_EQ(row.field(20).is_nil(), true); + ASSERT_EQ(row.field(21).sbigint(), int64_t(69)); + ASSERT_EQ(row.field(22).string(), "13"); + ASSERT_EQ(row.field(23).float64(), double(16.9)); + ASSERT_EQ(row.field(24).float32(), float(6.7)); + ASSERT_EQ(row.field(25).sinteger(), int32_t(24)); + ASSERT_EQ(row.field(26).sinteger(), int32_t(37)); + ASSERT_EQ(row.field(27).float64(), double(69.56)); + ASSERT_EQ(row.field(28).ssmall(), int16_t(234)); + ASSERT_EQ(row.field(29).stiny(), 6); + ASSERT_EQ(row.field(30).string(), "col31"); + ASSERT_EQ(row.field(31).string(), "col32"); + ASSERT_EQ(row.field(32).string(), "col33"); + ASSERT_EQ(row.field(33).string(), "col34"); + ASSERT_EQ(row.field(34).string(), "col35"); + ASSERT_EQ(row.field(35).string(), "col36"); + ASSERT_EQ(row.field(36).is_nil(), true); + ASSERT_EQ(row.field(37).stiny(), 9); + ASSERT_EQ(row.field(38).string(), "col39"); + ASSERT_EQ(row.field(39).string(), "col40"); + ASSERT_EQ(row.field(40).string(), "col4"); // size is 4 + ASSERT_EQ(row.field(41).string(), "col42"); + } + } +} + +TEST_F(MysqlTest, transaction) { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "drop table brpc_tx_" << brpc::MYSQL_table_suffix; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + } + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "CREATE TABLE IF NOT EXISTS `brpc_tx_" << brpc::MYSQL_table_suffix + << "` (`Id` int(11) NOT NULL AUTO_INCREMENT,`LastName` " + "varchar(255) DEFAULT " + "NULL,`FirstName` decimal(10,0) DEFAULT NULL,`Address` varchar(255) DEFAULT " + "NULL,`City` varchar(255) DEFAULT NULL, PRIMARY KEY (`Id`)) ENGINE=InnoDB " + "AUTO_INCREMENT=1157 DEFAULT CHARSET=utf8"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } + { + brpc::MysqlTransactionOptions tx_options; + tx_options.readonly = false; + tx_options.isolation_level = brpc::MysqlIsoRepeatableRead; + brpc::MysqlTransactionUniquePtr tx(brpc::NewMysqlTransaction(channel, tx_options)); + ASSERT_FALSE(tx == NULL) << "Fail to create transaction"; + uint64_t idx1, idx2; + { + brpc::MysqlRequest request(tx.get()); + std::stringstream ss; + ss << "insert into brpc_tx_" << brpc::MYSQL_table_suffix + << "(LastName,FirstName, Address) values " + "('lucy',12.5,'beijing')"; + ASSERT_EQ(request.Query(ss.str()), true); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + idx1 = response.reply(0).ok().index(); + } + { + brpc::MysqlRequest request(tx.get()); + std::stringstream ss; + ss << "insert into brpc_tx_" << brpc::MYSQL_table_suffix + << "(LastName,FirstName, Address) values " + "('lilei',12.6,'shanghai')"; + ASSERT_EQ(request.Query(ss.str()), true); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + idx2 = response.reply(0).ok().index(); + } + + LOG(INFO) << "idx1=" << idx1 << " idx2=" << idx2; + // not commit, so return 0 rows + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "select * from brpc_tx_" << brpc::MYSQL_table_suffix << " where id in (" << idx1 + << "," << idx2 << ")"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).row_count(), 0ul); + } + + { ASSERT_EQ(tx->commit(), true); } + // after commit, so return 2 rows + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "select * from brpc_tx_" << brpc::MYSQL_table_suffix << " where id in (" << idx1 + << "," << idx2 << ")"; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).row_count(), 2ul); + } + } + + { + brpc::MysqlTransactionOptions tx_options; + tx_options.readonly = true; + tx_options.isolation_level = brpc::MysqlIsoReadCommitted; + + brpc::MysqlTransactionUniquePtr tx(brpc::NewMysqlTransaction(channel, tx_options)); + ASSERT_FALSE(tx == NULL) << "Fail to create transaction"; + + { + brpc::MysqlRequest request(tx.get()); + std::stringstream ss; + ss << "update brpc_tx_" << brpc::MYSQL_table_suffix + << " set Address = 'hangzhou' where Id=1"; + ASSERT_EQ(request.Query(ss.str()), true); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_ERROR, response.reply(0).type()); + } + } +} + +// mysql prepared statement +TEST_F(MysqlTest, statement) { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + // zero parameter + { + std::stringstream ss; + ss << "select * from brpc_table_" << brpc::MYSQL_table_suffix << " limit 1"; + auto stmt(brpc::NewMysqlStatement(channel, ss.str())); + ASSERT_FALSE(stmt == NULL) << "Fail to create statement"; + { + brpc::MysqlRequest request(stmt.get()); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_resultset(), true); + } + { + brpc::MysqlRequest request(stmt.get()); + ASSERT_EQ(request.AddParam(1157), true); + + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_resultset(), true); + } + } + // one parameter + { + std::stringstream ss; + ss << "select * from brpc_table_" << brpc::MYSQL_table_suffix << " where col1 = ?"; + auto stmt(brpc::NewMysqlStatement(channel, ss.str())); + ASSERT_FALSE(stmt == NULL) << "Fail to create statement"; + { + brpc::MysqlRequest request(stmt.get()); + ASSERT_EQ(request.AddParam(1157), true); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_resultset(), true); + } + { + brpc::MysqlRequest request(stmt.get()); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_error(), true); + } + } + // two parameter + { + std::stringstream ss; + ss << "select * from brpc_table_" << brpc::MYSQL_table_suffix + << " where col1 = ? and col2 = ?"; + auto stmt(brpc::NewMysqlStatement(channel, ss.str())); + ASSERT_FALSE(stmt == NULL) << "Fail to create statement"; + { + brpc::MysqlRequest request(stmt.get()); + ASSERT_EQ(request.AddParam(1157), true); + ASSERT_EQ(request.AddParam("col2"), true); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_resultset(), true); + } + { + brpc::MysqlRequest request(stmt.get()); + brpc::MysqlResponse response; + brpc::Controller cntl; + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(response.reply(0).is_error(), true); + } + } +} + +TEST_F(MysqlTest, drop_table) { + brpc::ChannelOptions options; + options.protocol = brpc::PROTOCOL_MYSQL; + options.connection_type = brpc::MYSQL_connection_type; + options.connect_timeout_ms = brpc::MYSQL_connect_timeout_ms; + options.timeout_ms = brpc::MYSQL_timeout_ms /*milliseconds*/; + options.auth = new brpc::policy::MysqlAuthenticator( + brpc::MYSQL_user, brpc::MYSQL_password, brpc::MYSQL_schema, "charset=utf8"); + std::stringstream ss; + ss << brpc::MYSQL_host + ":" + brpc::MYSQL_port; + brpc::Channel channel; + ASSERT_EQ(0, channel.Init(ss.str().c_str(), &options)); + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "delete from brpc_table_" << brpc::MYSQL_table_suffix; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } + + { + brpc::MysqlRequest request; + brpc::MysqlResponse response; + brpc::Controller cntl; + std::stringstream ss; + ss << "drop table brpc_table_" << brpc::MYSQL_table_suffix; + request.Query(ss.str()); + channel.CallMethod(NULL, &cntl, &request, &response, NULL); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + ASSERT_EQ(1ul, response.reply_size()); + ASSERT_EQ(brpc::MYSQL_RSP_OK, response.reply(0).type()); + } +} + +} // namespace