Add peers_in_max and peers_out_max configuration.

Replaces peers_max with individual inbound and
outbound configuration. If peers_max is configured
then outbound and inbound connections are derived
as prior to this change. Otherwise the new
configuration is used. Config::outPeers is no longer
adjusted up or down.
This commit is contained in:
Gregory Tsipenyuk
2020-09-23 18:03:45 -04:00
parent e1a2939f89
commit 70c4eccef1
8 changed files with 289 additions and 75 deletions

View File

@@ -142,7 +142,16 @@ public:
// True to ask peers not to relay current IP.
bool PEER_PRIVATE = false;
// peers_max is a legacy configuration, which is going to be replaced
// with individual inbound peers peers_in_max and outbound peers
// peers_out_max configuration. for now we support both the legacy and
// the new configuration. if peers_max is configured then peers_in_max and
// peers_out_max are ignored.
std::size_t PEERS_MAX = 0;
// is true if peers_max is configured
bool legacyPeersMax_ = false;
std::size_t PEERS_OUT_MAX = 0;
std::size_t PEERS_IN_MAX = 0;
std::chrono::seconds WEBSOCKET_PING_FREQ = std::chrono::minutes{5};

View File

@@ -72,6 +72,8 @@ struct ConfigSection
#define SECTION_PATH_SEARCH_MAX "path_search_max"
#define SECTION_PEER_PRIVATE "peer_private"
#define SECTION_PEERS_MAX "peers_max"
#define SECTION_PEERS_IN_MAX "peers_in_max"
#define SECTION_PEERS_OUT_MAX "peers_out_max"
#define SECTION_REDUCE_RELAY "reduce_relay"
#define SECTION_RELAY_PROPOSALS "relay_proposals"
#define SECTION_RELAY_VALIDATIONS "relay_validations"

View File

