enh(MongoDB): Introduce ReplicaSetURI for handling custom URI.

This commit is contained in:
Matej Kenda
2025-12-08 20:05:27 +01:00
parent bd123a2d07
commit e159c739dd
6 changed files with 1005 additions and 159 deletions

View File

@@ -22,6 +22,7 @@
#include "Poco/MongoDB/Connection.h"
#include "Poco/MongoDB/ReadPreference.h"
#include "Poco/MongoDB/TopologyDescription.h"
#include "Poco/MongoDB/ReplicaSetURI.h"
#include "Poco/Net/SocketAddress.h"
#include <vector>
#include <string>
@@ -135,7 +136,7 @@ public:
/// Throws Poco::IOException if initial discovery fails.
explicit ReplicaSet(const std::string& uri);
/// Creates a ReplicaSet from a MongoDB URI.
/// Creates a ReplicaSet from a MongoDB URI string.
/// Format: mongodb://host1:port1,host2:port2,...?options
///
/// Supported URI options:
@@ -152,6 +153,28 @@ public:
/// Throws Poco::SyntaxException if URI is invalid.
/// Throws Poco::UnknownURISchemeException if scheme is not "mongodb".
explicit ReplicaSet(const ReplicaSetURI& uri);
/// Creates a ReplicaSet from a ReplicaSetURI object.
/// This allows for programmatic URI construction and modification before
/// creating the replica set connection.
///
/// The ReplicaSetURI stores servers as strings without DNS resolution.
/// This constructor resolves the server strings to SocketAddress objects.
/// Servers that cannot be resolved are skipped and will be marked as
/// unavailable during topology discovery.
///
/// Example:
/// ReplicaSetURI uri;
/// uri.addServer("host1:27017");
/// uri.addServer("host2:27017");
/// uri.setReplicaSet("rs0");
/// uri.setReadPreference("primaryPreferred");
/// ReplicaSet rs(uri);
///
/// Throws Poco::InvalidArgumentException if the URI contains no servers
/// or if no servers can be resolved.
/// Throws Poco::IOException if initial discovery fails.
virtual ~ReplicaSet();
/// Destroys the ReplicaSet and stops background monitoring.

View File

