Skip to content

Commit c9bbd45

Browse files
committed
feat(p2p): add runtime-level connect stats for CLI and HTTP parity
1 parent 8953e52 commit c9bbd45

1 file changed

Lines changed: 170 additions & 3 deletions

File tree

include/vix/p2p/P2P.hpp

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515

1616
#include <memory>
1717
#include <optional>
18+
#include <string>
19+
#include <unordered_map>
20+
#include <mutex>
21+
#include <chrono>
22+
#include <cstdint>
23+
#include <algorithm>
24+
1825
#include <vix/p2p/Node.hpp>
1926

2027
namespace vix::p2p
2128
{
22-
2329
class P2P
2430
{
2531
public:
@@ -33,36 +39,197 @@ namespace vix::p2p
3339
virtual NodeStats stats() const = 0;
3440
};
3541

42+
// Runtime-level stats (CLI parity)
43+
struct ConnectStats
44+
{
45+
std::uint64_t connect_attempts{0};
46+
std::uint64_t connect_deduped{0};
47+
std::uint64_t connect_failures{0};
48+
std::uint64_t backoff_skips{0};
49+
std::uint64_t tracked_endpoints{0};
50+
};
51+
52+
struct RuntimeStats : NodeStats
53+
{
54+
ConnectStats connect{};
55+
};
56+
3657
class P2PRuntime final : public P2P
3758
{
3859
public:
39-
explicit P2PRuntime(std::shared_ptr<Node> node) : node_(std::move(node)) {}
60+
explicit P2PRuntime(std::shared_ptr<Node> node)
61+
: node_(std::move(node))
62+
{
63+
}
4064

4165
void start() override
4266
{
4367
if (node_)
4468
node_->start();
4569
}
70+
4671
void stop() override
4772
{
4873
if (node_)
4974
node_->stop();
5075
}
5176

77+
// Default: manual connect (same spirit as CLI --connect)
5278
bool connect(const PeerEndpoint &ep) override
5379
{
54-
return node_ ? node_->connect(ep) : false;
80+
return connect(ep, /*manual=*/true);
81+
}
82+
83+
// Used by discovery/bootstrap (auto connects)
84+
bool connect_auto(const PeerEndpoint &ep)
85+
{
86+
return connect(ep, /*manual=*/false);
5587
}
5688

5789
NodeStats stats() const override
5890
{
5991
return node_ ? node_->stats() : NodeStats{};
6092
}
6193

94+
RuntimeStats runtime_stats() const
95+
{
96+
RuntimeStats out{};
97+
if (node_)
98+
static_cast<NodeStats &>(out) = node_->stats();
99+
100+
std::lock_guard<std::mutex> lock(mu_);
101+
out.connect = connect_stats_;
102+
out.connect.tracked_endpoints = table_.size();
103+
return out;
104+
}
105+
62106
std::shared_ptr<Node> node() const { return node_; }
63107

108+
private:
109+
struct ConnectEntry
110+
{
111+
std::uint32_t failures{0};
112+
std::chrono::steady_clock::time_point backoff_until{};
113+
std::chrono::steady_clock::time_point last_attempt{};
114+
};
115+
116+
static std::string to_lower_copy(std::string s)
117+
{
118+
for (auto &c : s)
119+
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
120+
return s;
121+
}
122+
123+
static std::string key_for(const PeerEndpoint &ep)
124+
{
125+
std::string scheme = ep.scheme.empty() ? "tcp" : to_lower_copy(ep.scheme);
126+
return scheme + "://" + ep.host + ":" + std::to_string(ep.port);
127+
}
128+
129+
bool allow_attempt_unlocked_(const PeerEndpoint &ep, bool manual)
130+
{
131+
const auto now = std::chrono::steady_clock::now();
132+
const std::string key = key_for(ep);
133+
auto &e = table_[key];
134+
135+
if (e.backoff_until.time_since_epoch().count() != 0 && now < e.backoff_until)
136+
{
137+
++connect_stats_.backoff_skips;
138+
return false;
139+
}
140+
141+
constexpr auto kMinAttemptGap = std::chrono::milliseconds(900);
142+
if (!manual && e.last_attempt.time_since_epoch().count() != 0 && (now - e.last_attempt) < kMinAttemptGap)
143+
{
144+
++connect_stats_.connect_deduped;
145+
return false;
146+
}
147+
148+
e.last_attempt = now;
149+
++connect_stats_.connect_attempts;
150+
return true;
151+
}
152+
153+
void mark_failure_unlocked_(const PeerEndpoint &ep, bool manual)
154+
{
155+
const auto now = std::chrono::steady_clock::now();
156+
const std::string key = key_for(ep);
157+
auto &e = table_[key];
158+
159+
++connect_stats_.connect_failures;
160+
161+
if (manual)
162+
{
163+
e.failures = std::min<std::uint32_t>(e.failures + 1, 8u);
164+
const std::uint64_t backoff_ms =
165+
std::min<std::uint64_t>(2500ULL * (1ULL << std::min<std::uint32_t>(e.failures - 1, 5u)), 12000ULL);
166+
e.backoff_until = now + std::chrono::milliseconds(backoff_ms);
167+
}
168+
else
169+
{
170+
e.failures = std::min<std::uint32_t>(e.failures + 1, 10u);
171+
const std::uint64_t backoff_ms =
172+
std::min<std::uint64_t>(2000ULL * (1ULL << std::min<std::uint32_t>(e.failures - 1, 6u)), 15000ULL);
173+
e.backoff_until = now + std::chrono::milliseconds(backoff_ms);
174+
}
175+
}
176+
177+
void mark_success_unlocked_(const PeerEndpoint &ep)
178+
{
179+
const std::string key = key_for(ep);
180+
auto it = table_.find(key);
181+
if (it == table_.end())
182+
return;
183+
it->second.failures = 0;
184+
it->second.backoff_until = {};
185+
}
186+
187+
bool connect(const PeerEndpoint &ep, bool manual)
188+
{
189+
if (!node_)
190+
return false;
191+
192+
// ensure started
193+
node_->start();
194+
195+
// guard + backoff/dedupe
196+
{
197+
std::lock_guard<std::mutex> lock(mu_);
198+
if (!allow_attempt_unlocked_(ep, manual))
199+
return false;
200+
}
201+
202+
// attempt connect
203+
try
204+
{
205+
const bool ok = node_->connect(ep);
206+
if (ok)
207+
{
208+
std::lock_guard<std::mutex> lock(mu_);
209+
mark_success_unlocked_(ep);
210+
}
211+
else
212+
{
213+
std::lock_guard<std::mutex> lock(mu_);
214+
mark_failure_unlocked_(ep, manual);
215+
}
216+
return ok;
217+
}
218+
catch (...)
219+
{
220+
std::lock_guard<std::mutex> lock(mu_);
221+
mark_failure_unlocked_(ep, manual);
222+
return false;
223+
}
224+
}
225+
64226
private:
65227
std::shared_ptr<Node> node_;
228+
229+
// ConnectGuard state (runtime truth)
230+
mutable std::mutex mu_;
231+
std::unordered_map<std::string, ConnectEntry> table_;
232+
ConnectStats connect_stats_{};
66233
};
67234

68235
} // namespace vix::p2p

0 commit comments

Comments
 (0)