Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-util/src/connection_manager.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::pin::Pin;
16
use core::task::{Context, Poll};
17
use core::time::Duration;
18
use std::collections::VecDeque;
19
use std::sync::Arc;
20
21
use futures::Future;
22
use futures::stream::{FuturesUnordered, StreamExt, unfold};
23
use nativelink_config::stores::Retry;
24
use nativelink_error::{Code, Error, make_err};
25
use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
26
use tonic::transport::{Channel, Endpoint, channel};
27
use tracing::{debug, error, info, warn};
28
29
use crate::background_spawn;
30
use crate::metrics::record_connection_acquired;
31
use crate::retry::{self, Retrier, RetryResult};
32
33
/// A helper utility that enables management of a suite of connections to an
34
/// upstream gRPC endpoint using Tonic.
35
#[derive(Debug)]
36
pub struct ConnectionManager {
37
    // The channel to request connections from the worker.
38
    worker_tx: mpsc::Sender<(String, oneshot::Sender<Connection>)>,
39
}
40
41
/// The index into `ConnectionManagerWorker::endpoints`.
42
type EndpointIndex = usize;
43
/// The identifier for a given connection to a given Endpoint, used to identify
44
/// when a particular connection has failed or becomes available.
45
type ConnectionIndex = usize;
46
47
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48
struct ChannelIdentifier {
49
    /// The index into `ConnectionManagerWorker::endpoints` that established this
50
    /// Channel.
51
    endpoint_index: EndpointIndex,
52
    /// A unique identifier for this particular connection to the Endpoint.
53
    connection_index: ConnectionIndex,
54
}
55
56
/// The requests that can be made from a Connection to the
57
/// `ConnectionManagerWorker` such as informing it that it's been dropped or that
58
/// an error occurred.
59
enum ConnectionRequest {
60
    /// Notify that a Connection was dropped, if it was dropped while the
61
    /// connection was still pending, then return the pending Channel to be
62
    /// added back to the available channels.
63
    Dropped(Option<EstablishedChannel>),
64
    /// Notify that a Connection was established, return the Channel to the
65
    /// available channels.
66
    Connected(EstablishedChannel),
67
    /// Notify that there was a transport error on the given Channel, the bool
68
    /// specifies whether the connection was in the process of being established
69
    /// or not (i.e. whether it's been returned to available channels yet).
70
    Error((ChannelIdentifier, bool)),
71
}
72
73
/// The result of a Future that connects to a given Endpoint.  This is a tuple
74
/// of the index into the `ConnectionManagerWorker::endpoints` that this
75
/// connection is for, the iteration of the connection and the result of the
76
/// connection itself.
77
type IndexedChannel = Result<EstablishedChannel, (ChannelIdentifier, Error)>;
78
79
/// A channel that has been established to an endpoint with some metadata around
80
/// it to allow identification of the Channel if it errors in order to correctly
81
/// remove it.
82
#[derive(Debug, Clone)]
83
struct EstablishedChannel {
84
    /// The Channel itself that the meta data relates to.
85
    channel: Channel,
86
    /// The identifier of the channel in the worker.
87
    identifier: ChannelIdentifier,
88
}
89
90
/// The context of the worker used to manage all of the connections.  This
91
/// handles reconnecting to endpoints on errors and multiple connections to a
92
/// given endpoint.
93
struct ConnectionManagerWorker {
94
    /// The endpoints to establish Channels and the identifier of the last
95
    /// connection attempt to that endpoint.
96
    endpoints: Vec<(ConnectionIndex, Endpoint)>,
97
    /// The channel used to communicate between a Connection and the worker.
98
    connection_tx: mpsc::UnboundedSender<ConnectionRequest>,
99
    /// Gates the maximum number of in-flight `Connection` objects.
100
    /// Was an explicit `usize` counter; now an `Arc<Semaphore>` so the
101
    /// `OwnedSemaphorePermit` held by each `Connection` releases on
102
    /// drop (RAII), instead of relying on a `ConnectionRequest::Dropped`
103
    /// round-trip that could be lost on tonic transport errors or task
104
    /// aborts.
105
    available_connections: Arc<Semaphore>,
106
107
    /// Whether `available_connections` reflects a configured limit. When the
108
    /// pool is unlimited its permit count is a sentinel, not a measurement.
109
    bounded_connections: bool,
110
    /// Channels that are currently being connected.
111
    connecting_channels: FuturesUnordered<Pin<Box<dyn Future<Output = IndexedChannel> + Send>>>,
112
    /// Connected channels that are available for use.
113
    available_channels: VecDeque<EstablishedChannel>,
114
    /// Requests for a Channel when available - (reason, request)
115
    waiting_connections: VecDeque<(String, oneshot::Sender<Connection>)>,
116
    /// The retry configuration for connecting to an Endpoint, on failure will
117
    /// restart the retrier after a 1 second delay.
118
    retrier: Retrier,
119
}
120
121
/// The maximum number of queued requests to obtain a connection from the
122
/// worker before applying back pressure to the requestor.  It makes sense to
123
/// keep this small since it has to wait for a response anyway.
124
const WORKER_BACKLOG: usize = 8;
125
126
impl ConnectionManager {
127
    /// Create a connection manager that creates a balance list between a given
128
    /// set of Endpoints.  This will restrict the number of concurrent requests
129
    /// and automatically re-connect upon transport error.
130
34
    pub fn new(
131
34
        endpoints: impl IntoIterator<Item = Endpoint>,
132
34
        mut connections_per_endpoint: usize,
133
34
        mut max_concurrent_requests: usize,
134
34
        retry: Retry,
135
34
        jitter_fn: retry::JitterFn,
136
34
    ) -> Self {
137
34
        let (worker_tx, worker_rx) = mpsc::channel(WORKER_BACKLOG);
138
        // The connection messages always come from sync contexts (e.g. drop)
139
        // and therefore, we'd end up spawning for them if this was bounded
140
        // which defeats the object since there would be no backpressure
141
        // applied. Therefore it makes sense for this to be unbounded.
142
34
        let (connection_tx, connection_rx) = mpsc::unbounded_channel();
143
34
        let endpoints = endpoints
144
34
            .into_iter()
145
34
            .map(|endpoint| (0, endpoint))
146
34
            .collect();
147
148
        // Zero means unlimited, which becomes a semaphore holding
149
        // Semaphore::MAX_PERMITS. That is a sentinel, not a free-slot count,
150
        // so remember which case this is and report no figure when unbounded.
151
34
        let bounded_connections = max_concurrent_requests != 0;
152
34
        if max_concurrent_requests == 0 {
153
31
            max_concurrent_requests = Semaphore::MAX_PERMITS;
154
31
        } else {
155
3
            max_concurrent_requests = max_concurrent_requests.min(Semaphore::MAX_PERMITS);
156
3
        }
157
34
        if connections_per_endpoint == 0 {
158
31
            connections_per_endpoint = 1;
159
31
        
}3
160
34
        let worker = ConnectionManagerWorker {
161
34
            endpoints,
162
34
            available_connections: Arc::new(Semaphore::new(max_concurrent_requests)),
163
34
            bounded_connections,
164
34
            connection_tx,
165
34
            connecting_channels: FuturesUnordered::new(),
166
34
            available_channels: VecDeque::new(),
167
34
            waiting_connections: VecDeque::new(),
168
34
            retrier: Retrier::new(
169
34
                Arc::new(|duration| 
Box::pin0
(
tokio::time::sleep0
(
duration0
))),
170
34
                jitter_fn,
171
34
                retry,
172
            ),
173
        };
174
34
        background_spawn!("connection_manager_worker_spawn", async move 
{32
175
32
            worker
176
32
                .service_requests(connections_per_endpoint, worker_rx, connection_rx)
177
32
                .await;
178
0
        });
179
34
        Self { worker_tx }
180
34
    }
181
182
    /// Get a Connection that can be used as a `tonic::Channel`, except it
183
    /// performs some additional counting to reconnect on error and restrict
184
    /// the number of concurrent connections.
185
254
    pub async fn connection(&self, reason: String) -> Result<Connection, Error> {
186
254
        let (tx, rx) = oneshot::channel();
187
254
        self.worker_tx
188
254
            .send((reason, tx))
189
254
            .await
190
254
            .map_err(|err| 
make_err!0
(
Code::Unavailable0
, "Requesting a new connection: {err:?}"))
?0
;
191
254
        rx.await
192
246
            .map_err(|err| 
make_err!0
(
Code::Unavailable0
, "Waiting for a new connection: {err:?}"))
193
246
    }
194
}
195
196
impl ConnectionManagerWorker {
197
32
    async fn service_requests(
198
32
        mut self,
199
32
        connections_per_endpoint: usize,
200
32
        mut worker_rx: mpsc::Receiver<(String, oneshot::Sender<Connection>)>,
201
32
        mut connection_rx: mpsc::UnboundedReceiver<ConnectionRequest>,
202
32
    ) {
203
        // Make the initial set of connections, connection failures will be
204
        // handled in the same way as future transport failures, so no need to
205
        // do anything special.
206
32
        for endpoint_index in 0..self.endpoints.len() {
207
36
            for _ in 
0..connections_per_endpoint32
{
208
36
                self.connect_endpoint(endpoint_index, None);
209
36
            }
210
        }
211
212
        // The main worker loop, when select resolves one of its arms the other
213
        // ones are cancelled, therefore it's important that they maintain no
214
        // state while `await`-ing.  This is enforced through the use of
215
        // non-async functions to do all of the work.
216
        loop {
217
597
            tokio::select! {
218
597
                
request254
= worker_rx.recv() => {
219
254
                    let Some((reason, request)) = request else {
220
                        // The ConnectionManager was dropped, shut down the
221
                        // worker.
222
0
                        break;
223
                    };
224
254
                    self.handle_worker(reason, request);
225
                }
226
597
                
maybe_request275
= connection_rx.recv() => {
227
275
                    if let Some(request) = maybe_request {
228
275
                        self.handle_connection(request);
229
275
                    
}0
230
                }
231
597
                
maybe_connection_result36
= self.connect_next() => {
232
36
                    if let Some(connection_result) = maybe_connection_result {
233
36
                        self.handle_connected(connection_result);
234
36
                    
}0
235
                }
236
            }
237
        }
238
0
    }
239
240
597
    async fn connect_next(&mut self) -> Option<IndexedChannel> 
{430
241
430
        if self.connecting_channels.is_empty() {
242
            // Make this Future never resolve, we will get cancelled by the
243
            // select if there's some change in state to `self` and can re-enter
244
            // and evaluate `connecting_channels` again.
245
367
            futures::future::pending::<()>().await;
246
63
        }
247
63
        self.connecting_channels.next().await
248
36
    }
249
250
    // This must never be made async otherwise the select may cancel it.
251
36
    fn handle_connected(&mut self, connection_result: IndexedChannel) {
252
36
        match connection_result {
253
36
            Ok(established_channel) => {
254
36
                self.available_channels.push_back(established_channel);
255
36
                self.maybe_available_connection();
256
36
            }
257
            // When the retrier runs out of attempts start again from the
258
            // beginning of the retry period.  Never want to be in a
259
            // situation where we give up on an Endpoint forever.
260
0
            Err((identifier, _)) => {
261
0
                self.connect_endpoint(identifier.endpoint_index, Some(identifier.connection_index));
262
0
            }
263
        }
264
36
    }
265
266
36
    fn connect_endpoint(&mut self, endpoint_index: usize, connection_index: Option<usize>) {
267
36
        let Some((current_connection_index, endpoint)) = self.endpoints.get_mut(endpoint_index)
268
        else {
269
            // Unknown endpoint, this should never happen.
270
0
            error!(?endpoint_index, "Connection to unknown endpoint requested");
271
0
            return;
272
        };
273
36
        let is_backoff = connection_index.is_some();
274
36
        let connection_index = connection_index.unwrap_or_else(|| {
275
36
            *current_connection_index += 1;
276
36
            *current_connection_index
277
36
        });
278
36
        if is_backoff {
279
0
            warn!(
280
                ?connection_index,
281
0
                endpoint = ?endpoint.uri(),
282
                "Connection failed, reconnecting"
283
            );
284
        } else {
285
36
            info!(
286
                ?connection_index,
287
36
                endpoint = ?endpoint.uri(),
288
                "Creating new connection"
289
            );
290
        }
291
36
        let identifier = ChannelIdentifier {
292
36
            endpoint_index,
293
36
            connection_index,
294
36
        };
295
36
        let connection_stream = unfold(endpoint.clone(), move |endpoint| async move {
296
36
            let result = endpoint.connect().await.map_err(|err| 
{0
297
0
                make_err!(
298
0
                    Code::Unavailable,
299
                    "Failed to connect to {:?}: {err:?}",
300
0
                    endpoint.uri()
301
                )
302
0
            });
303
36
            Some((
304
36
                result.map_or_else(RetryResult::Retry, RetryResult::Ok),
305
36
                endpoint,
306
36
            ))
307
72
        });
308
36
        let retrier = self.retrier.clone();
309
36
        self.connecting_channels.push(Box::pin(async move {
310
36
            if is_backoff {
311
                // Just in case the retry config is 0, then we need to
312
                // introduce some delay so we aren't in a hard loop.
313
0
                tokio::time::sleep(Duration::from_secs(1)).await;
314
36
            }
315
36
            retrier.retry(connection_stream).await.map_or_else(
316
0
                |err| Err((identifier, err)),
317
36
                |channel| {
318
36
                    Ok(EstablishedChannel {
319
36
                        channel,
320
36
                        identifier,
321
36
                    })
322
36
                },
323
            )
324
36
        }));
325
36
    }
326
327
    /// Free slots, or `None` when the pool is unlimited and the permit count
328
    /// is a sentinel rather than a measurement.
329
254
    fn free_connection_slots(&self) -> Option<usize> {
330
254
        self.bounded_connections
331
254
            .then(|| 
self.available_connections215
.
available_permits215
())
332
254
    }
333
334
    // This must never be made async otherwise the select may cancel it.
335
254
    fn handle_worker(&mut self, reason: String, tx: oneshot::Sender<Connection>) {
336
254
        let maybe_permit = self.available_connections.clone().try_acquire_owned().ok();
337
254
        if let Some(
permit253
) = maybe_permit
338
253
            && let Some(
channel153
) = self.available_channels.pop_front()
339
        {
340
153
            debug!(reason, "ConnectionManager: request running");
341
153
            record_connection_acquired("grpc", self.free_connection_slots(), false);
342
153
            self.provide_channel(channel, tx, permit);
343
        } else {
344
101
            debug!(
345
101
                available_permits = self.available_connections.available_permits(),
346
101
                available_channels = self.available_channels.len(),
347
101
                waiting_connections = self.waiting_connections.len(),
348
                reason,
349
                "ConnectionManager: no connection available, request queued",
350
            );
351
101
            record_connection_acquired("grpc", self.free_connection_slots(), true);
352
101
            self.waiting_connections.push_back((reason, tx));
353
        }
354
254
    }
355
356
254
    fn provide_channel(
357
254
        &self,
358
254
        channel: EstablishedChannel,
359
254
        tx: oneshot::Sender<Connection>,
360
254
        permit: OwnedSemaphorePermit,
361
254
    ) {
362
254
        drop(tx.send(Connection {
363
254
            tx: self.connection_tx.clone(),
364
254
            pending_channel: Some(channel.channel.clone()),
365
254
            channel,
366
254
            _permit: permit,
367
254
        }));
368
254
    }
369
370
311
    fn maybe_available_connection(&mut self) {
371
412
        while !self.waiting_connections.is_empty() && 
!self.available_channels.is_empty()110
{
372
101
            let Some(permit) = self.available_connections.clone().try_acquire_owned().ok() else {
373
0
                break;
374
            };
375
101
            let Some(channel) = self.available_channels.pop_front() else {
376
0
                drop(permit);
377
0
                break;
378
            };
379
101
            let Some((reason, tx)) = self.waiting_connections.pop_front() else {
380
0
                self.available_channels.push_front(channel);
381
0
                drop(permit);
382
0
                break;
383
            };
384
101
            debug!(reason, "ConnectionManager: channel available, running");
385
101
            self.provide_channel(channel, tx, permit);
386
        }
387
311
    }
388
389
    // This must never be made async otherwise the select may cancel it.
390
275
    fn handle_connection(&mut self, request: ConnectionRequest) {
391
275
        match request {
392
236
            ConnectionRequest::Dropped(maybe_channel) => {
393
236
                if let Some(
channel209
) = maybe_channel {
394
209
                    self.available_channels.push_back(channel);
395
209
                
}27
396
236
                self.maybe_available_connection();
397
            }
398
39
            ConnectionRequest::Connected(channel) => {
399
39
                self.available_channels.push_back(channel);
400
39
                self.maybe_available_connection();
401
39
            }
402
            // Handle a transport error on a connection by making it unavailable
403
            // for use and establishing a new connection to the endpoint.
404
0
            ConnectionRequest::Error((identifier, was_pending)) => {
405
0
                let should_reconnect = if was_pending {
406
0
                    true
407
                } else {
408
0
                    let original_length = self.available_channels.len();
409
0
                    self.available_channels
410
0
                        .retain(|channel| channel.identifier != identifier);
411
                    // Only reconnect if it wasn't already disconnected.
412
0
                    original_length != self.available_channels.len()
413
                };
414
0
                if should_reconnect {
415
0
                    self.connect_endpoint(identifier.endpoint_index, None);
416
0
                }
417
            }
418
        }
419
275
    }
420
}
421
422
/// An instance of this is obtained for every communication with the gGRPC
423
/// service.  This handles the permit for limiting concurrency, and also
424
/// re-connecting the underlying channel on error.  It depends on users
425
/// reporting all errors.
426
/// NOTE: This should never be cloneable because its lifetime is linked to the
427
///       semaphore permit it carries — `_permit` is released exactly once,
428
///       when the `Connection` drops.
429
#[derive(Debug)]
430
pub struct Connection {
431
    /// Communication with `ConnectionManagerWorker` to inform about transport
432
    /// errors and when the Connection is dropped.
433
    tx: mpsc::UnboundedSender<ConnectionRequest>,
434
    /// If set, the Channel that will be returned to the worker when connection
435
    /// completes (success or failure) or when the Connection is dropped if that
436
    /// happens before connection completes.
437
    pending_channel: Option<Channel>,
438
    /// The identifier to send to `tx`.
439
    channel: EstablishedChannel,
440
    _permit: OwnedSemaphorePermit,
441
}
442
443
impl Drop for Connection {
444
254
    fn drop(&mut self) {
445
254
        let pending_channel = self
446
254
            .pending_channel
447
254
            .take()
448
254
            .map(|channel| EstablishedChannel {
449
215
                channel,
450
215
                identifier: self.channel.identifier,
451
215
            });
452
254
        drop(self.tx.send(ConnectionRequest::Dropped(pending_channel)));
453
254
    }
454
}
455
456
/// A wrapper around the `channel::ResponseFuture` that forwards errors to the `tx`.
457
pub struct ResponseFuture {
458
    /// The wrapped future that actually does the work.
459
    inner: channel::ResponseFuture,
460
    /// Communication with `ConnectionManagerWorker` to inform about transport
461
    /// errors.
462
    connection_tx: mpsc::UnboundedSender<ConnectionRequest>,
463
    /// The identifier to send to `connection_tx` on a transport error.
464
    identifier: ChannelIdentifier,
465
}
466
467
impl core::fmt::Debug for ResponseFuture {
468
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
469
0
        f.debug_struct("ResponseFuture")
470
0
            .field("inner", &self.inner)
471
0
            .field("connection_tx", &self.connection_tx)
472
0
            .field("identifier", &self.identifier)
473
0
            .finish()
474
0
    }