@@ -0,0 +1,186 @@
//
// ReplicaSetURI.h
//
// Library: MongoDB
// Package: MongoDB
// Module: ReplicaSetURI
//
// Definition of the ReplicaSetURI class.
//
// Copyright (c) 2025, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier: BSL-1.0
//
#ifndef MongoDB_ReplicaSetURI_INCLUDED
#define MongoDB_ReplicaSetURI_INCLUDED
#include "Poco/MongoDB/MongoDB.h"
#include "Poco/MongoDB/ReadPreference.h"
#include "Poco/URI.h"
#include <vector>
#include <string>
namespace Poco {
namespace MongoDB {
class MongoDB_API ReplicaSetURI
/// Class for parsing and generating MongoDB replica set URIs.
///
/// This class handles parsing of MongoDB connection strings in the format:
/// mongodb://[username:password@]host1[:port1][,host2[:port2],...][/[database][?options]]
///
/// It also provides functionality to:
/// - Access and modify the list of servers
/// - Access and modify configuration options
/// - Generate a URI string from the current state
///
/// Usage example:
/// ReplicaSetURI uri("mongodb://host1:27017,host2:27017/?replicaSet=rs0");
///
/// // Access parsed data
/// std::vector<Net::SocketAddress> servers = uri.servers();
/// std::string setName = uri.replicaSet();
///
/// // Modify and regenerate
/// uri.addServer(Net::SocketAddress("host3:27017"));
/// uri.setReadPreference("secondaryPreferred");
/// std::string newUri = uri.toString();
{
public:
ReplicaSetURI();
/// Creates an empty ReplicaSetURI.
explicit ReplicaSetURI(const std::string& uri);
/// Creates a ReplicaSetURI by parsing the given MongoDB connection string.
///
/// Throws Poco::SyntaxException if the URI format is invalid.
/// Throws Poco::UnknownURISchemeException if the scheme is not "mongodb".
~ReplicaSetURI();
/// Destroys the ReplicaSetURI.
// Server management
[[nodiscard]] const std::vector<std::string>& servers() const;
/// Returns the list of server addresses as strings (host:port format).
/// Servers are NOT resolved - they remain as strings exactly as provided in the URI.
void setServers(const std::vector<std::string>& servers);
/// Sets the list of server addresses as strings (host:port format).
void addServer(const std::string& server);
/// Adds a server to the list as a string (host:port format).
void clearServers();
/// Clears the list of servers.
// Configuration options
[[nodiscard]] std::string replicaSet() const;
/// Returns the replica set name, or empty string if not set.
void setReplicaSet(const std::string& name);
/// Sets the replica set name.
[[nodiscard]] ReadPreference readPreference() const;
/// Returns the read preference.
void setReadPreference(const ReadPreference& pref);
/// Sets the read preference.
void setReadPreference(const std::string& mode);
/// Sets the read preference from a string mode.
/// Valid modes: primary, primaryPreferred, secondary, secondaryPreferred, nearest
[[nodiscard]] unsigned int connectTimeoutMS() const;
/// Returns the connection timeout in milliseconds.
void setConnectTimeoutMS(unsigned int timeoutMS);
/// Sets the connection timeout in milliseconds.
[[nodiscard]] unsigned int socketTimeoutMS() const;
/// Returns the socket timeout in milliseconds.
void setSocketTimeoutMS(unsigned int timeoutMS);
/// Sets the socket timeout in milliseconds.
[[nodiscard]] unsigned int heartbeatFrequency() const;
/// Returns the heartbeat frequency in seconds.
void setHeartbeatFrequency(unsigned int seconds);
/// Sets the heartbeat frequency in seconds.
[[nodiscard]] unsigned int reconnectRetries() const;
/// Returns the number of reconnection retries.
void setReconnectRetries(unsigned int retries);
/// Sets the number of reconnection retries.
[[nodiscard]] unsigned int reconnectDelay() const;
/// Returns the reconnection delay in seconds.
void setReconnectDelay(unsigned int seconds);
/// Sets the reconnection delay in seconds.
[[nodiscard]] std::string database() const;
/// Returns the database name from the URI path, or empty string if not set.
void setDatabase(const std::string& database);
/// Sets the database name.
[[nodiscard]] std::string username() const;
/// Returns the username, or empty string if not set.
void setUsername(const std::string& username);
/// Sets the username.
[[nodiscard]] std::string password() const;
/// Returns the password, or empty string if not set.
void setPassword(const std::string& password);
/// Sets the password.
// URI generation
[[nodiscard]] std::string toString() const;
/// Generates a MongoDB connection string from the current configuration.
/// Format: mongodb://[username:password@]host1:port1[,host2:port2,...][/database][?options]
// Parsing
void parse(const std::string& uri);
/// Parses a MongoDB connection string and updates the configuration.
///
/// Throws Poco::SyntaxException if the URI format is invalid.
/// Throws Poco::UnknownURISchemeException if the scheme is not "mongodb".
private:
void parseOptions(const Poco::URI::QueryParameters& params);
/// Parses query parameters from a QueryParameters collection.
std::string buildQueryString() const;
/// Builds the query string from current configuration options.
std::vector<std::string> _servers;
/// Server addresses stored as strings (host:port format).
/// NOT resolved to avoid DNS errors for non-existent hosts.
std::string _replicaSet;
ReadPreference _readPreference{ReadPreference::Primary};
unsigned int _connectTimeoutMS{10000};
unsigned int _socketTimeoutMS{30000};
unsigned int _heartbeatFrequency{10};
unsigned int _reconnectRetries{10};
unsigned int _reconnectDelay{1};
std::string _database;
std::string _username;
std::string _password;
};
} } // namespace Poco::MongoDB
#endif // MongoDB_ReplicaSetURI_INCLUDED

View File

