-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement client-side timeouts #329
Conversation
19eff12
to
b287ef2
Compare
Tested manually by setting up a program with very low client timeout and observing the orphan count rise up to the threshold. |
scylla/src/transport/connection.rs
Outdated
AllocStreamId { | ||
stream_id_sender: oneshot::Sender<i16>, | ||
response_handler: ResponseHandler, | ||
}, | ||
// Send a request to the server | ||
Request { | ||
stream_id: i16, | ||
serialized_request: SerializedRequest, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those should be a single message. Consider a situation in which the future returned from send_request
sends AllocStreamId
, then is dropped while waiting for the stream ID and does not send a Request
message. The stream ID will not be freed in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, right, makes sense to reduce the back-and-forth anyway. And all it takes is to add the stream_id_sender to Request
. Will do in v2
3cfb15a
to
6b4051f
Compare
v2:
|
@@ -61,9 +61,17 @@ pub struct Connection { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the commit message: I believe there are 32k streams available on a connection, not 16k.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but negative ids are reserved for events iirc, so users can de facto use 16k
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, the pool is 64k and 32k are for the user, you're right
scylla/src/transport/connection.rs
Outdated
if let Some(id) = hmap.allocate(response_handler) { | ||
stream_id_sender | ||
.send(id) | ||
.map_err(|_| QueryError::UnableToAllocStreamId)?; | ||
id | ||
} else { | ||
error!("Unable to allocate stream id"); | ||
return Err(QueryError::UnableToAllocStreamId); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an error occurs here, it will be returned from the writer
function, which will close the whole connection and will propagate this error to everybody who waits for a response on this connection. Previously, if we couldn't allocate a stream ID for a request, only the one who waits for this request got an error (because the Sender half of the response channel was dropped and the Receiver gets an error). Is that an intended change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was intended, but we can discuss whether it's too harsh or not
"Too many orphaned stream ids: {}, dropping connection", | ||
hmap.orphan_count() | ||
); | ||
return Err(QueryError::TooManyOrphanedStreamIds(hmap.orphan_count())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit worried that immediately closing a connection in case of too many orphaned IDs may unnecessarily cause availability issues. What if all connections start timing out requests at the same rate and we decide to close all of them for that reason at the same time?
scylla/src/transport/session_test.rs
Outdated
// Spontaneous errors are expected when running with a client timeout set to 0 seconds. | ||
// If they happen, the test case is assumed to be correct |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that it would be nice for client-side timeouts to be configurable per-query, not per-session. Latency requirements may be different for different queries - in particular, setting the timeout too low may cause some of the internal queries not to work correctly, e.g. topology refresh. Also, if we did that, this test could be written differently so that it only expects failure from the query with explicitly set client-side timeout.
We could have a separate internal timeout for queries (maybe the connection timeout could be used?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, I'll add a way to define a client timeout per query
req.set_stream(stream_id); | ||
write_half.write_all(req.get_data()).await?; | ||
Task::Orphan { stream_id } => { | ||
let mut hmap = handler_map.try_lock().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, we probably can remove the entry from handler_map
corresponding to the orphaned stream ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? The id is orphaned, but a response to it can still arrive, so once it does, it will remove the entry as well. And if it doesn't and eventually we hit max orphan count, the connection dies anway. Or do you mean that only as an optimization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mainly meant it as an optimization - the memory used by the entry can be freed at this point because the contents of the entry won't be used (it's a Sender of a oneshot channel which won't be read from).
6b4051f
to
8a32ff9
Compare
The configuration option will be used to apply client-side timeouts for requests.
which indicates, that the query timed out client-side, but it can still be processed by Scylla for all we know.
Client-side timeout is now applied to requests - if a request takes longer to receive a response than the specified timeout, it will be reported as an error.
The error fires when a threshold of orphaned stream ids, i.e. the ones on which the client stopped waiting, but no response came from the server, is reached.
8a32ff9
to
b993a7c
Compare
v3:
|
The fact that the whole connection is killed now in the event of not enough stream ids is still not addressed, but after some consideration I decided to keep the original semantics. We should have better handling of no stream ids anyway, but there's no point in severing a connection which could still have ongoing requests just because somebody pushed one too many new requests in there. |
When a client-side timeout fires, the driver stops waiting for a response for a particular stream id. We cannot however release this stream id to the pool, because the response may still arrive late. Such abandonded stream ids are tracked separately, and once a connection has too many of them, it breaks and a new connection should be established instead. The current threshold is hardcoded to 1024 (out of ~32k stream ids total).
The number of acceptable orphaned stream ids is now configurable. After the specified limit is hit, the connection will be killed.
It's now possible to specify the client-side timeout independently for each query.
The test case checks if client-side timeout fires when its time passes.
1. a typo 2. a statement about unlimited concurrency was a bit misleading
A short description of client-side timeouts is added to the docs.
b993a7c
to
d453980
Compare
v4:
|
review ping |
scylla/src/transport/connection.rs
Outdated
.map_err(|e| { | ||
QueryError::ClientTimeout(format!( | ||
.map_err(|_| QueryError::UnableToAllocStreamId)?; | ||
let received = match tokio::time::timeout(self.config.client_timeout, receiver).await { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Library user can decide to stop polling future returned by Session::query*
method, causing this line to be last line executed. In this case no Task::Orphan
message will be send via self.submit_channel
.
I think that speculative execution can also cancel futures, so the problem persists even without user intentionally canceling futures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true, but it's also no worse than what we have now - the connection would eventually go out of stream ids and die. Users should be encouraged to use our timeout instead of tokio timeout. Do you have any suggestions on how to approach this issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One idea that comes to my mind would be to make the router
responsible for detecting timeouts and marking stream IDs as orphaned. It would keep a BTreeMap
-based queue of stream IDs ordered by their deadlines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more for a different solution.
The problem in current solution is that we are not able to deliver notification about orphanage to the router
, due to future cancellation.
To ensure, that some action is run in an async function, even when a function's future was dropped, one can use a destructor of an object living in the body of this function. I propose introducing such object to the send_request
function, to use it for notifying router
about orphanage.
Due to destructors not being async, notifying via bounded channel as it was done now is not an option. Luckily, unbounded tokio channels do not require awaiting to send some value, so they can be used in rust's drop implementations.
struct OrphanhoodNotifier {
disabled: bool, // initialized to false
stream_id: i16,
sender: UnboundedSender<i16>,
}
impl OrphanhoodNotifier {
fn disable(mut self) {
self.disabled = true;
}
}
impl Drop for OrphanhoodNotifier {
fn drop(&mut self) {
if (!self.disabled) {
self.sender.send(self.stream_id);
}
}
}
I imagine, that the OrphanhoodNotifier
would be sent instead of raw stream_id
using stream_id_sender
(as this would protect us from not notifying about orphanage, when send_request
is canceled before receiving stream_id
from stream_id_receiver
but after it submitted Task::Request
). After receival of query response, orphan notification would be disabled by calling .disable()
method of OrphanhoodNotifier
.
Such design would also require changes in router
's way of allocating stream_id
s and marking stream_id
s as orphaned.
Why? Let's consider a situation, when:
- A request is sent by
send_request
. Let's call this invocation ofsend_request
"the first invocation". - The request sent by the first invocation gets a
stream_id = 1
. - Connection's
router
executes this request and, after receiving response,stream_id = 1
goes back to pool. - Next, another request is sent by another invocation of
send_request
(let's call it "the second invocation"). - The second request also gets a
stream_id = 1
, because it was already available in a pool. - Now, if a late orphanage notification somehow arrives from the first invocation of
send_request
, it would mark the wrong request (request currently havingstream_id = 1
sent by thesend_request
second invocation).
One way to get rid of this problem, is to track generations of stream_ids.
struct OrphanhoodNotifier {
disabled: bool, // initialized to false
stream_id: i16,
generation: u64, // new field denoting the stream_id's generation number
sender: UnboundedSender<i16>,
}
On the router side, an associative array containing mapping stream_id
-> current generation number
would be kept. When a new stream_id is allocated, it's generation number inside the associative array is incremented. If a orphanhood notification is received, there should be check if the generations match (the one in notification and other in associative array). After check succeeds, stream_id
can be considered as orphaned. If the check does not succeed, we can be sure, that the router already received the response, and the stream_id
was reused.
I believe this design is better than keeping BTreeMap queue of stream IDs, because it allows to handle timeouts by future cancellation (and therefore allows us to implement timeouts without knowing the timeout duration in advance). Being able to handle cancellations is crucial as the implementation of speculative execution heavily depends on them (by using select!
).
Noble cause, but the implementation is severely outdated. Closing this one, we'll get it rewritten. |
Pre-review checklist
Fixes:
annotations to PR description.This pull request introduces client-side timeouts and a management layer for orphaned stream ids. Orphaned stream ids are the ones for which the client-side timeout already fired, but no response came from the server yet. In such scenario, a stream id cannot be released yet, because the response associated with the same id may still be received later. Instead, orphaned stream ids are tracked per connection, and once a threshold is reached, the connection is killed.
Fixes #304
Fixes #305