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
2027namespace 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