@@ -13,12 +13,11 @@
#include "Poco/MongoDB/ReplicaSet.h"
#include "Poco/MongoDB/ReplicaSetURI.h"
#include "Poco/MongoDB/OpMsgMessage.h"
#include "Poco/MongoDB/TopologyChangeNotification.h"
#include "Poco/Exception.h"
#include "Poco/Random.h"
#include "Poco/URI.h"
#include "Poco/NumberParser.h"
#include "Poco/NotificationCenter.h"
#include <chrono>
@@ -89,8 +88,87 @@ ReplicaSet::ReplicaSet(const std::vector<Net::SocketAddress>& seeds):
ReplicaSet::ReplicaSet(const std::string& uri)
{
// Parse URI first to extract seeds and configuration
parseURI(uri);
// Parse URI using ReplicaSetURI
ReplicaSetURI parsedURI(uri);
// Extract configuration from parsed URI
// Resolve server strings to SocketAddress objects here
_config.seeds.clear();
for (const auto& serverStr : parsedURI.servers())
{
try
{
_config.seeds.emplace_back(serverStr);
}
catch (const std::exception& e)
{
// Skip servers that cannot be resolved via DNS
// Note: URI parsing already succeeded - ReplicaSetURI stores servers as strings.
// Servers that fail DNS resolution are not added to the seed list.
// Only resolvable servers will be used for topology discovery.
}
}
_config.setName = parsedURI.replicaSet();
_config.readPreference = parsedURI.readPreference();
_config.connectTimeoutSeconds = (parsedURI.connectTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up)
_config.socketTimeoutSeconds = (parsedURI.socketTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up)
_config.heartbeatFrequencySeconds = parsedURI.heartbeatFrequency();
_config.serverReconnectRetries = parsedURI.reconnectRetries();
_config.serverReconnectDelaySeconds = parsedURI.reconnectDelay();
if (_config.seeds.empty())
{
throw Poco::InvalidArgumentException("Replica set URI must contain at least one host");
}
// Update topology with set name from config
_topology.setName(_config.setName);
// Add seed servers to topology
for (const auto& seed : _config.seeds)
{
_topology.addServer(seed);
}
// Perform initial discovery
updateTopologyFromAllServers();
// Start monitoring if enabled
if (_config.enableMonitoring)
{
startMonitoring();
}
}
ReplicaSet::ReplicaSet(const ReplicaSetURI& uri)
{
// Extract configuration from ReplicaSetURI object
// Resolve server strings to SocketAddress objects here
_config.seeds.clear();
for (const auto& serverStr : uri.servers())
{
try
{
_config.seeds.emplace_back(serverStr);
}
catch (const std::exception& e)
{
// Skip servers that cannot be resolved via DNS
// Note: URI parsing already succeeded - ReplicaSetURI stores servers as strings.
// Servers that fail DNS resolution are not added to the seed list.
// Only resolvable servers will be used for topology discovery.
}
}
_config.setName = uri.replicaSet();
_config.readPreference = uri.readPreference();
_config.connectTimeoutSeconds = (uri.connectTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up)
_config.socketTimeoutSeconds = (uri.socketTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up)
_config.heartbeatFrequencySeconds = uri.heartbeatFrequency();
_config.serverReconnectRetries = uri.reconnectRetries();
_config.serverReconnectDelaySeconds = uri.reconnectDelay();
if (_config.seeds.empty())
{
@@ -533,155 +611,4 @@ void ReplicaSet::updateTopologyFromAllServers() noexcept
}
void ReplicaSet::parseURI(const std::string& uri)
{
// Parse MongoDB URI: mongodb://[user:pass@]host1:port1,host2:port2[,hostN:portN]/[database][?options]
// MongoDB URIs can contain comma-separated hosts which Poco::URI doesn't handle correctly.
// We need to extract the host list manually first, then create a simplified URI for Poco::URI
// to parse the scheme, path, and query parameters.
// Find the scheme delimiter
auto schemeEnd = uri.find("://");
if (schemeEnd == std::string::npos)
{
throw Poco::SyntaxException("Invalid URI: missing scheme delimiter");
}
std::string scheme = uri.substr(0, schemeEnd);
if (scheme != "mongodb"s)
{
throw Poco::UnknownURISchemeException("Replica set URI must use 'mongodb' scheme");
}
// Find where the authority (hosts) section ends
// It ends at either '/' (path) or '?' (query)
std::string::size_type authorityStart = schemeEnd + 3; // Skip "://"
std::string::size_type authorityEnd = uri.find_first_of("/?", authorityStart);
// Extract authority and the rest of the URI
std::string authority;
std::string pathAndQuery;
if (authorityEnd != std::string::npos)
{
authority = uri.substr(authorityStart, authorityEnd - authorityStart);
pathAndQuery = uri.substr(authorityEnd);
}
else
{
authority = uri.substr(authorityStart);
pathAndQuery = "";
}
// Remove userinfo if present (username:password@)
const auto atPos = authority.find('@');
const auto hostsStr = (atPos != std::string::npos) ? authority.substr(atPos + 1) : authority;
// Parse comma-separated hosts
_config.seeds.clear();
std::string::size_type start = 0;
std::string::size_type end;
while ((end = hostsStr.find(',', start)) != std::string::npos)
{
const auto hostPort = hostsStr.substr(start, end - start);
if (!hostPort.empty())
{
try
{
_config.seeds.emplace_back(hostPort);
}
catch (...)
{
// Skip invalid host addresses
}
}
start = end + 1;
}
// Parse last host
const auto lastHost = hostsStr.substr(start);
if (!lastHost.empty())
{
try
{
_config.seeds.emplace_back(lastHost);
}
catch (...)
{
// Skip invalid host address
}
}
if (_config.seeds.empty())
{
throw Poco::SyntaxException("No valid hosts found in replica set URI");
}
// Now parse query parameters using Poco::URI
// Create a simplified URI with just the scheme and path/query for Poco::URI to parse
std::string simplifiedURI = scheme + "://localhost" + pathAndQuery;
Poco::URI theURI(simplifiedURI);
// Parse query parameters
Poco::URI::QueryParameters params = theURI.getQueryParameters();
for (const auto& param : params)
{
if (param.first == "replicaSet"s)
{
_config.setName = param.second;
}
else if (param.first == "readPreference"s)
{
// Parse read preference mode
if (param.second == "primary"s)
{
_config.readPreference = ReadPreference(ReadPreference::Primary);
}
else if (param.second == "primaryPreferred"s)
{
_config.readPreference = ReadPreference(ReadPreference::PrimaryPreferred);
}
else if (param.second == "secondary"s)
{
_config.readPreference = ReadPreference(ReadPreference::Secondary);
}
else if (param.second == "secondaryPreferred"s)
{
_config.readPreference = ReadPreference(ReadPreference::SecondaryPreferred);
}
else if (param.second == "nearest"s)
{
_config.readPreference = ReadPreference(ReadPreference::Nearest);
}
}
else if (param.first == "connectTimeoutMS"s)
{
Poco::Int64 timeoutMs = Poco::NumberParser::parse64(param.second);
_config.connectTimeoutSeconds = static_cast<unsigned int>((timeoutMs + 999) / 1000); // Convert ms to seconds (round up)
}
else if (param.first == "socketTimeoutMS"s)
{
Poco::Int64 timeoutMs = Poco::NumberParser::parse64(param.second);
_config.socketTimeoutSeconds = static_cast<unsigned int>((timeoutMs + 999) / 1000); // Convert ms to seconds (round up)
}
else if (param.first == "heartbeatFrequency"s)
{
_config.heartbeatFrequencySeconds = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "reconnectRetries"s)
{
_config.serverReconnectRetries = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "reconnectDelay"s)
{
_config.serverReconnectDelaySeconds = Poco::NumberParser::parseUnsigned(param.second);
}
// Note: readPreferenceTags and maxStalenessSeconds would require more complex parsing
// and are not commonly used, so we skip them for now
}
}
} } // namespace Poco::MongoDB