@@ -362,7 +362,50 @@ Config::loadFromString(std::string const& fileContents)
PEER_PRIVATE = beast::lexicalCastThrow<bool>(strTemp);
if (getSingleSection(secConfig, SECTION_PEERS_MAX, strTemp, j_))
{
PEERS_MAX = beast::lexicalCastThrow<std::size_t>(strTemp);
legacyPeersMax_ = true;
}
else
{
std::optional<int> peers_in_max{};
if (getSingleSection(secConfig, SECTION_PEERS_IN_MAX, strTemp, j_))
{
peers_in_max = beast::lexicalCastThrow<std::size_t>(strTemp);
if (*peers_in_max > 1000)
Throw<std::runtime_error>(
"Invalid value specified in [" SECTION_PEERS_IN_MAX
"] section; the value must be less or equal than 1000");
}
std::optional<int> peers_out_max{};
if (getSingleSection(secConfig, SECTION_PEERS_OUT_MAX, strTemp, j_))
{
peers_out_max = beast::lexicalCastThrow<std::size_t>(strTemp);
if (*peers_out_max < 10 || *peers_out_max > 1000)
Throw<std::runtime_error>(
"Invalid value specified in [" SECTION_PEERS_OUT_MAX
"] section; the value must be in range 10-1000");
}
// if one section is configured then the other must be configured too
if ((peers_in_max && !peers_out_max) ||
(peers_out_max && !peers_in_max))
Throw<std::runtime_error>("Both sections [" SECTION_PEERS_IN_MAX
"]"
"and [" SECTION_PEERS_OUT_MAX
"] must be configured");
// if none is configured then we assume the legacy path
if (!peers_in_max && !peers_out_max)
{
legacyPeersMax_ = true;
}
else
{
PEERS_IN_MAX = *peers_in_max;
PEERS_OUT_MAX = *peers_out_max;
}
}
if (getSingleSection(secConfig, SECTION_NODE_SIZE, strTemp, j_))
{

View File

@@ -499,37 +499,11 @@ OverlayImpl::checkStopped()
void
OverlayImpl::onPrepare()
{
PeerFinder::Config config;
if (app_.config().PEERS_MAX != 0)
config.maxPeers = app_.config().PEERS_MAX;
config.outPeers = config.calcOutPeers();
auto const port = serverHandler_.setup().overlay.port;
config.peerPrivate = app_.config().PEER_PRIVATE;
// Servers with peer privacy don't want to allow incoming connections
config.wantIncoming = (!config.peerPrivate) && (port != 0);
// This will cause servers configured as validators to request that
// peers they connect to never report their IP address. We set this
// after we set the 'wantIncoming' because we want a "soft" version
// of peer privacy unless the operator explicitly asks for it.
if (!app_.getValidationPublicKey().empty())
config.peerPrivate = true;
// if it's a private peer or we are running as standalone
// automatic connections would defeat the purpose.
config.autoConnect =
!app_.config().standalone() && !app_.config().PEER_PRIVATE;
config.listeningPort = port;
config.features = "";
config.ipLimit = setup_.ipLimit;
// Enforce business rules
config.applyTuning();
PeerFinder::Config config = PeerFinder::Config::makeConfig(
app_.config(),
serverHandler_.setup().overlay.port,
!app_.getValidationPublicKey().empty(),
setup_.ipLimit);
m_peerFinder->setConfig(config);

View File

@@ -22,6 +22,7 @@
#include <ripple/beast/clock/abstract_clock.h>
#include <ripple/beast/utility/PropertyStream.h>
#include <ripple/core/Config.h>
#include <ripple/core/Stoppable.h>
#include <ripple/peerfinder/Slot.h>
#include <boost/asio/ip/tcp.hpp>
@@ -45,17 +46,20 @@ struct Config
*/
int maxPeers;
/** Legacy config if peers_max is set. */
bool legacyConfig;
/** The number of automatic outbound connections to maintain.
Outbound connections are only maintained if autoConnect
is `true`. The value can be fractional; The decision to round up
or down will be made using a per-process pseudorandom number and
a probability proportional to the fractional part.
Example:
If outPeers is 9.3, then 30% of nodes will maintain 9 outbound
connections, while 70% of nodes will maintain 10 outbound
connections.
is `true`.
*/
double outPeers;
int outPeers;
/** The number of automatic inbound connections to maintain.
Inbound connections are only maintained if wantIncoming
is `true`.
*/
int inPeers;
/** `true` if we want our IP address kept private. */
bool peerPrivate = true;
@@ -81,7 +85,7 @@ struct Config
Config();
/** Returns a suitable value for outPeers according to the rules. */
double
std::size_t
calcOutPeers() const;
/** Adjusts the values so they follow the business rules. */
@@ -91,6 +95,20 @@ struct Config
/** Write the configuration into a property stream */
void
onWrite(beast::PropertyStream::Map& map);
/** Make PeerFinder::Config from configuration parameters
* @param config server's configuration
* @param port server's listening port
* @param validationPublicKey true if validation public key is not empty
* @param ipLimit limit of incoming connections per IP
* @return PeerFinder::Config
*/
static Config
makeConfig(
ripple::Config const& config,
std::uint16_t port,
bool validationPublicKey,
int ipLimit);
};
//------------------------------------------------------------------------------

View File

@@ -48,8 +48,6 @@ public:
, m_acceptCount(0)
, m_closingCount(0)
{
m_roundingThreshold =
std::generate_canonical<double, 10>(default_prng());
}
//--------------------------------------------------------------------------
@@ -136,28 +134,28 @@ public:
void
onConfig(Config const& config)
{
// Calculate the number of outbound peers we want. If we dont want or
// can't accept incoming, this will simply be equal to maxPeers.
// Otherwise we calculate a fractional amount based on percentages and
// pseudo-randomly round up or down.
//
if (config.wantIncoming)
if (config.legacyConfig)
{
// Round outPeers upwards using a Bernoulli distribution
m_out_max = std::floor(config.outPeers);
if (m_roundingThreshold < (config.outPeers - m_out_max))
++m_out_max;
}
else
{
m_out_max = config.maxPeers;
}
// Calculate the number of outbound peers we want. If we dont want
// or can't accept incoming, this will simply be equal to maxPeers.
if (config.wantIncoming)
m_out_max = config.outPeers;
else
m_out_max = config.maxPeers;
// Calculate the largest number of inbound connections we could take.
if (config.maxPeers >= m_out_max)
m_in_max = config.maxPeers - m_out_max;
// Calculate the largest number of inbound connections we could
// take.
if (config.maxPeers >= m_out_max)
m_in_max = config.maxPeers - m_out_max;
else
m_in_max = 0;
}
else
m_in_max = 0;
{
m_out_max = config.outPeers;
if (config.wantIncoming)
m_in_max = config.inPeers;
}
}
/** Returns the number of accepted connections that haven't handshaked. */
@@ -350,13 +348,6 @@ private:
// Number of connections that are gracefully closing.
int m_closingCount;
/** Fractional threshold below which we round down.
This is used to round the value of Config::outPeers up or down in
such a way that the network-wide average number of outgoing
connections approximates the recommended, fractional value.
*/
double m_roundingThreshold;
};
} // namespace PeerFinder

