ZaStoGram_desktop/Telegram/SourceFiles/mtproto/proxy/dial_pacer.h
loop-uh f81ffcaed2 Drop the dial concurrency cap the relays disproved
The pacer capped unproven handshakes in flight at two, on the premise that
a public mtproxy answers about that many at once and silently drops the
rest. Measured against live relays the premise is wrong by an order of
magnitude: they carried eight simultaneous dials through to resPQ, then
twenty-four, without a miss.

The cost of being wrong is not symmetric. Under the cap the third dial and
every one after it waited for a slot, and a slot could take the whole
handshake budget to come back; a couple of misses then walked the spacing
up in two second steps to thirty, and the queue clamped at two minutes. One
bad start - a sleep, a network change, a DC switch - was enough to put a
healthy relay behind half a minute of spacing, on state that is process
wide and shared by every account.

So the cap is gone. What stays is the spacing, which is what actually keeps
a cold start from arriving as one burst, and the failure spacing, which
acts on misses that happened instead of on a guess about a relay we have
not dialed yet. A relay that does drop its overflow still walks itself up
the spacing - it just no longer drags the relays that never needed it.

The backoff is gentler with the cap gone: it starts after four misses
rather than two, in half second steps, and stops at four seconds, where the
session's own retry timer is already the slower of the two. Beyond that
point growing the spacing only delays the recovery of a relay that came
back.

Also: a connection that wins on a slower route through confirmBestConnection
is connected, and its lease was being dropped unclaimed, which the pacer
reads as a miss against a relay that had just carried a Telegram reply.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:28:06 +03:00

93 lines
3.1 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/basic_types.h"
#include "mtproto/proxy/data.h"
#include <crl/crl_time.h>
namespace MTP {
class RuntimeEnvironment;
namespace details {
// Paces outgoing connection attempts towards a single proxy server.
//
// Every session dials on its own, so a cold start or a proxy switch makes
// every session of every account open its socket in the same millisecond.
// Some public mtproxies take that batch in stride; others answer the first
// two or three handshakes and silently drop the rest, which the client then
// reads as client_hello_sent_no_server_hello or connected_no_mtproto_data
// across everything that did not get in.
//
// Which kind a relay is cannot be known before dialing it, so the limit is
// not a constant: it starts at what a client asks for anyway and is raised
// by proof, never lowered below concurrency the relay has already answered.
//
// A lease is taken when a connection is created and lives until the attempt
// either proves the relay or dies, so the limit counts unproven handshakes
// and not established connections: a steady state of many live sessions is
// fine, only the burst is spread out.
//
// A run of attempts that never proved anything also stretches the spacing,
// which is the only thing standing between a blackholed proxy and every
// session redialing it on its own eight second retry timer. This is a rate
// limiter and nothing more: it does not mark endpoints unhealthy, does not
// pick proxies and never touches the handshake shape.
class ProxyDialLease final {
public:
ProxyDialLease() = default;
ProxyDialLease(const ProxyDialLease &other) = delete;
ProxyDialLease &operator=(const ProxyDialLease &other) = delete;
ProxyDialLease(ProxyDialLease &&other) noexcept;
ProxyDialLease &operator=(ProxyDialLease &&other) noexcept;
~ProxyDialLease();
// How long the caller must wait before starting the attempt.
[[nodiscard]] crl::time delay() const;
// The proxy relayed a Telegram reply through this attempt.
void proven();
// The attempt is over without having proved anything.
void release();
// The attempt is being dropped for a reason of ours - it lost a route
// race, or the proxy was switched under it. The slot comes back, but the
// proxy did nothing wrong and is not moved towards the failure spacing.
void cancel();
private:
friend ProxyDialLease ReserveProxyDial(
not_null<RuntimeEnvironment*> runtime,
const ProxyData &proxy);
ProxyDialLease(QString key, crl::time delay);
enum class Verdict {
Proven,
Failed,
Cancelled,
};
void finish(Verdict verdict);
QString _key;
crl::time _delay = 0;
};
// Direct connections are never paced, so for them this returns an empty
// lease with a zero delay.
[[nodiscard]] ProxyDialLease ReserveProxyDial(
not_null<RuntimeEnvironment*> runtime,
const ProxyData &proxy);
} // namespace details
} // namespace MTP