View File

@@ -0,0 +1,488 @@
//
// ReplicaSetURI.cpp
//
// Library: MongoDB
// Package: MongoDB
// Module: ReplicaSetURI
//
// Copyright (c) 2025, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier: BSL-1.0
//
#include "Poco/MongoDB/ReplicaSetURI.h"
#include "Poco/URI.h"
#include "Poco/NumberParser.h"
#include "Poco/Exception.h"
#include "Poco/String.h"
#include <sstream>
using namespace std::string_literals;
namespace Poco {
namespace MongoDB {
ReplicaSetURI::ReplicaSetURI()
{
}
ReplicaSetURI::ReplicaSetURI(const std::string& uri)
{
parse(uri);
}
ReplicaSetURI::~ReplicaSetURI()
{
}
const std::vector<std::string>& ReplicaSetURI::servers() const
{
return _servers;
}
void ReplicaSetURI::setServers(const std::vector<std::string>& servers)
{
_servers = servers;
}
void ReplicaSetURI::addServer(const std::string& server)
{
_servers.push_back(server);
}
void ReplicaSetURI::clearServers()
{
_servers.clear();
}
std::string ReplicaSetURI::replicaSet() const
{
return _replicaSet;
}
void ReplicaSetURI::setReplicaSet(const std::string& name)
{
_replicaSet = name;
}
ReadPreference ReplicaSetURI::readPreference() const
{
return _readPreference;
}
void ReplicaSetURI::setReadPreference(const ReadPreference& pref)
{
_readPreference = pref;
}
void ReplicaSetURI::setReadPreference(const std::string& mode)
{
std::string lowerMode = Poco::toLower(mode);
if (lowerMode == "primary"s)
{
_readPreference = ReadPreference(ReadPreference::Primary);
}
else if (lowerMode == "primarypreferred"s)
{
_readPreference = ReadPreference(ReadPreference::PrimaryPreferred);
}
else if (lowerMode == "secondary"s)
{
_readPreference = ReadPreference(ReadPreference::Secondary);
}
else if (lowerMode == "secondarypreferred"s)
{
_readPreference = ReadPreference(ReadPreference::SecondaryPreferred);
}
else if (lowerMode == "nearest"s)
{
_readPreference = ReadPreference(ReadPreference::Nearest);
}
else
{
throw Poco::InvalidArgumentException("Invalid read preference mode: " + mode);
}
}
unsigned int ReplicaSetURI::connectTimeoutMS() const
{
return _connectTimeoutMS;
}
void ReplicaSetURI::setConnectTimeoutMS(unsigned int timeoutMS)
{
_connectTimeoutMS = timeoutMS;
}
unsigned int ReplicaSetURI::socketTimeoutMS() const
{
return _socketTimeoutMS;
}
void ReplicaSetURI::setSocketTimeoutMS(unsigned int timeoutMS)
{
_socketTimeoutMS = timeoutMS;
}
unsigned int ReplicaSetURI::heartbeatFrequency() const
{
return _heartbeatFrequency;
}
void ReplicaSetURI::setHeartbeatFrequency(unsigned int seconds)
{
_heartbeatFrequency = seconds;
}
unsigned int ReplicaSetURI::reconnectRetries() const
{
return _reconnectRetries;
}
void ReplicaSetURI::setReconnectRetries(unsigned int retries)
{
_reconnectRetries = retries;
}
unsigned int ReplicaSetURI::reconnectDelay() const
{
return _reconnectDelay;
}
void ReplicaSetURI::setReconnectDelay(unsigned int seconds)
{
_reconnectDelay = seconds;
}
std::string ReplicaSetURI::database() const
{
return _database;
}
void ReplicaSetURI::setDatabase(const std::string& database)
{
_database = database;
}
std::string ReplicaSetURI::username() const
{
return _username;
}
void ReplicaSetURI::setUsername(const std::string& username)
{
_username = username;
}
std::string ReplicaSetURI::password() const
{
return _password;
}
void ReplicaSetURI::setPassword(const std::string& password)
{
_password = password;
}
std::string ReplicaSetURI::toString() const
{
if (_servers.empty())
{
throw Poco::InvalidArgumentException("Cannot generate URI: no servers configured");
}
std::ostringstream uri;
// Scheme
uri << "mongodb://";
// User info
if (!_username.empty())
{
uri << _username;
if (!_password.empty())
{
uri << ":" << _password;
}
uri << "@";
}
// Hosts
for (std::size_t i = 0; i < _servers.size(); ++i)
{
if (i > 0)
{
uri << ",";
}
uri << _servers[i];
}
// Database
if (!_database.empty())
{
uri << "/" << _database;
}
// Query parameters
std::string queryString = buildQueryString();
if (!queryString.empty())
{
// Add leading '/' if we don't have a database
if (_database.empty())
{
uri << "/";
}
uri << "?" << queryString;
}
return uri.str();
}
void ReplicaSetURI::parse(const std::string& uri)
{
// MongoDB URIs can contain comma-separated hosts which Poco::URI doesn't handle correctly.
// We need to extract the host list manually first, then create a simplified URI for Poco::URI
// to parse the scheme, path, and query parameters.
// Find the scheme delimiter
auto schemeEnd = uri.find("://");
if (schemeEnd == std::string::npos)
{
throw Poco::SyntaxException("Invalid URI: missing scheme delimiter");
}
std::string scheme = uri.substr(0, schemeEnd);
if (scheme != "mongodb"s)
{
throw Poco::UnknownURISchemeException("Replica set URI must use 'mongodb' scheme");
}
// Find where the authority (hosts) section ends
// It ends at either '/' (path) or '?' (query)
std::string::size_type authorityStart = schemeEnd + 3; // Skip "://"
std::string::size_type authorityEnd = uri.find_first_of("/?", authorityStart);
// Extract authority and the rest of the URI
std::string authority;
std::string pathAndQuery;
if (authorityEnd != std::string::npos)
{
authority = uri.substr(authorityStart, authorityEnd - authorityStart);
pathAndQuery = uri.substr(authorityEnd);
}
else
{
authority = uri.substr(authorityStart);
pathAndQuery = "";
}
// Parse user info if present (username:password@)
const auto atPos = authority.find('@');
std::string hostsStr;
if (atPos != std::string::npos)
{
std::string userInfo = authority.substr(0, atPos);
hostsStr = authority.substr(atPos + 1);
// Parse username and password
auto colonPos = userInfo.find(':');
if (colonPos != std::string::npos)
{
_username = userInfo.substr(0, colonPos);
_password = userInfo.substr(colonPos + 1);
}
else
{
_username = userInfo;
_password = "";
}
}
else
{
hostsStr = authority;
_username = "";
_password = "";
}
// Parse comma-separated hosts
// Store as strings WITHOUT resolving - resolution happens in ReplicaSet
_servers.clear();
std::string::size_type start = 0;
std::string::size_type end;
while ((end = hostsStr.find(',', start)) != std::string::npos)
{
const auto hostPort = hostsStr.substr(start, end - start);
if (!hostPort.empty())
{
_servers.push_back(hostPort);
}
start = end + 1;
}
// Parse last host
const auto lastHost = hostsStr.substr(start);
if (!lastHost.empty())
{
_servers.push_back(lastHost);
}
if (_servers.empty())
{
throw Poco::SyntaxException("No valid hosts found in replica set URI");
}
// Parse path and query using Poco::URI
// Create a simplified URI with just the scheme and path/query for Poco::URI to parse
std::string simplifiedURI = scheme + "://localhost" + pathAndQuery;
Poco::URI theURI(simplifiedURI);
// Extract database from path
std::string path = theURI.getPath();
if (!path.empty() && path[0] == '/')
{
_database = path.substr(1); // Remove leading '/'
}
else
{
_database = path;
}
// Parse query parameters
Poco::URI::QueryParameters params = theURI.getQueryParameters();
parseOptions(params);
}
void ReplicaSetURI::parseOptions(const Poco::URI::QueryParameters& params)
{
for (const auto& param : params)
{
if (param.first == "replicaSet"s)
{
_replicaSet = param.second;
}
else if (param.first == "readPreference"s)
{
setReadPreference(param.second);
}
else if (param.first == "connectTimeoutMS"s)
{
_connectTimeoutMS = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "socketTimeoutMS"s)
{
_socketTimeoutMS = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "heartbeatFrequency"s)
{
_heartbeatFrequency = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "reconnectRetries"s)
{
_reconnectRetries = Poco::NumberParser::parseUnsigned(param.second);
}
else if (param.first == "reconnectDelay"s)
{
_reconnectDelay = Poco::NumberParser::parseUnsigned(param.second);
}
// Add other options as needed
}
}
std::string ReplicaSetURI::buildQueryString() const
{
std::vector<std::string> params;
if (!_replicaSet.empty())
{
params.push_back("replicaSet=" + _replicaSet);
}
if (_readPreference.mode() != ReadPreference::Primary)
{
params.push_back("readPreference=" + _readPreference.toString());
}
if (_connectTimeoutMS != 10000) // Only add if non-default
{
params.push_back("connectTimeoutMS=" + std::to_string(_connectTimeoutMS));
}
if (_socketTimeoutMS != 30000) // Only add if non-default
{
params.push_back("socketTimeoutMS=" + std::to_string(_socketTimeoutMS));
}
if (_heartbeatFrequency != 10) // Only add if non-default
{
params.push_back("heartbeatFrequency=" + std::to_string(_heartbeatFrequency));
}
if (_reconnectRetries != 10) // Only add if non-default
{
params.push_back("reconnectRetries=" + std::to_string(_reconnectRetries));
}
if (_reconnectDelay != 1) // Only add if non-default
{
params.push_back("reconnectDelay=" + std::to_string(_reconnectDelay));
}
if (params.empty())
{
return "";
}
std::ostringstream queryString;
for (std::size_t i = 0; i < params.size(); ++i)
{
if (i > 0)
{
queryString << "&";
}
queryString << params[i];
}
return queryString.str();
}
} } // namespace Poco::MongoDB

