From ace3a42717c34e446604a9dd7b41cac7a96a2b53 Mon Sep 17 00:00:00 2001 From: Hua Liu <58683130+liuh-80@users.noreply.github.com> Date: Thu, 23 Jun 2022 08:00:01 +0800 Subject: [PATCH] Re-write sonic-db-cli with c++ for sonic startup performance issue (#607) (#636) Fix sonic-db-cli high CPU usage on SONiC startup issue: https://github.com/Azure/sonic-buildimage/issues/10218 ETA of this issue will be 2022/05/31 Re-write sonic-cli with c++. Add c++ unit test to cover all code. Pass all E2E test scenario. - [ ] 201811 - [ ] 201911 - [ ] 202006 - [ ] 202012 - [ ] 202106 - [ ] 202111 Re-write sonic-cli with c++ for sonic startup performance issue --- Makefile.am | 2 +- common/rediscommand.cpp | 10 + common/rediscommand.h | 1 + common/redisreply.cpp | 43 ++- common/redisreply.h | 4 + configure.ac | 1 + debian/control | 6 + debian/sonic-db-cli.dirs | 1 + debian/sonic-db-cli.install | 1 + sonic-db-cli/Makefile.am | 19 ++ sonic-db-cli/main.cpp | 24 ++ sonic-db-cli/sonic-db-cli.cpp | 330 ++++++++++++++++++++++++ sonic-db-cli/sonic-db-cli.h | 55 ++++ tests/Makefile.am | 3 +- tests/cli_test_data/cli_help_output.txt | 23 ++ tests/cli_test_data/cli_ping_output.txt | 1 + tests/cli_ut.cpp | 286 ++++++++++++++++++++ 17 files changed, 807 insertions(+), 3 deletions(-) create mode 100644 debian/sonic-db-cli.dirs create mode 100644 debian/sonic-db-cli.install create mode 100755 sonic-db-cli/Makefile.am create mode 100755 sonic-db-cli/main.cpp create mode 100755 sonic-db-cli/sonic-db-cli.cpp create mode 100755 sonic-db-cli/sonic-db-cli.h create mode 100755 tests/cli_test_data/cli_help_output.txt create mode 100755 tests/cli_test_data/cli_ping_output.txt create mode 100755 tests/cli_ut.cpp diff --git a/Makefile.am b/Makefile.am index 5140185f8..a77d6b9a4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,3 +1,3 @@ -SUBDIRS = common pyext tests +SUBDIRS = common pyext sonic-db-cli tests ACLOCAL_AMFLAGS = -I m4 diff --git a/common/rediscommand.cpp b/common/rediscommand.cpp index 51fe98b9e..a24f3a6b8 100644 --- a/common/rediscommand.cpp +++ b/common/rediscommand.cpp @@ -49,6 +49,16 @@ void RedisCommand::formatArgv(int argc, const char **argv, const size_t *argvlen } } +void RedisCommand::format(const vector &commands) +{ + vector args; + for (auto& command : commands) + { + args.push_back(command.c_str()); + } + formatArgv(static_cast(args.size()), args.data(), NULL); +} + /* Format HMSET key multiple field value command */ void RedisCommand::formatHMSET(const std::string &key, const std::vector &values) diff --git a/common/rediscommand.h b/common/rediscommand.h index fb96f4584..5ea4c6122 100644 --- a/common/rediscommand.h +++ b/common/rediscommand.h @@ -25,6 +25,7 @@ class RedisCommand { void format(const char *fmt, ...); void formatArgv(int argc, const char **argv, const size_t *argvlen); + void format(const std::vector &commands); /* Format HMSET key multiple field value command */ #ifndef SWIG diff --git a/common/redisreply.cpp b/common/redisreply.cpp index a2ef46e27..ca4ba1315 100644 --- a/common/redisreply.cpp +++ b/common/redisreply.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -158,7 +159,7 @@ void RedisReply::checkReplyType(int expectedType) const char *err = (m_reply->type == REDIS_REPLY_STRING || m_reply->type == REDIS_REPLY_ERROR) ? m_reply->str : "NON-STRING-REPLY"; - string errmsg = "Expected to get redis type " + to_string(expectedType) + " got type " + to_string(m_reply->type) + ", err: " + err; + string errmsg = "Expected to get redis type " + std::to_string(expectedType) + " got type " + std::to_string(m_reply->type) + ", err: " + err; SWSS_LOG_ERROR("%s", errmsg.c_str()); throw system_error(make_error_code(errc::io_error), errmsg); } @@ -225,4 +226,44 @@ template<> RedisMessage RedisReply::getReply() return ret; } + +string RedisReply::to_string() +{ + return to_string(getContext()); +} + +string RedisReply::to_string(redisReply *reply) +{ + switch(reply->type) + { + case REDIS_REPLY_INTEGER: + return std::to_string(reply->integer); + + case REDIS_REPLY_STRING: + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_NIL: + return string(reply->str, reply->len); + + case REDIS_REPLY_ARRAY: + { + stringstream result; + for (size_t i = 0; i < reply->elements; i++) + { + result << to_string(reply->element[i]); + + if (i < reply->elements - 1) + { + result << endl; + } + } + return result.str(); + } + + default: + SWSS_LOG_ERROR("invalid type %d for message", reply->type); + return string(); + } +} + } diff --git a/common/redisreply.h b/common/redisreply.h index b25653293..a8f3458de 100644 --- a/common/redisreply.h +++ b/common/redisreply.h @@ -92,6 +92,10 @@ class RedisReply /* Check that the status is QUEUED, throw exception otherwise */ void checkStatusQueued(); + std::string to_string(); + + static std::string to_string(redisReply *reply); + private: void checkStatus(const char *status); void checkReply(); diff --git a/configure.ac b/configure.ac index ff909b1cc..eda00eaeb 100644 --- a/configure.ac +++ b/configure.ac @@ -98,6 +98,7 @@ AC_CONFIG_FILES([ pyext/Makefile pyext/py2/Makefile pyext/py3/Makefile + sonic-db-cli/Makefile tests/Makefile ]) diff --git a/debian/control b/debian/control index 215c1093d..e7ae8d1d3 100644 --- a/debian/control +++ b/debian/control @@ -35,3 +35,9 @@ Architecture: any Depends: ${shlibs:Depends}, ${misc:Pre-Depends} Section: libs Description: This package contains Switch State Service common Python3 library. + +Package: sonic-db-cli +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Pre-Depends} +Section: libs +Description: This package contains SONiC DB cli. diff --git a/debian/sonic-db-cli.dirs b/debian/sonic-db-cli.dirs new file mode 100644 index 000000000..e77248175 --- /dev/null +++ b/debian/sonic-db-cli.dirs @@ -0,0 +1 @@ +usr/bin diff --git a/debian/sonic-db-cli.install b/debian/sonic-db-cli.install new file mode 100644 index 000000000..6e8e0ec68 --- /dev/null +++ b/debian/sonic-db-cli.install @@ -0,0 +1 @@ +usr/bin/sonic-db-cli diff --git a/sonic-db-cli/Makefile.am b/sonic-db-cli/Makefile.am new file mode 100755 index 000000000..a8b52653a --- /dev/null +++ b/sonic-db-cli/Makefile.am @@ -0,0 +1,19 @@ +INCLUDES = -I $(top_srcdir) + +if DEBUG +DBGFLAGS = -ggdb -DDEBUG +else +DBGFLAGS = -g -DNDEBUG +endif + +lib_LTLIBRARIES = libsonicdbcli.la +libsonicdbcli_la_SOURCES = sonic-db-cli.cpp +libsonicdbcli_la_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) +libsonicdbcli_la_CPPFLAGS = $(DBGFLAGS) $(AM_CPPFLAGS) $(CFLAGS_COMMON) +libsonicdbcli_la_LDFLAGS = -L$(top_srcdir)/common -lswsscommon -lpthread + +bin_PROGRAMS = sonic-db-cli +sonic_db_cli_SOURCES = sonic-db-cli.cpp main.cpp +sonic_db_cli_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) +sonic_db_cli_CPPFLAGS = $(DBGFLAGS) $(AM_CPPFLAGS) $(CFLAGS_COMMON) +sonic_db_cli_LDFLAGS = -L$(top_srcdir)/common -lswsscommon -lpthread diff --git a/sonic-db-cli/main.cpp b/sonic-db-cli/main.cpp new file mode 100755 index 000000000..b0fde5ea5 --- /dev/null +++ b/sonic-db-cli/main.cpp @@ -0,0 +1,24 @@ +#include "sonic-db-cli.h" +#include "common/dbconnector.h" + +using namespace swss; +using namespace std; + +int main(int argc, char** argv) +{ + auto initializeGlobalConfig = []() + { + SonicDBConfig::initializeGlobalConfig(SonicDBConfig::DEFAULT_SONIC_DB_GLOBAL_CONFIG_FILE); + }; + + auto initializeConfig = []() + { + SonicDBConfig::initialize(SonicDBConfig::DEFAULT_SONIC_DB_CONFIG_FILE); + }; + + return sonic_db_cli( + argc, + argv, + initializeGlobalConfig, + initializeConfig); +} \ No newline at end of file diff --git a/sonic-db-cli/sonic-db-cli.cpp b/sonic-db-cli/sonic-db-cli.cpp new file mode 100755 index 000000000..a69502600 --- /dev/null +++ b/sonic-db-cli/sonic-db-cli.cpp @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include "sonic-db-cli.h" + +using namespace swss; +using namespace std; + +void printUsage() +{ + cout << "usage: sonic-db-cli [-h] [-s] [-n NAMESPACE] db_or_op [cmd [cmd ...]]" << endl; + cout << endl; + cout << "SONiC DB CLI:" << endl; + cout << endl; + cout << "positional arguments:" << endl; + cout << " db_or_op Database name Or Unary operation(only PING/SAVE/FLUSHALL supported)" << endl; + cout << " cmd Command to execute in database" << endl; + cout << endl; + cout << "optional arguments:" << endl; + cout << " -h, --help show this help message and exit" << endl; + cout << " -s, --unixsocket Override use of tcp_port and use unixsocket" << endl; + cout << " -n NAMESPACE, --namespace NAMESPACE" << endl; + cout << " Namespace string to use asic0/asic1.../asicn" << endl; + cout << endl; + cout << "**sudo** needed for commands accesing a different namespace [-n], or using unixsocket connection [-s]" << endl; + cout << endl; + cout << "Example 1: sonic-db-cli -n asic0 CONFIG_DB keys \\*" << endl; + cout << "Example 2: sonic-db-cli -n asic2 APPL_DB HGETALL VLAN_TABLE:Vlan10" << endl; + cout << "Example 3: sonic-db-cli APPL_DB HGET VLAN_TABLE:Vlan10 mtu" << endl; + cout << "Example 4: sonic-db-cli -n asic3 APPL_DB EVAL \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\" 2 k1 k2 v1 v2" << endl; + cout << "Example 5: sonic-db-cli PING | sonic-db-cli -s PING" << endl; + cout << "Example 6: sonic-db-cli SAVE | sonic-db-cli -s SAVE" << endl; + cout << "Example 7: sonic-db-cli FLUSHALL | sonic-db-cli -s FLUSHALL" << endl; +} + +string handleSingleOperation( + const string& netns, + const string& db_name, + const string& operation, + bool isTcpConn) +{ + shared_ptr client; + auto host = SonicDBConfig::getDbHostname(db_name, netns); + string message = "Could not connect to Redis at " + host + ":"; + try + { + auto db_id = SonicDBConfig::getDbId(db_name, netns); + if (!isTcpConn && db_name != "redis_chassis.server") + { + auto db_socket = SonicDBConfig::getDbSock(db_name); + message += db_name + ": Connection refused"; + client = make_shared(db_id, db_socket, 0); + } + else + { + auto port = SonicDBConfig::getDbPort(db_name, netns); + message += port + ": Connection refused"; + client = make_shared(db_id, host, port, 0); + } + } + catch (const exception& e) + { + return message; + } + + if (operation == "PING" + || operation == "SAVE" + || operation == "FLUSHALL") + { + RedisReply reply(client.get(), operation); + auto response = reply.getContext(); + if (nullptr != response) + { + return string(); + } + } + else + { + throw std::invalid_argument("Operation " + operation +" is not supported"); + } + + return message; +} + +int handleAllInstances( + const string& netns, + const string& operation, + bool isTcpConn) +{ + auto db_names = SonicDBConfig::getDbList(netns); + // Operate All Redis Instances in Parallel + // TODO: if one of the operations failed, it could fail quickly and not necessary to wait all other operations + list> responses; + for (auto& db_name : db_names) + { + future response = std::async(std::launch::async, handleSingleOperation, netns, db_name, operation, isTcpConn); + responses.push_back(std::move(response)); + } + + bool operation_failed = false; + for (auto& response : responses) + { + auto respstr = response.get(); + if (respstr != "") + { + cout << respstr << endl; + operation_failed = true; + } + } + + if (operation_failed) + { + return 1; + } + + if (operation == "PING") + { + cout << "PONG" << endl; + } + else + { + cout << "OK" << endl; + } + + return 0; +} + +int executeCommands( + const string& db_name, + vector& commands, + const string& netns, + bool isTcpConn) +{ + shared_ptr client = nullptr; + try + { + int db_id = SonicDBConfig::getDbId(db_name, netns); + if (isTcpConn) + { + auto host = SonicDBConfig::getDbHostname(db_name, netns); + auto port = SonicDBConfig::getDbPort(db_name, netns); + client = make_shared(db_id, host, port, 0); + } + else + { + auto db_socket = SonicDBConfig::getDbSock(db_name); + client = make_shared(db_id, db_socket, 0); + } + } + catch (const exception& e) + { + cerr << "Invalid database name input : '" << db_name << "'" << endl; + cerr << e.what() << endl; + return 1; + } + + try + { + RedisCommand command; + command.format(commands); + RedisReply reply(client.get(), command); + /* + sonic-db-cli output format mimic the non-tty mode output format from redis-cli + based on our usage in SONiC, None and list type output from python API needs to be modified + with these changes, it is enough for us to mimic redis-cli in SONiC so far since no application uses tty mode redis-cli output + */ + cout << reply.to_string() << endl; + } + catch (const std::system_error& e) + { + cerr << e.what() << endl; + return 1; + } + + return 0; +} + +void parseCliArguments( + int argc, + char** argv, + Options &options) +{ + // Parse argument with getopt https://man7.org/linux/man-pages/man3/getopt.3.html + const char* short_options = "hsn"; + static struct option long_options[] = { + {"help", optional_argument, NULL, 'h' }, + {"unixsocket", optional_argument, NULL, 's' }, + {"namespace", optional_argument, NULL, 'n' }, + // The last element of the array has to be filled with zeros. + {0, 0, 0, 0 } + }; + + // prevent getopt_long print "invalid option" message. + opterr = 0; + while(optind < argc) + { + int opt = getopt_long(argc, argv, short_options, long_options, NULL); + if (opt != -1) + { + switch (opt) { + case 'h': + options.m_help = true; + break; + + case 's': + options.m_unixsocket = true; + break; + + case 'n': + if (optind < argc) + { + options.m_namespace = argv[optind]; + optind++; + } + else + { + throw invalid_argument("namespace value is missing."); + } + break; + + default: + // argv contains unknown argument + throw invalid_argument("Unknown argument:" + string(argv[optind])); + } + } + else + { + // db_or_op and cmd are non-option arguments + options.m_db_or_op = argv[optind]; + optind++; + + while(optind < argc) + { + auto cmdstr = string(argv[optind]); + options.m_cmd.push_back(cmdstr); + optind++; + } + } + } +} + +int sonic_db_cli( + int argc, + char** argv, + function initializeGlobalConfig, + function initializeConfig) +{ + Options options; + try + { + parseCliArguments(argc, argv, options); + } + catch (invalid_argument const& e) + { + cerr << "Command Line Error: " << e.what() << endl; + printUsage(); + return -1; + } + catch (logic_error const& e) + { + // getopt_long throw logic_error when found a unknown option without value. + cerr << "Unknown option without value: " << e.what() << endl; + printUsage(); + return -1; + } + + if (options.m_help) + { + printUsage(); + return 0; + } + + if (!options.m_db_or_op.empty()) + { + auto dbOrOperation = options.m_db_or_op; + auto netns = options.m_namespace; + bool isTcpConn = !options.m_unixsocket; + // Load the database config for the namespace + if (!netns.empty()) + { + initializeGlobalConfig(); + } + else + { + // SonicDBConfig may initialized when run cli with UT + initializeConfig(); + // Use the tcp connectivity if namespace is local and unixsocket cmd_option is present. + isTcpConn = true; + netns = ""; + } + + if (options.m_cmd.size() != 0) + { + auto commands = options.m_cmd; + return executeCommands(dbOrOperation, commands, netns, isTcpConn); + } + else if (dbOrOperation == "PING" + || dbOrOperation == "SAVE" + || dbOrOperation == "FLUSHALL") + { + // redis-cli doesn't depend on database_config.json which could raise some exceptions + // sonic-db-cli catch all possible exceptions and handle it as a failure case which not return 'OK' or 'PONG' + try + { + return handleAllInstances(netns, dbOrOperation, isTcpConn); + } + catch (const exception& e) + { + cerr << "An exception of type " << e.what() << " occurred. Arguments:" << endl; + for (int idx = 0; idx < argc; idx++) + { + cerr << argv[idx] << " "; + } + cerr << endl; + return 1; + } + } + else + { + printUsage(); + } + } + else + { + printUsage(); + } + + return 0; +} \ No newline at end of file diff --git a/sonic-db-cli/sonic-db-cli.h b/sonic-db-cli/sonic-db-cli.h new file mode 100755 index 000000000..be4fcd3a0 --- /dev/null +++ b/sonic-db-cli/sonic-db-cli.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include "common/dbconnector.h" +#include "common/dbinterface.h" +#include "common/redisreply.h" + +struct Options +{ + bool m_help = false; + bool m_unixsocket = false; + std::string m_namespace; + std::string m_db_or_op; + std::vector m_cmd; +}; + +void printUsage(); + +void printRedisReply(swss::RedisReply& reply); + +std::shared_ptr connectDbInterface( + const std::string& db_name, + const std::string& netns, + bool isTcpConn); + +int executeCommands( + const std::string& db_name, + std::vector& commands, + const std::string& netns, + bool isTcpConn); + +std::string handleSingleOperation( + const std::string& netns, + const std::string& db_name, + const std::string& operation, + bool isTcpConn); + +int handleAllInstances( + const std::string& netns, + const std::string& operation, + bool isTcpConn); + +void parseCliArguments( + int argc, + char** argv, + Options &options); + +int sonic_db_cli( + int argc, + char** argv, + std::function initializeGlobalConfig, + std::function initializeConfig); \ No newline at end of file diff --git a/tests/Makefile.am b/tests/Makefile.am index a84a6df95..437765db0 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -35,8 +35,9 @@ tests_SOURCES = redis_ut.cpp \ boolean_ut.cpp \ status_code_util_test.cpp \ saiaclschema_ut.cpp \ + cli_ut.cpp \ main.cpp tests_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_GTEST) $(LIBNL_CFLAGS) tests_CPPFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_GTEST) $(LIBNL_CFLAGS) -tests_LDADD = $(LDADD_GTEST) -lpthread -L$(top_srcdir)/common -lswsscommon $(LIBNL_LIBS) $(CODE_COVERAGE_LIBS) +tests_LDADD = $(LDADD_GTEST) -lpthread -L$(top_srcdir)/common -lswsscommon $(LIBNL_LIBS) $(CODE_COVERAGE_LIBS) -L$(top_srcdir)/sonic-db-cli -lsonicdbcli diff --git a/tests/cli_test_data/cli_help_output.txt b/tests/cli_test_data/cli_help_output.txt new file mode 100755 index 000000000..1d286df17 --- /dev/null +++ b/tests/cli_test_data/cli_help_output.txt @@ -0,0 +1,23 @@ +usage: sonic-db-cli [-h] [-s] [-n NAMESPACE] db_or_op [cmd [cmd ...]] + +SONiC DB CLI: + +positional arguments: + db_or_op Database name Or Unary operation(only PING/SAVE/FLUSHALL supported) + cmd Command to execute in database + +optional arguments: + -h, --help show this help message and exit + -s, --unixsocket Override use of tcp_port and use unixsocket + -n NAMESPACE, --namespace NAMESPACE + Namespace string to use asic0/asic1.../asicn + +**sudo** needed for commands accesing a different namespace [-n], or using unixsocket connection [-s] + +Example 1: sonic-db-cli -n asic0 CONFIG_DB keys \* +Example 2: sonic-db-cli -n asic2 APPL_DB HGETALL VLAN_TABLE:Vlan10 +Example 3: sonic-db-cli APPL_DB HGET VLAN_TABLE:Vlan10 mtu +Example 4: sonic-db-cli -n asic3 APPL_DB EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2 +Example 5: sonic-db-cli PING | sonic-db-cli -s PING +Example 6: sonic-db-cli SAVE | sonic-db-cli -s SAVE +Example 7: sonic-db-cli FLUSHALL | sonic-db-cli -s FLUSHALL diff --git a/tests/cli_test_data/cli_ping_output.txt b/tests/cli_test_data/cli_ping_output.txt new file mode 100755 index 000000000..87eb7c81d --- /dev/null +++ b/tests/cli_test_data/cli_ping_output.txt @@ -0,0 +1 @@ +PONG diff --git a/tests/cli_ut.cpp b/tests/cli_ut.cpp new file mode 100755 index 000000000..a3516963b --- /dev/null +++ b/tests/cli_ut.cpp @@ -0,0 +1,286 @@ +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include "common/dbconnector.h" +#include "sonic-db-cli/sonic-db-cli.h" + +using namespace swss; +using namespace std; + +const string config_file = "./tests/redis_multi_db_ut_config/database_config.json"; +const string global_config_file = "./tests/redis_multi_db_ut_config/database_global.json"; + +int sonic_db_cli(int argc, char** argv) +{ + auto initializeGlobalConfig = []() + { + if (!SonicDBConfig::isGlobalInit()) + { + SonicDBConfig::initializeGlobalConfig(global_config_file); + } + }; + + auto initializeConfig = []() + { + if (!SonicDBConfig::isInit()) + { + SonicDBConfig::initialize(config_file); + } + }; + + return sonic_db_cli( + argc, + argv, + initializeGlobalConfig, + initializeConfig); +} + +string readFileContent(string file_name) +{ + ifstream help_output_file(file_name); + stringstream buffer; + buffer << help_output_file.rdbuf(); + return buffer.str(); +} + +string runCli(int argc, char** argv) +{ + optind = 0; + testing::internal::CaptureStdout(); + EXPECT_EQ(0, sonic_db_cli(argc, argv)); + auto output = testing::internal::GetCapturedStdout(); + return output; +} + +TEST(sonic_db_cli, test_cli_help) +{ + char *args[2]; + args[0] = "sonic-db-cli"; + args[1] = "-h"; + + auto output = runCli(2, args); + auto expected_output = readFileContent("./tests/cli_test_data/cli_help_output.txt"); + EXPECT_EQ(expected_output, output); +} + +TEST(sonic_db_cli, test_cli_ping_cmd) +{ + char *args[2]; + args[0] = "sonic-db-cli"; + args[1] = "PING"; + + auto output = runCli(2, args); + EXPECT_EQ("PONG\n", output); +} + +TEST(sonic_db_cli, test_cli_save_cmd) +{ + char *args[2]; + args[0] = "sonic-db-cli"; + args[1] = "SAVE"; + + auto output = runCli(2, args); + EXPECT_EQ("OK\n", output); +} + +TEST(sonic_db_cli, test_cli_flush_cmd) +{ + char *args[2]; + args[0] = "sonic-db-cli"; + args[1] = "FLUSHALL"; + + auto output = runCli(2, args); + EXPECT_EQ("OK\n", output); +} + +TEST(sonic_db_cli, test_cli_run_cmd) +{ + char *args[5]; + args[0] = "sonic-db-cli"; + args[1] = "TEST_DB"; + + // set key to test DB + args[2] = "SET"; + args[3] = "testkey"; + args[4] = "testvalue"; + auto output = runCli(5, args); + EXPECT_EQ("OK\n", output); + + // get key from test db + args[2] = "GET"; + args[3] = "testkey"; + output = runCli(4, args); + EXPECT_EQ("testvalue\n", output); + + // get keys from test db + args[2] = "keys"; + args[3] = "*"; + output = runCli(4, args); + EXPECT_EQ("testkey\n", output); +} + +TEST(sonic_db_cli, test_cli_multi_ns_cmd) +{ + char *args[7]; + args[0] = "sonic-db-cli"; + args[1] = "-n"; + args[2] = "asic0"; + args[3] = "TEST_DB"; + + // set key to test DB + args[4] = "SET"; + args[5] = "testkey"; + args[6] = "testvalue"; + auto output = runCli(7, args); + EXPECT_EQ("OK\n", output); + + // get key from test db + args[4] = "GET"; + args[5] = "testkey"; + output = runCli(6, args); + EXPECT_EQ("testvalue\n", output); + + // get keys from test db + args[4] = "keys"; + args[5] = "*"; + output = runCli(6, args); + EXPECT_EQ("testkey\n", output); +} + +TEST(sonic_db_cli, test_cli_unix_socket_cmd) +{ + char *args[8]; + args[0] = "sonic-db-cli"; + args[1] = "-s"; + args[2] = "-n"; + args[3] = "asic0"; + args[4] = "TEST_DB"; + + // set key to test DB + args[5] = "SET"; + args[6] = "testkey"; + args[7] = "testvalue"; + auto output = runCli(8, args); + EXPECT_EQ("OK\n", output); + + // get key from test db + args[5] = "GET"; + args[6] = "testkey"; + output = runCli(7, args); + EXPECT_EQ("testvalue\n", output); +} + +TEST(sonic_db_cli, test_cli_eval_cmd) +{ + char *args[11]; + args[0] = "sonic-db-cli"; + args[1] = "-n"; + args[2] = "asic0"; + args[3] = "TEST_DB"; + + // run eval command: EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2 + args[4] = "EVAL"; + args[5] = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"; + args[6] = "2"; + args[7] = "k1"; + args[8] = "k2"; + args[9] = "v1"; + args[10] = "v2"; + auto output = runCli(11, args); + EXPECT_EQ("k1\nk2\nv1\nv2\n", output); +} + +void flushDB(char* ns, char* database) +{ + char *args[5]; + args[0] = "sonic-db-cli"; + args[1] = "-n"; + args[2] = ns; + args[3] = database; + args[4] = "FLUSHDB"; + optind = 0; + sonic_db_cli(5, args); +} + +void generateTestData(char* ns, char* database) +{ + flushDB(ns, database); + char *args[7]; + args[0] = "sonic-db-cli"; + args[1] = "-n"; + args[2] = ns; + args[3] = database; + + args[4] = "EVAL"; + args[5] = "local i=0 while (i<100000) do i=i+1 redis.call('SET', i, i) end"; + args[6] = "0"; + optind = 0; + sonic_db_cli(7, args); +} + +TEST(sonic_db_cli, test_parallel_cmd) { + char *args[7]; + args[0] = "sonic-db-cli"; + args[1] = "-n"; + args[2] = "asic0"; + args[4] = "SAVE"; + + auto db_names = swss::SonicDBConfig::getDbList("asic0"); + for (auto& db_name : db_names) + { + generateTestData("asic0", const_cast(db_name.c_str())); + } + db_names = swss::SonicDBConfig::getDbList("asic1"); + for (auto& db_name : db_names) + { + generateTestData("asic1", const_cast(db_name.c_str())); + } + + // save 2 DBs and get save DB performance + auto begin_time = clock(); + db_names = swss::SonicDBConfig::getDbList("asic0"); + args[2] = "asic0"; + for (auto& db_name : db_names) + { + args[3] = const_cast(db_name.c_str()); + optind = 0; + sonic_db_cli(5, args); + } + + db_names = swss::SonicDBConfig::getDbList("asic1"); + args[2] = "asic1"; + for (auto& db_name : db_names) + { + args[3] = const_cast(db_name.c_str()); + optind = 0; + sonic_db_cli(5, args); + } + + auto sequential_time = float( clock () - begin_time ); + + // prepare data + db_names = swss::SonicDBConfig::getDbList("asic0"); + for (auto& db_name : db_names) + { + generateTestData("asic0", const_cast(db_name.c_str())); + } + db_names = swss::SonicDBConfig::getDbList("asic1"); + for (auto& db_name : db_names) + { + generateTestData("asic0", const_cast(db_name.c_str())); + } + + // save 2 DBs in parallel, and get save DB performance + begin_time = clock(); + + args[1] = "SAVE"; + optind = 0; + sonic_db_cli(2, args); + + auto parallen_time = float( clock () - begin_time ); + EXPECT_TRUE(parallen_time < sequential_time); +} \ No newline at end of file