View File

@@ -25,7 +25,9 @@ namespace PeerFinder {
Config::Config()
: maxPeers(Tuning::defaultMaxPeers)
, legacyConfig(false)
, outPeers(calcOutPeers())
, inPeers(0)
, wantIncoming(true)
, autoConnect(true)
, listeningPort(0)
@@ -33,21 +35,28 @@ Config::Config()
{
}
double
std::size_t
Config::calcOutPeers() const
{
return std::max(
maxPeers * Tuning::outPercent * 0.01, double(Tuning::minOutCount));
return std::round(std::max(
maxPeers * Tuning::outPercent * 0.01, double(Tuning::minOutCount)));
}
void
Config::applyTuning()
{
if (maxPeers < Tuning::minOutCount)
maxPeers = Tuning::minOutCount;
outPeers = calcOutPeers();
if (legacyConfig)
{
if (maxPeers < Tuning::minOutCount)
maxPeers = Tuning::minOutCount;
outPeers = calcOutPeers();
auto const inPeers = maxPeers - outPeers;
inPeers = maxPeers - outPeers;
}
else
{
maxPeers = 0;
}
if (ipLimit == 0)
{
@@ -78,5 +87,53 @@ Config::onWrite(beast::PropertyStream::Map& map)
map["ip_limit"] = ipLimit;
}
Config
Config::makeConfig(
ripple::Config const& cfg,
std::uint16_t port,
bool validationPublicKey,
int ipLimit)
{
PeerFinder::Config config;
config.legacyConfig = cfg.legacyPeersMax_;
if (config.legacyConfig)
{
if (cfg.PEERS_MAX != 0)
config.maxPeers = cfg.PEERS_MAX;
config.outPeers = config.calcOutPeers();
}
else
{
config.outPeers = cfg.PEERS_OUT_MAX;
config.inPeers = cfg.PEERS_IN_MAX;
}
config.peerPrivate = cfg.PEER_PRIVATE;
// Servers with peer privacy don't want to allow incoming connections
config.wantIncoming = (!config.peerPrivate) && (port != 0);
// This will cause servers configured as validators to request that
// peers they connect to never report their IP address. We set this
// after we set the 'wantIncoming' because we want a "soft" version
// of peer privacy unless the operator explicitly asks for it.
if (validationPublicKey)
config.peerPrivate = true;
// if it's a private peer or we are running as standalone
// automatic connections would defeat the purpose.
config.autoConnect = !cfg.standalone() && !cfg.PEER_PRIVATE;
config.listeningPort = port;
config.features = "";
config.ipLimit = ipLimit;
// Enforce business rules
config.applyTuning();
return config;
}
} // namespace PeerFinder
} // namespace ripple

View File

