From a97ad6ca336e5a170b499559ecf64b7d4945f3c2 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Mon, 1 Jun 2026 18:14:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(udprelay):=20=D0=B1=D0=B0=D1=80=D1=8C=D0=B5?= =?UTF-8?q?=D1=80=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=B0=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D0=B2=D0=B0=20=D0=BA=D1=8D=D1=88?= =?UTF-8?q?=D0=B0=20creds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Стрим 1 поднимается первым и сигналит okchan; стримы 2..N ждут сигнал (или ctx/safety-timeout 20s) и переиспользуют тёплый кэш credentials вместо N одновременных запросов к VK — снимает thundering herd на старте (частая причина rate-limit/captcha). --- internal/proxy/udprelay/run.go | 40 ++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/internal/proxy/udprelay/run.go b/internal/proxy/udprelay/run.go index eb2e0bc..d94aa17 100644 --- a/internal/proxy/udprelay/run.go +++ b/internal/proxy/udprelay/run.go @@ -42,6 +42,11 @@ type Params struct { ClientID string } +// streamStartBarrier — максимум, который стримы 2..N ждут прогрева кэша +// credentials стримом 1 перед стартом. Защита от вечного стопора, если +// стрим 1 не поднимается. +const streamStartBarrier = 20 * time.Second + // ErrFatal возвращается из Run, когда поток встречает условие, требующее // завершения всего приложения (см. provider.ErrFatalNoStreams). Вызывающий // должен проверить через errors.Is и вызвать os.Exit сам — udprelay не @@ -115,19 +120,36 @@ func Run(ctx context.Context, dtlsDialer *dtlsdial.Dialer, auth AuthHandler, log }) t := time.Tick(200 * time.Millisecond) - // Поток 1 получает okchan для сигнализации о первом успешном handshake в лог. - // Все потоки стартуют одновременно — нет барьера между потоком 1 и остальными, - // поэтому медленный DTLS handshake потока 1 не задерживает потоки 2..N. + // Стрим 1 стартует первым и сигналит okchan при первом успешном handshake. + // Остальные стримы ждут этот сигнал (или ctx/safety-timeout), чтобы не бить + // по VK API одновременно: стрим 1 прогревает кэш credentials + // (streams-per-cred), после чего 2..N переиспользуют тёплый кэш вместо N + // параллельных запросов к VK — это снимает thundering herd на старте + // (частая причина rate-limit/captcha при одновременном подъёме всех стримов). okchan := make(chan struct{}, 1) - for i := 0; i < numStreams; i++ { + { + cchan := make(chan net.PacketConn) + wg.Go(func() { + DTLSLoop(runCtx, deps, params, peer, listenConn, inboundChan, cchan, okchan, 1) + }) + wg.Go(func() { + TURNLoop(runCtx, deps, params, peer, cchan, t, 1) + }) + } + + // Барьер: ждём первый успешный стрим, отмену ctx, либо safety-timeout — + // если стрим 1 не поднимается, не стопорим остальные навсегда (liveness). + select { + case <-okchan: + case <-runCtx.Done(): + case <-time.After(streamStartBarrier): + } + + for i := 1; i < numStreams; i++ { cchan := make(chan net.PacketConn) - var ok chan<- struct{} - if i == 0 { - ok = okchan - } streamID := i + 1 wg.Go(func() { - DTLSLoop(runCtx, deps, params, peer, listenConn, inboundChan, cchan, ok, streamID) + DTLSLoop(runCtx, deps, params, peer, listenConn, inboundChan, cchan, nil, streamID) }) wg.Go(func() { TURNLoop(runCtx, deps, params, peer, cchan, t, streamID)