MXS-1777 Refactor selection.
This commit refactors slave selection. The compare is still pair-wise but isolated into a small run_comparison() function. The function get_slave_candidate() is used when new connections are created, which I both moved and modified (had to move due to scoping), so diff is off. The slave selection for routing: get_slave_backend() now has the filtering logic from old get_slave_backend() and compare_backends(), the latter of which is removed. Backend functions mostly take shared_ptr<SRWBackend> in various forms (as is, const ref, in a container). Ideally the shared_ptr would be used only to where it is really needed, and either naked ptrs or references to RWBackend would be used. This refactor does not address that issue, but compounds it by using even deeper shared_ptr structures. Fixing that in a future commit.
This commit is contained in:
@ -20,6 +20,7 @@
|
||||
|
||||
#include <maxscale/cdefs.h>
|
||||
#include <maxbase/jansson.h>
|
||||
#include <maxbase/average.hh>
|
||||
#include <maxscale/config.h>
|
||||
#include <maxscale/dcb.h>
|
||||
|
||||
@ -131,7 +132,8 @@ typedef struct server
|
||||
long persistmaxtime; /**< Maximum number of seconds connection can live */
|
||||
bool proxy_protocol; /**< Send proxy-protocol header to backends when connecting
|
||||
* routing sessions. */
|
||||
SERVER_PARAM *parameters; /**< Additional custom parameters which may affect routing decisions. */
|
||||
SERVER_PARAM* parameters; /**< Additional custom parameters which may affect routing
|
||||
* decisions. */
|
||||
// Base variables
|
||||
SPINLOCK lock; /**< Access lock. Required when modifying server status or settings. */
|
||||
bool is_active; /**< Server is active and has not been "destroyed" */
|
||||
@ -146,16 +148,21 @@ typedef struct server
|
||||
int64_t triggered_at; /**< Time when the last event was triggered */
|
||||
// Status descriptors. Updated automatically by a monitor or manually by the admin
|
||||
uint64_t status; /**< Current status flag bitmap */
|
||||
int maint_request; /**< Is admin requesting Maintenance=ON/OFF on the server? */
|
||||
int maint_request; /**< Is admin requesting Maintenance=ON/OFF on the
|
||||
* server? */
|
||||
char version_string[MAX_SERVER_VERSION_LEN]; /**< Server version string as given by backend */
|
||||
uint64_t version; /**< Server version numeric representation */
|
||||
server_type_t server_type; /**< Server type (MariaDB or MySQL), deduced from version string */
|
||||
long node_id; /**< Node id, server_id for M/S or local_index for Galera */
|
||||
int rlag; /**< Replication Lag for Master/Slave replication */
|
||||
server_type_t server_type; /**< Server type (MariaDB or MySQL), deduced from
|
||||
* version string */
|
||||
long node_id; /**< Node id, server_id for M/S or local_index for
|
||||
* Galera */
|
||||
int rlag; /**< Replication Lag for Master/Slave replication
|
||||
* */
|
||||
unsigned long node_ts; /**< Last timestamp set from M/S monitor module */
|
||||
long master_id; /**< Master server id of this node */
|
||||
// Misc fields
|
||||
bool master_err_is_logged; /**< If node failed, this indicates whether it is logged. Only used
|
||||
bool master_err_is_logged; /**< If node failed, this indicates whether it is logged. Only
|
||||
* used
|
||||
* by rwsplit. TODO: Move to rwsplit */
|
||||
bool warn_ssl_not_enabled;/**< SSL not used for an SSL enabled server */
|
||||
MxsDiskSpaceThreshold* disk_space_threshold;/**< Disk space thresholds */
|
||||
@ -185,14 +192,37 @@ typedef struct server
|
||||
// Bits providing general information
|
||||
#define SERVER_DISK_SPACE_EXHAUSTED (1 << 31) /**<< The disk space of the server is exhausted */
|
||||
|
||||
#define STRSRVSTATUS(s) (server_is_master(s) ? "RUNNING MASTER" : \
|
||||
(server_is_slave(s) ? "RUNNING SLAVE" : \
|
||||
(server_is_joined(s) ? "RUNNING JOINED" : \
|
||||
(server_is_ndb(s) ? "RUNNING NDB" : \
|
||||
((server_is_running(s) && server_is_in_maint(s)) ? "RUNNING MAINTENANCE" : \
|
||||
(server_is_relay(s) ? "RUNNING RELAY" : \
|
||||
(server_is_usable(s) ? "RUNNING (only)" : \
|
||||
(server_is_down(s) ? "DOWN" : "UNKNOWN STATUS"))))))))
|
||||
#define STRSRVSTATUS(s) \
|
||||
(server_is_master(s) ? "RUNNING MASTER" \
|
||||
: (server_is_slave(s) ? "RUNNING SLAVE" \
|
||||
: (server_is_joined(s) ? "RUNNING JOINED" \
|
||||
: (server_is_ndb(s) ? \
|
||||
"RUNNING NDB" \
|
||||
: (( \
|
||||
server_is_running( \
|
||||
s) \
|
||||
&& \
|
||||
server_is_in_maint( \
|
||||
s)) \
|
||||
? \
|
||||
"RUNNING MAINTENANCE" \
|
||||
: ( \
|
||||
server_is_relay( \
|
||||
s) \
|
||||
? \
|
||||
"RUNNING RELAY" \
|
||||
: ( \
|
||||
server_is_usable( \
|
||||
s) \
|
||||
? \
|
||||
"RUNNING (only)" \
|
||||
: ( \
|
||||
server_is_down( \
|
||||
s) \
|
||||
? \
|
||||
"DOWN" \
|
||||
: \
|
||||
"UNKNOWN STATUS"))))))))
|
||||
|
||||
/**
|
||||
* Is the server valid and active?
|
||||
@ -223,7 +253,7 @@ inline bool server_is_usable(const SERVER* server)
|
||||
|
||||
inline bool status_is_running(uint64_t status)
|
||||
{
|
||||
return (status & SERVER_RUNNING);
|
||||
return status & SERVER_RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -303,8 +333,8 @@ inline bool server_is_slave(const SERVER* server)
|
||||
|
||||
inline bool status_is_relay(uint64_t status)
|
||||
{
|
||||
return (status & (SERVER_RUNNING | SERVER_RELAY | SERVER_MAINT)) == \
|
||||
(SERVER_RUNNING | SERVER_RELAY);
|
||||
return (status & (SERVER_RUNNING | SERVER_RELAY | SERVER_MAINT)) \
|
||||
== (SERVER_RUNNING | SERVER_RELAY);
|
||||
}
|
||||
|
||||
inline bool server_is_relay(const SERVER* server)
|
||||
@ -314,8 +344,8 @@ inline bool server_is_relay(const SERVER* server)
|
||||
|
||||
inline bool status_is_joined(uint64_t status)
|
||||
{
|
||||
return (status & (SERVER_RUNNING | SERVER_JOINED | SERVER_MAINT)) ==
|
||||
(SERVER_RUNNING | SERVER_JOINED);
|
||||
return (status & (SERVER_RUNNING | SERVER_JOINED | SERVER_MAINT))
|
||||
== (SERVER_RUNNING | SERVER_JOINED);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -341,14 +371,14 @@ inline bool server_is_ndb(const SERVER* server)
|
||||
|
||||
inline bool server_is_in_cluster(const SERVER* server)
|
||||
{
|
||||
return ((server->status &
|
||||
(SERVER_MASTER | SERVER_SLAVE | SERVER_RELAY | SERVER_JOINED | SERVER_NDB)) != 0);
|
||||
return (server->status
|
||||
& (SERVER_MASTER | SERVER_SLAVE | SERVER_RELAY | SERVER_JOINED | SERVER_NDB)) != 0;
|
||||
}
|
||||
|
||||
inline bool status_is_slave_of_ext_master(uint64_t status)
|
||||
{
|
||||
return ((status & (SERVER_RUNNING | SERVER_SLAVE_OF_EXT_MASTER)) ==
|
||||
(SERVER_RUNNING | SERVER_SLAVE_OF_EXT_MASTER));
|
||||
return (status & (SERVER_RUNNING | SERVER_SLAVE_OF_EXT_MASTER))
|
||||
== (SERVER_RUNNING | SERVER_SLAVE_OF_EXT_MASTER);
|
||||
}
|
||||
|
||||
inline bool server_is_slave_of_ext_master(const SERVER* server)
|
||||
@ -358,7 +388,7 @@ inline bool server_is_slave_of_ext_master(const SERVER* server)
|
||||
|
||||
inline bool status_is_disk_space_exhausted(uint64_t status)
|
||||
{
|
||||
return (status & SERVER_DISK_SPACE_EXHAUSTED);
|
||||
return status & SERVER_DISK_SPACE_EXHAUSTED;
|
||||
}
|
||||
|
||||
inline bool server_is_disk_space_exhausted(const SERVER* server)
|
||||
@ -392,6 +422,15 @@ extern SERVER* server_alloc(const char *name, MXS_CONFIG_PARAMETER* params);
|
||||
*/
|
||||
bool server_serialize(const SERVER* server);
|
||||
|
||||
/**
|
||||
* @brief Add a server parameter
|
||||
*
|
||||
* @param server Server where the parameter is added
|
||||
* @param name Parameter name
|
||||
* @param value Parameter value
|
||||
*/
|
||||
void server_add_parameter(SERVER* server, const char* name, const char* value);
|
||||
|
||||
/**
|
||||
* @brief Remove a server parameter
|
||||
*
|
||||
@ -469,7 +508,10 @@ extern void server_transfer_status(SERVER *dest_server, const SERVER *source_ser
|
||||
extern void server_add_mon_user(SERVER* server, const char* user, const char* passwd);
|
||||
extern size_t server_get_parameter(const SERVER* server, const char* name, char* out, size_t size);
|
||||
extern void server_update_credentials(SERVER* server, const char* user, const char* passwd);
|
||||
extern DCB* server_get_persistent(SERVER *server, const char *user, const char* ip, const char *protocol,
|
||||
extern DCB* server_get_persistent(SERVER* server,
|
||||
const char* user,
|
||||
const char* ip,
|
||||
const char* protocol,
|
||||
int id);
|
||||
extern void server_update_address(SERVER* server, const char* address);
|
||||
extern void server_update_port(SERVER* server, unsigned short port);
|
||||
|
||||
@ -382,6 +382,25 @@ mxs::SRWBackend get_root_master(const mxs::SRWBackendList& backends);
|
||||
*/
|
||||
std::pair<int, int> get_slave_counts(mxs::SRWBackendList& backends, mxs::SRWBackend& master);
|
||||
|
||||
|
||||
/* TODO, hopefully temporary */
|
||||
using BackendSPtrVec = std::vector<mxs::SRWBackend*>;
|
||||
|
||||
/**
|
||||
* Find the best backend based on categorizing the servers, and then applying
|
||||
* selection criteria to the best category.
|
||||
*
|
||||
* @param backends: vector of SRWBackend
|
||||
* @param sc: which select_criteria_t to use
|
||||
* @param master_accept_reads: NOTE: even if this is false, in some cases a master can
|
||||
* still be selected for reads.
|
||||
*
|
||||
* @return Valid iterator into argument backends, or end(backends) if empty
|
||||
*/
|
||||
BackendSPtrVec::const_iterator find_best_backend(const BackendSPtrVec& backends,
|
||||
select_criteria_t sc,
|
||||
bool masters_accept_reads);
|
||||
|
||||
/*
|
||||
* The following are implemented in rwsplit_tmp_table_multi.c
|
||||
*/
|
||||
|
||||
@ -49,7 +49,7 @@ void ResponseStat::query_ended()
|
||||
|
||||
if (++m_sample_count == m_num_filter_samples)
|
||||
{
|
||||
std::sort(begin(m_samples), end(m_samples));
|
||||
std::sort(m_samples.begin(), m_samples.end());
|
||||
maxbase::Duration new_sample = m_samples[m_num_filter_samples / 2];
|
||||
m_average.add(std::chrono::duration<double>(new_sample).count());
|
||||
m_sample_count = 0;
|
||||
|
||||
@ -12,8 +12,7 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <maxscale/cppdefs.hh>
|
||||
|
||||
#include <maxscale/ccdefs.hh>
|
||||
#include <maxbase/stopwatch.hh>
|
||||
#include <maxbase/average.hh>
|
||||
|
||||
@ -22,7 +21,6 @@
|
||||
*/
|
||||
namespace maxscale
|
||||
{
|
||||
|
||||
/**
|
||||
* Query response statistics. Uses median of N samples to filter noise, then
|
||||
* uses those medians to calculate the average response time.
|
||||
@ -39,7 +37,8 @@ public:
|
||||
* @param num_synch_samples - this many medians before the average should be synced, or
|
||||
* @param sync_duration - this much time between syncs.
|
||||
*/
|
||||
ResponseStat(int ignore_first_n = 5, int num_filter_samples = 3,
|
||||
ResponseStat(int ignore_first_n = 5,
|
||||
int num_filter_samples = 3,
|
||||
maxbase::Duration sync_duration = std::chrono::seconds(5));
|
||||
void query_started();
|
||||
void query_ended();// ok to call without a query_started
|
||||
|
||||
@ -38,47 +38,6 @@ using namespace maxscale;
|
||||
|
||||
extern int (*criteria_cmpfun[LAST_CRITERIA])(const SRWBackend&, const SRWBackend&);
|
||||
|
||||
/**
|
||||
* Find out which of the two backend servers has smaller value for select
|
||||
* criteria property.
|
||||
*
|
||||
* @param cand previously selected candidate
|
||||
* @param new challenger
|
||||
* @param sc select criteria
|
||||
*
|
||||
* @return pointer to backend reference of that backend server which has smaller
|
||||
* value in selection criteria. If either reference pointer is NULL then the
|
||||
* other reference pointer value is returned.
|
||||
*/
|
||||
static SRWBackend compare_backends(SRWBackend a, SRWBackend b, select_criteria_t sc)
|
||||
{
|
||||
int (*p)(const SRWBackend&, const SRWBackend&) = criteria_cmpfun[sc];
|
||||
|
||||
if (!a)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
else if (!b)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
// Prefer servers that are not busy executing session commands
|
||||
bool a_busy = a->in_use() && a->has_session_commands();
|
||||
bool b_busy = b->in_use() && b->has_session_commands();
|
||||
|
||||
if (a_busy && !b_busy)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
else if (!a_busy && b_busy)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
return p(a, b) <= 0 ? a : b;
|
||||
}
|
||||
|
||||
void RWSplitSession::handle_connection_keepalive(SRWBackend& target)
|
||||
{
|
||||
mxb_assert(target);
|
||||
@ -582,45 +541,36 @@ SRWBackend RWSplitSession::get_hinted_backend(char *name)
|
||||
|
||||
SRWBackend RWSplitSession::get_slave_backend(int max_rlag)
|
||||
{
|
||||
SRWBackend rval;
|
||||
// create a list of useable backends (includes masters, function name is a bit off),
|
||||
// then feed that list to compare.
|
||||
BackendSPtrVec candidates;
|
||||
auto counts = get_slave_counts(m_backends, m_current_master);
|
||||
|
||||
for (auto it = m_backends.begin(); it != m_backends.end(); it++)
|
||||
for (auto& backend : m_backends)
|
||||
{
|
||||
auto& backend = *it;
|
||||
bool can_take_slave_into_use = backend->is_slave()
|
||||
&& !backend->in_use()
|
||||
&& can_recover_servers()
|
||||
&& backend->can_connect()
|
||||
&& counts.second < m_router->max_slave_count();
|
||||
|
||||
if ((backend->is_master() || backend->is_slave()) && // Either a master or a slave
|
||||
rpl_lag_is_ok(backend, max_rlag)) // Not lagging too much
|
||||
bool master_or_slave = backend->is_master() || backend->is_slave();
|
||||
bool is_useable = backend->in_use() || can_take_slave_into_use;
|
||||
bool not_a_slacker = rpl_lag_is_ok(backend, max_rlag);
|
||||
|
||||
bool server_is_candidate = master_or_slave && is_useable && not_a_slacker;
|
||||
|
||||
if (server_is_candidate)
|
||||
{
|
||||
if (backend->in_use() || (can_recover_servers() && backend->can_connect()))
|
||||
{
|
||||
if (!rval)
|
||||
{
|
||||
// No previous candidate, accept any valid server (includes master)
|
||||
if ((backend->is_master() && backend == m_current_master) ||
|
||||
backend->is_slave())
|
||||
{
|
||||
rval = backend;
|
||||
}
|
||||
}
|
||||
else if (backend->in_use() || counts.second < m_router->max_slave_count())
|
||||
{
|
||||
if (!m_config.master_accept_reads && rval->is_master())
|
||||
{
|
||||
// Pick slaves over masters with master_accept_reads=false
|
||||
rval = backend;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Compare the two servers and pick the best one
|
||||
rval = compare_backends(rval, backend, m_config.slave_selection_criteria);
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.push_back(&backend);
|
||||
}
|
||||
}
|
||||
|
||||
return rval;
|
||||
BackendSPtrVec::const_iterator rval = find_best_backend(candidates,
|
||||
m_config.slave_selection_criteria,
|
||||
m_config.master_accept_reads);
|
||||
|
||||
return (rval == candidates.end()) ? SRWBackend() : **rval;
|
||||
}
|
||||
|
||||
SRWBackend RWSplitSession::get_master_backend()
|
||||
|
||||
@ -45,48 +45,6 @@ static bool valid_for_slave(const SRWBackend& backend, const SRWBackend& master)
|
||||
(!master || backend != master);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the best slave candidate
|
||||
*
|
||||
* This function iterates through @c backend and tries to find the best backend
|
||||
* reference that is not in use. @c cmpfun will be called to compare the backends.
|
||||
*
|
||||
* @param rses Router client session
|
||||
* @param master The master server
|
||||
* @param cmpfun qsort() compatible comparison function
|
||||
*
|
||||
* @return The best slave backend reference or NULL if no candidates could be found
|
||||
*/
|
||||
static SRWBackend get_slave_candidate(const SRWBackendList& backends, const SRWBackend& master,
|
||||
int (*cmpfun)(const SRWBackend&, const SRWBackend&))
|
||||
{
|
||||
SRWBackend candidate;
|
||||
|
||||
for (SRWBackendList::const_iterator it = backends.begin();
|
||||
it != backends.end(); it++)
|
||||
{
|
||||
const SRWBackend& backend = *it;
|
||||
|
||||
if (!backend->in_use() && backend->can_connect() &&
|
||||
valid_for_slave(backend, master))
|
||||
{
|
||||
if (candidate)
|
||||
{
|
||||
if (cmpfun(candidate, backend) > 0)
|
||||
{
|
||||
candidate = backend;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
candidate = backend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
/** Compare number of connections from this router in backend servers */
|
||||
static int backend_cmp_router_conn(const SRWBackend& a, const SRWBackend& b)
|
||||
{
|
||||
@ -223,6 +181,104 @@ int (*criteria_cmpfun[LAST_CRITERIA])(const SRWBackend&, const SRWBackend&) =
|
||||
backend_cmp_response_time
|
||||
};
|
||||
|
||||
// This is still the current compare method. The response-time compare, along with anything
|
||||
// using weights, have to change to use the whole array at once to be correct. Id est, everything
|
||||
// will change to use the whole array in the next iteration.
|
||||
static BackendSPtrVec::const_iterator run_comparison(const BackendSPtrVec& candidates,
|
||||
select_criteria_t sc)
|
||||
{
|
||||
if (candidates.empty()) return candidates.end();
|
||||
|
||||
auto best = candidates.begin();
|
||||
|
||||
for (auto rival = std::next(best);
|
||||
rival != candidates.end();
|
||||
rival = std::next(rival))
|
||||
{
|
||||
if (criteria_cmpfun[sc](**best, **rival) > 0)
|
||||
{
|
||||
best = rival;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Find the best slave candidate for a new connection.
|
||||
*
|
||||
* @param bends backends
|
||||
* @param master the master server
|
||||
* @param sc which select_criteria_t to use
|
||||
*
|
||||
* @return The best slave backend reference or null if no candidates could be found
|
||||
*/
|
||||
static SRWBackend get_slave_candidate(const SRWBackendList& bends,
|
||||
const SRWBackend& master,
|
||||
select_criteria_t sc)
|
||||
{
|
||||
// TODO, nantti, see if this and get_slave_backend can be combined to a single function
|
||||
BackendSPtrVec backends;
|
||||
for (auto& b : bends) // match intefaces. TODO, should go away in the future.
|
||||
{
|
||||
backends.push_back(const_cast<SRWBackend*>(&b));
|
||||
}
|
||||
BackendSPtrVec candidates;
|
||||
|
||||
for (auto& backend : backends)
|
||||
{
|
||||
if (!(*backend)->in_use()
|
||||
&& (*backend)->can_connect()
|
||||
&& valid_for_slave(*backend, master))
|
||||
{
|
||||
candidates.push_back(backend);
|
||||
}
|
||||
}
|
||||
|
||||
return !candidates.empty() ? **run_comparison(candidates, sc) : SRWBackend();
|
||||
|
||||
}
|
||||
|
||||
BackendSPtrVec::const_iterator find_best_backend(const BackendSPtrVec& backends,
|
||||
select_criteria_t sc,
|
||||
bool masters_accept_reads)
|
||||
{
|
||||
// Divide backends to priorities. The set of highest priority backends will then compete.
|
||||
std::map<int, BackendSPtrVec> priority_map;;
|
||||
int best_priority {INT_MAX}; // low numbers are high priority
|
||||
|
||||
for (auto& pSBackend : backends)
|
||||
{
|
||||
auto& backend = **pSBackend;
|
||||
bool is_busy = backend.in_use() && backend.has_session_commands();
|
||||
bool acts_slave = backend.is_slave() || (backend.is_master() && masters_accept_reads);
|
||||
|
||||
int priority;
|
||||
if (acts_slave)
|
||||
{
|
||||
if (!is_busy)
|
||||
{
|
||||
priority = 1; // highest priority, idle servers
|
||||
}
|
||||
else
|
||||
{
|
||||
priority = 13; // lowest priority, busy servers
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
priority = 2; // idle masters with masters_accept_reads==false
|
||||
}
|
||||
|
||||
priority_map[priority].push_back(pSBackend);
|
||||
best_priority = std::min(best_priority, priority);
|
||||
}
|
||||
|
||||
auto best = run_comparison(priority_map[best_priority], sc);
|
||||
|
||||
return std::find(backends.begin(), backends.end(), *best);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Log server connections
|
||||
*
|
||||
@ -351,10 +407,7 @@ bool RWSplit::select_connect_backend_servers(MXS_SESSION *session,
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Check slave selection criteria and set compare function */
|
||||
select_criteria_t select_criteria = cnf.slave_selection_criteria;
|
||||
auto cmpfun = criteria_cmpfun[select_criteria];
|
||||
mxb_assert(cmpfun);
|
||||
auto select_criteria = cnf.slave_selection_criteria;
|
||||
|
||||
if (mxs_log_is_priority_enabled(LOG_INFO))
|
||||
{
|
||||
@ -389,9 +442,9 @@ bool RWSplit::select_connect_backend_servers(MXS_SESSION *session,
|
||||
if (slaves_connected < max_nslaves)
|
||||
{
|
||||
/** Connect to all possible slaves */
|
||||
for (SRWBackend backend(get_slave_candidate(backends, master, cmpfun));
|
||||
for (SRWBackend backend(get_slave_candidate(backends, master, select_criteria));
|
||||
backend && slaves_connected < max_nslaves;
|
||||
backend = get_slave_candidate(backends, master, cmpfun))
|
||||
backend = get_slave_candidate(backends, master, select_criteria))
|
||||
{
|
||||
if (backend->can_connect() && backend->connect(session, sescmd_list))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user