fn obtain_circuit_or_continuation_info<D: MockableConnectorData>(
connector: &HsClientConnector<impl Runtime, D>,
netdir: &Arc<NetDir>,
hsid: &HsId,
secret_keys: &HsClientSecretKeys,
table_index: TableIndex,
rechecks: &mut impl Iterator,
guard: MutexGuard<'_, Services<D>>,
) -> Result<Either<(Arc<Mutex<Option<ConnError>>>, Receiver), Arc<D::DataTunnel>>, ConnError>Expand description
Obtain a circuit from the Services table, or return a continuation
This is the workhorse function for get_or_launch_connection.
get_or_launch_connection, together with obtain_circuit_or_continuation_info,
form a condition variable loop:
We check to see if we have a circuit. If so, we return it. Otherwise, we make sure that a circuit is being constructed, and then go into a condvar wait; we’ll be signaled when the construction completes.
So the connection task we spawn does not return the circuit, or error, via an inter-task stream. It stores it in the data structure and wakes up all the client tasks. (This means there is only one success path for the client task code.)
There are some wrinkles:
§Existence of this as a separate function
The usual structure for a condition variable loop would be something like this:
loop {
test state and maybe break;
cv.wait(guard).await; // consumes guard, unlocking after enqueueing us as a waiter
guard = lock();
}However, Rust does not currently understand that the mutex is not
actually a captured variable held across an await point,
when the variable is consumed before the await, and re-stored afterwards.
As a result, the async future becomes erroneously !Send:
https://github.com/rust-lang/rust/issues/104883.
We want the unstable feature -Zdrop-tracking:
https://github.com/rust-lang/rust/issues/97331.
Instead, to convince the compiler, we must use a scope-based drop of the mutex guard. That means converting the “test state and maybe break” part into a sub-function. That’s what this function is.
It returns Right if the loop should be exited, returning the circuit to the caller.
It returns Left if the loop needs to do a condition variable wait.
§We’re using a barrier as a condition variable
We want to be signaled when the task exits. Indeed, only when it exits.
This functionality is most conveniently in a postage::barrier.
§Nested loops
Sometimes we want to go round again without unlocking. Sometimes we must unlock and wait and relock.
The drop tracking workaround (see above) means we have to do these two
in separate scopes.
So there are two nested loops: one here, and one in get_or_launch_connection.
They both use the same backstop rechecks counter.