475
}
476
477
/// This is mostly copied from `tonic::transport::channel` except it wraps it
478
/// to allow messaging about connection success and failure.
479
impl tonic::codegen::Service<tonic::codegen::http::Request<tonic::body::Body>> for Connection {
480
    type Response = tonic::codegen::http::Response<tonic::body::Body>;
481
    type Error = tonic::transport::Error;
482
    type Future = ResponseFuture;
483
484
39
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
485
39
        let result = self.channel.channel.poll_ready(cx);
486
39
        if let Poll::Ready(result) = &result {
487
39
            match result {
488
                Ok(()) => {
489
39
                    if let Some(pending_channel) = self.pending_channel.take() {
490
39
                        drop(
491
39
                            self.tx
492
39
                                .send(ConnectionRequest::Connected(EstablishedChannel {
493
39
                                    channel: pending_channel,
494
39
                                    identifier: self.channel.identifier,
495
39
                                })),
496
39
                        );
497
39
                    
}0
498
                }
499
0
                Err(err) => {
500
0
                    debug!(?err, "Error while creating connection on channel");
501
0
                    drop(self.tx.send(ConnectionRequest::Error((
502
0
                        self.channel.identifier,
503
0
                        self.pending_channel.take().is_some(),
504
0
                    ))));
505
                }
506
            }
507
0
        }
508
39
        result
509
39
    }
510
511
39
    fn call(&mut self, request: tonic::codegen::http::Request<tonic::body::Body>) -> Self::Future {
512
39
        ResponseFuture {
513
39
            inner: self.channel.channel.call(request),
514
39
            connection_tx: self.tx.clone(),
515
39
            identifier: self.channel.identifier,
516
39
        }
517
39
    }
518
}
519
520
/// This is mostly copied from `tonic::transport::channel` except it wraps it
521
/// to allow messaging about connection failure.
522
impl Future for ResponseFuture {
523
    type Output =
524
        Result<tonic::codegen::http::Response<tonic::body::Body>, tonic::transport::Error>;
525
526
120
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
527
120
        let result = Pin::new(&mut self.inner).poll(cx);
528
39
        if let Poll::Ready(Err(_)) = &result {
529
0
            drop(
530
0
                self.connection_tx
531
0
                    .send(ConnectionRequest::Error((self.identifier, false))),
532
0
            );
533
120
        }
534
120
        result
535
120
    }
536
}