View File

@@ -15,6 +15,7 @@
#include "Poco/MongoDB/TopologyDescription.h"
#include "Poco/MongoDB/ReadPreference.h"
#include "Poco/MongoDB/ReplicaSet.h"
#include "Poco/MongoDB/ReplicaSetURI.h"
#include "Poco/MongoDB/Document.h"
#include "Poco/MongoDB/Array.h"
#include "Poco/Net/SocketAddress.h"
@@ -1070,7 +1071,7 @@ void ReplicaSetTest::testReplicaSetURIParsing()
}
// Test case 2: URI with three hosts and replica set name
std::string uri2 = "mongodb://host1:27017,host2:27018,host3:27019/?replicaSet=rs0";
std::string uri2 = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0";
try
{
@@ -1093,8 +1094,8 @@ void ReplicaSetTest::testReplicaSetURIParsing()
assertTrue(true);
}
// Test case 3: URI with hosts containing dots and dashes
std::string uri3 = "mongodb://mongo-1.example.com:27017,mongo-2.example.com:27017/?readPreference=primaryPreferred";
// Test case 3: URI with multiple localhost servers
std::string uri3 = "mongodb://127.0.0.1:27017,127.0.0.1:27018/?readPreference=primaryPreferred";
try
{
@@ -1119,6 +1120,217 @@ void ReplicaSetTest::testReplicaSetURIParsing()
}
// ============================================================================
// ReplicaSetURI Class Tests
// ============================================================================
void ReplicaSetTest::testReplicaSetURIClass()
{
// Test parsing a complex URI
std::string uri = "mongodb://user:pass@localhost:27017,localhost:27018,localhost:27019/testdb?replicaSet=rs0&readPreference=secondaryPreferred&connectTimeoutMS=5000&socketTimeoutMS=15000&heartbeatFrequency=20&reconnectRetries=5&reconnectDelay=3";
ReplicaSetURI parsedURI(uri);
// Verify servers (stored as strings, not resolved)
const auto& servers = parsedURI.servers();
assertEqual(3, static_cast<int>(servers.size()));
assertEqual("localhost:27017"s, servers[0]);
assertEqual("localhost:27018"s, servers[1]);
assertEqual("localhost:27019"s, servers[2]);
// Verify configuration
assertEqual("rs0"s, parsedURI.replicaSet());
assertEqual(static_cast<int>(ReadPreference::SecondaryPreferred), static_cast<int>(parsedURI.readPreference().mode()));
assertEqual(5000u, parsedURI.connectTimeoutMS());
assertEqual(15000u, parsedURI.socketTimeoutMS());
assertEqual(20u, parsedURI.heartbeatFrequency());
assertEqual(5u, parsedURI.reconnectRetries());
assertEqual(3u, parsedURI.reconnectDelay());
// Verify database and user info
assertEqual("testdb"s, parsedURI.database());
assertEqual("user"s, parsedURI.username());
assertEqual("pass"s, parsedURI.password());
// Test parsing URI without optional parameters
std::string simpleUri = "mongodb://localhost:27017,localhost:27018";
ReplicaSetURI simpleURI(simpleUri);
const auto& simpleServers = simpleURI.servers();
assertEqual(2, static_cast<int>(simpleServers.size()));
assertTrue(simpleURI.replicaSet().empty());
assertTrue(simpleURI.database().empty());
assertTrue(simpleURI.username().empty());
assertEqual(static_cast<int>(ReadPreference::Primary), static_cast<int>(simpleURI.readPreference().mode()));
}
void ReplicaSetTest::testReplicaSetURIToString()
{
// Create a URI object and set properties
ReplicaSetURI uri;
uri.addServer("localhost:27017"s);
uri.addServer("localhost:27018"s);
uri.addServer("localhost:27019"s);
uri.setReplicaSet("rs0"s);
uri.setReadPreference("secondary"s);
uri.setConnectTimeoutMS(5000);
uri.setReconnectRetries(5);
uri.setReconnectDelay(2);
std::string uriString = uri.toString();
// Parse the generated URI back
ReplicaSetURI parsedBack(uriString);
// Verify round-trip
assertEqual(3, static_cast<int>(parsedBack.servers().size()));
assertEqual("rs0"s, parsedBack.replicaSet());
assertEqual(static_cast<int>(ReadPreference::Secondary), static_cast<int>(parsedBack.readPreference().mode()));
assertEqual(5000u, parsedBack.connectTimeoutMS());
assertEqual(5u, parsedBack.reconnectRetries());
assertEqual(2u, parsedBack.reconnectDelay());
// Test URI with database and user info
ReplicaSetURI uriWithAuth;
uriWithAuth.addServer("localhost:27017"s);
uriWithAuth.setUsername("admin"s);
uriWithAuth.setPassword("secret"s);
uriWithAuth.setDatabase("mydb"s);
std::string authUriString = uriWithAuth.toString();
// Verify the URI contains the expected components
assertTrue(authUriString.find("admin:secret@") != std::string::npos);
assertTrue(authUriString.find("localhost:27017") != std::string::npos);
assertTrue(authUriString.find("/mydb") != std::string::npos);
}
void ReplicaSetTest::testReplicaSetURIModification()
{
// Start with a parsed URI
std::string originalUri = "mongodb://localhost:27017,localhost:27018/?replicaSet=rs0&reconnectRetries=10";
ReplicaSetURI uri(originalUri);
// Verify initial state
assertEqual(2, static_cast<int>(uri.servers().size()));
assertEqual("rs0"s, uri.replicaSet());
assertEqual(10u, uri.reconnectRetries());
// Modify servers
uri.addServer("localhost:27019"s);
assertEqual(3, static_cast<int>(uri.servers().size()));
// Modify configuration
uri.setReplicaSet("rs1"s);
uri.setReadPreference("primaryPreferred"s);
uri.setReconnectRetries(20);
assertEqual("rs1"s, uri.replicaSet());
assertEqual(static_cast<int>(ReadPreference::PrimaryPreferred), static_cast<int>(uri.readPreference().mode()));
assertEqual(20u, uri.reconnectRetries());
// Clear and reset servers
uri.clearServers();
assertEqual(0, static_cast<int>(uri.servers().size()));
uri.addServer("127.0.0.1:27017"s);
assertEqual(1, static_cast<int>(uri.servers().size()));
assertEqual("127.0.0.1:27017"s, uri.servers()[0]);
// Test setServers
std::vector<std::string> newServers;
newServers.push_back("host1:27020"s);
newServers.push_back("host2:27021"s);
uri.setServers(newServers);
assertEqual(2, static_cast<int>(uri.servers().size()));
assertEqual("host1:27020"s, uri.servers()[0]);
assertEqual("host2:27021"s, uri.servers()[1]);
// Verify the modified URI can be converted to string
std::string modifiedUri = uri.toString();
assertTrue(modifiedUri.find("host1:27020") != std::string::npos);
assertTrue(modifiedUri.find("host2:27021") != std::string::npos);
}
void ReplicaSetTest::testReplicaSetWithURIObject()
{
// Test creating a ReplicaSet using a ReplicaSetURI object
// This allows programmatic configuration before connecting
ReplicaSetURI uri;
uri.addServer("localhost:27017"s);
uri.addServer("localhost:27018"s);
uri.setReplicaSet("rs0"s);
uri.setReadPreference("secondaryPreferred"s);
uri.setConnectTimeoutMS(5000);
uri.setReconnectRetries(5);
uri.setReconnectDelay(2);
try
{
// Create ReplicaSet from the URI object
ReplicaSet rs(uri);
// Verify configuration was applied
ReplicaSet::Config config = rs.configuration();
assertEqual(2, static_cast<int>(config.seeds.size()));
assertEqual("rs0"s, config.setName);
assertEqual(static_cast<int>(ReadPreference::SecondaryPreferred), static_cast<int>(config.readPreference.mode()));
assertEqual(5u, static_cast<unsigned int>(config.connectTimeoutSeconds)); // 5000ms -> 5s
assertEqual(5u, static_cast<unsigned int>(config.serverReconnectRetries));
assertEqual(2u, config.serverReconnectDelaySeconds);
// If we get here, URI object constructor worked
assertTrue(true);
}
catch (const Poco::SyntaxException& e)
{
std::string msg = "ReplicaSet construction with URI object failed with SyntaxException: " + e.displayText();
fail(msg);
}
catch (const Poco::UnknownURISchemeException& e)
{
std::string msg = "ReplicaSet construction with URI object failed with UnknownURISchemeException: " + e.displayText();
fail(msg);
}
catch (...)
{
// Connection failure is expected since servers don't exist
// We only care that the configuration was properly extracted from the URI object
assertTrue(true);
}
// Test that empty URI object throws exception
ReplicaSetURI emptyUri;
bool exceptionThrown = false;
try
{
ReplicaSet rs(emptyUri);
}
catch (const Poco::InvalidArgumentException& e)
{
// Expected - URI must contain at least one host
exceptionThrown = true;
assertTrue(e.displayText().find("at least one") != std::string::npos);
}
catch (...)
{
// Any other exception is also acceptable for empty URI
exceptionThrown = true;
}
assertTrue(exceptionThrown);
}
CppUnit::Test* ReplicaSetTest::suite()
{
auto* pSuite = new CppUnit::TestSuite("ReplicaSetTest");
@@ -1161,5 +1373,11 @@ CppUnit::Test* ReplicaSetTest::suite()
// ReplicaSet URI parsing tests
CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIParsing);
// ReplicaSetURI class tests
CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIClass);
CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIToString);
CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIModification);
CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetWithURIObject);
return pSuite;
}

View File

@@ -57,6 +57,10 @@ public:
void testReadPreferenceWithTags();
void testReplicaSetURIParsing();
void testReplicaSetURIClass();
void testReplicaSetURIToString();
void testReplicaSetURIModification();
void testReplicaSetWithURIObject();
void setUp() override;
void tearDown() override;