@@ -20,6 +20,7 @@
#include <ripple/basics/Slice.h>
#include <ripple/basics/chrono.h>
#include <ripple/beast/unit_test.h>
#include <ripple/core/Config.h>
#include <ripple/peerfinder/impl/Logic.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/SecretKey.h>
@@ -156,11 +157,130 @@ public:
BEAST_EXPECT(n <= (seconds + 59) / 60);
}
void
test_config()
{
// if peers_max is configured then peers_in_max and peers_out_max are
// ignored
auto run = [&](std::string const& test,
std::optional<std::uint16_t> maxPeers,
std::optional<std::uint16_t> maxIn,
std::optional<std::uint16_t> maxOut,
std::uint16_t port,
std::uint16_t expectOut,
std::uint16_t expectIn,
std::uint16_t expectIpLimit) {
ripple::Config c;
testcase(test);
std::string toLoad = "";
int max = 0;
if (maxPeers)
{
max = maxPeers.value();
toLoad += "[peers_max]\n" + std::to_string(max) + "\n" +
"[peers_in_max]\n" + std::to_string(*maxIn) + "\n" +
"[peers_out_max]\n" + std::to_string(*maxOut) + "\n";
}
else if (maxIn && maxOut)
{
toLoad += "[peers_in_max]\n" + std::to_string(*maxIn) + "\n" +
"[peers_out_max]\n" + std::to_string(*maxOut) + "\n";
}
c.loadFromString(toLoad);
BEAST_EXPECT(
(c.legacyPeersMax_ && c.PEERS_MAX == max &&
c.PEERS_IN_MAX == 0 && c.PEERS_OUT_MAX == 0) ||
(!c.legacyPeersMax_ && c.PEERS_IN_MAX == *maxIn &&
c.PEERS_OUT_MAX == *maxOut));
Config config = Config::makeConfig(c, port, false, 0);
Counts counts;
counts.onConfig(config);
BEAST_EXPECT(
counts.out_max() == expectOut &&
counts.inboundSlots() == expectIn &&
config.ipLimit == expectIpLimit);
};
// if max_peers == 0 => maxPeers = 21,
// else if max_peers < 10 => maxPeers = 10 else maxPeers = max_peers
// expectOut => if legacy => max(0.15 * maxPeers, 10),
// if legacy && !wantIncoming => maxPeers else max_out_peers
// expectIn => if legacy && wantIncoming => maxPeers - outPeers
// else if !wantIncoming => 0 else max_in_peers
// ipLimit => if expectIn <= 21 => 2 else 2 + min(5, expectIn/21)
// ipLimit = max(1, min(ipLimit, expectIn/2))
// legacy test with max_peers
run("legacy no config", {}, {}, {}, 4000, 10, 11, 2);
run("legacy max_peers 0", 0, 100, 10, 4000, 10, 11, 2);
run("legacy max_peers 5", 5, 100, 10, 4000, 10, 0, 1);
run("legacy max_peers 20", 20, 100, 10, 4000, 10, 10, 2);
run("legacy max_peers 100", 100, 100, 10, 4000, 15, 85, 6);
run("legacy max_peers 20, private", 20, 100, 10, 0, 20, 0, 2);
// test with max_in_peers and max_out_peers
run("new in 100/out 10", {}, 100, 10, 4000, 10, 100, 6);
run("new in 0/out 10", {}, 0, 10, 4000, 10, 0, 1);
run("new in 100/out 10, private", {}, 100, 10, 0, 10, 0, 6);
}
void
test_invalid_config()
{
testcase("invalid config");
auto run = [&](std::string const& toLoad) {
ripple::Config c;
try
{
c.loadFromString(toLoad);
fail();
}
catch (...)
{
pass();
}
};
run(R"rippleConfig(
[peers_in_max]
100
)rippleConfig");
run(R"rippleConfig(
[peers_out_max]
100
)rippleConfig");
run(R"rippleConfig(
[peers_in_max]
100
[peers_out_max]
5
)rippleConfig");
run(R"rippleConfig(
[peers_in_max]
1001
[peers_out_max]
10
)rippleConfig");
run(R"rippleConfig(
[peers_in_max]
10
[peers_out_max]
1001
)rippleConfig");
}
void
run() override
{
test_backoff1();
test_backoff2();
test_config();
test_invalid_config();
}
};