-
-
Notifications
You must be signed in to change notification settings - Fork 643
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
Add serverset #6921
Add serverset #6921
Conversation
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.
This is large: will review before end of day. One quick comment.
} | ||
|
||
#[derive(Clone, Copy, Debug)] | ||
pub struct BackoffConfig { |
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.
Could/should we use https://docs.rs/tokio-retry/0.2.0/tokio_retry/ here?
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.
There's a comment about avoiding futures here because fork safety; when we're out of our fork world, I will gladly rewrite this to use tokio_retry
:)
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.
This looks good to me, I won't approve because I don't have much experience with multithreading and thus I can't be confident that this is correct. But it looks nice!
src/rust/engine/serverset/src/lib.rs
Outdated
if let Some(ref mut unhealthy_info) = *unhealthy_info { | ||
unhealthy_info.unhealthy_since = Instant::now(); | ||
// failure_backoff_ratio's numer and denom both fit in u8s, so hopefully this won't | ||
// overflow of lose too much precision... |
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.
s/of/or/
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.
Done
src/rust/engine/serverset/src/lib.rs
Outdated
// overflow of lose too much precision... | ||
unhealthy_info.next_attempt_after *= *self.inner.failure_backoff_ratio.numer(); | ||
unhealthy_info.next_attempt_after /= *self.inner.failure_backoff_ratio.denom(); | ||
unhealthy_info.next_attempt_after = std::cmp::min( |
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.
This series of assignments seems odd to me, why not calculate the correct value and then assign it once? As far as I can tell we won't have any write race conditions because we lock unhealty_info, we won't have read RCs because self.inner
is atomic, and we won't have read-after-write hazards on self.inner.failure_backoff_ratio
because it doesn't change except for initialization. And even if it did change, the correct behavior would presumably to make a thread-local copy first to read from.
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.
Done
src/rust/engine/serverset/src/lib.rs
Outdated
// failure_backoff_ratio's numer and denom both fit in u8s, so hopefully this won't | ||
// overflow of lose too much precision... | ||
unhealthy_info.next_attempt_after *= *self.inner.failure_backoff_ratio.denom(); | ||
unhealthy_info.next_attempt_after /= *self.inner.failure_backoff_ratio.numer(); |
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.
Same as above
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.
Done
src/rust/engine/serverset/src/lib.rs
Outdated
break (i, server); | ||
}; | ||
|
||
let serverset: Serverset<T> = (*self).clone(); |
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.
How expensive is this clone? It looks like not very much, because it's essentially a copy of an Arc
, but just checking.
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, just an Arc
, super cheap :)
src/rust/engine/serverset/src/lib.rs
Outdated
assert_eq!(expect, saw); | ||
} | ||
|
||
fn mark(s: &Serverset<&'static str>, health: Health) { |
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.
Stringly typed servers, I like it! (I really do, it's a cute way to test)
src/rust/engine/serverset/src/lib.rs
Outdated
/// last known status. | ||
/// | ||
/// If all resources are unhealthy, this function will block the calling thread until the backoff | ||
/// period has completed. We'd probably prefer to use some Future-based scheduling, but that |
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.
Maybe add a TODO for the Future? (a little, unfunny pun intended)
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.
Done
#[derive(Debug)] | ||
struct Backend<T> { | ||
server: T, | ||
unhealthy_info: Arc<Mutex<Option<UnhealthyInfo>>>, |
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.
Would the interface improve if we made this an Option<Mutex<Arc<>>>
? Here is a solution for the Scoping problem.
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.
The Option
is specifically the thing we may set/unset under the lock, so it needs to be this way around :)
pub fn next(&self) -> (T, Box<Fn(Health) + Send + Sync>) { | ||
let (i, server) = loop { | ||
let i = self.inner.next.fetch_add(1, Ordering::Relaxed) % self.inner.servers.len(); | ||
let server = &self.inner.servers[i]; |
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 assume that inner.servers
only gets mutated at creation and the privateness of that field guarantees that it doesn't get mutated elsewhere. If that is the case, this is okay. Otherwise, there could be a disconnect between i
and the actual vector.
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.
Still maybe worth throwing an RWLock, if reading has little overhead.
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.
In fact, the fact that we Arc
up the Inner
means that the Vec
cannot ever be mutated, because there's no way for anyone to get a mutable reference to it (unless they are the only holder of the Arc
, in which case they would need to unwrap it, which proves that no one else could have any dangling callbacks) :) Yay rust!
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.
See the note about busywaiting. But I like that this is separated out as an independent crate!
src/rust/engine/serverset/src/lib.rs
Outdated
max_lame, | ||
} = backoff_config; | ||
|
||
if backoff_ratio < 1.0 { |
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.
Is it worth giving BackoffConfig
its own fn new
to perform these calculations/validation, and then having this method be infallible? The advantage would be that it would be useful to give Inner
an instance of BackoffConfig
rather than namespacing the fields with failure_
.
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.
Done
src/rust/engine/serverset/src/lib.rs
Outdated
/// unhealthy. | ||
/// | ||
/// The inverse is used when easing back in after health recovery. | ||
pub backoff_ratio: f64, |
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.
Given that we think we'll be switching to a library for the backoff implementation, using ratios here seems like it might be overkill. Would assuming a 2x ratio (or having a configurable integer ratio) be reasonable until then?
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.
After futzing with Futures, I'm pretty sure we don't want to switch to a library for the backoff implementation. None of the existing ones I could find support ease in as well as backoff, and the code is now pretty well encapsulated. I'd be happy to split off a separate backoff crate, if you'd like.
src/rust/engine/serverset/src/lib.rs
Outdated
/// would require this type to be Resettable because of our fork model, which would be very | ||
/// complex. | ||
/// | ||
/// TODO: Switch to use tokio_retry when we don't need to worry about forking without execing. |
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.
On this topic: we already guarantee that all threads/pools/connections/etc are shut down or dropped before forking... I believe that because tokio_retry
does not create threads (instead, it uses the Timer
facility provided per-thread by tokio_runtime), it should be safe to use tokio_retry
. One thing we'd need to ensure though is that we're interacting with the Timer
from within the runtime's threads (ie, not on the io-pool
... we should probably kill the io-pool
).
If you think otherwise though, #6818 will need an update.
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.
Updated to use futures :)
src/rust/engine/serverset/src/lib.rs
Outdated
|
||
let serverset: Serverset<T> = (*self).clone(); | ||
|
||
let callback = Box::new(move |health: Health| serverset.callback(i, health)); |
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.
Given that we have to trust the caller to call the callback anyway, maybe giving them an opaque token to explicitly call serverset.callback
with would be a bit more efficient (would avoid the allocation of the callback function).
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.
Done
src/rust/engine/serverset/src/lib.rs
Outdated
} | ||
|
||
fn multiply(duration: Duration, fraction: num_rational::Ratio<u32>) -> Duration { | ||
(duration * *fraction.numer()) / *fraction.denom() |
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.
Dividing first might be a better defense against overflow? Worse for precision though.
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.
Switched to just use floats everywhere. Which makes me pretty sad, but added a link to rust-lang/rust#54361
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 assumed that the reason for the ratio was to allow for test determinism? Or are things "close enough" with floats for that not to matter?
let i = self.inner.next.fetch_add(1, Ordering::Relaxed) % self.inner.servers.len(); | ||
let server = &self.inner.servers[i]; | ||
let unhealthy_info = server.unhealthy_info.lock(); | ||
if let Some(ref unhealthy_info) = *unhealthy_info { |
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 all hosts are unhealthy, I think that this is going to busywait. In order to not busywait, you'd need to make the result of this method optional. And if the result were optional, your caller would still probably want to lean on a Timer
.
So... I feel like incorporating timer usage (either via tokio_retry
or directly via tokio_timer
) is likely more ergonomic. But giving this a Result<_, Duration>
to retry at would be an alternative.
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.
Switched to use a timer.
Specifically not using tokio_retry
here because the actual retries should be driven by the caller; what we want is to lame underlying servers here (with their own exponential backoff), and have the caller immediately retry without backoff.
Something we could do is have each server return a Future
for when it's going to be usable (weighting between multiple healthy servers, and having unhealthy servers give a (jittered) delay) until they're healthy, and have a select
around them, which we give to the caller, but that feels like a lot more stuff being scheduled and dropped. Maybe I'm overestimating how expensive kicking off and selecting between futures is, though...
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.
Either explicitly passing in a futures_timer::Timer
or switching to tokio_timer
will likely be necessary: otherwise, looks good.
src/rust/engine/serverset/src/lib.rs
Outdated
let (index, instant) = earliest_future.unwrap(); | ||
let server = self.inner.servers[index].server.clone(); | ||
// Note that Delay::new_at(time in the past) gets immediately scheduled. | ||
Delay::new_at(instant) |
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.
So, futures_timer
does have a dedicated thread, which means it isn't fork safe out of the box. To use it, you'd need to construct and provide the Timer
reference manually (as described there), and ensure that it is shut down before forking.
An advantage to using tokio_timer
(I think) is that it will "magically" use thread-local references to find the runtime it is running on, so you wouldn't need to pass in an explicit Runtime or Timer instance. The downside, of course, is that that is magical... you could fall into the trap I mentioned above with io-pool
and see a panic due to it not being explicit.
src/rust/engine/serverset/src/lib.rs
Outdated
.to_boxed() | ||
} | ||
|
||
pub fn callback(&self, callback_token: CallbackToken, health: Health) { |
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.
nit: Now that these are actually exposed, giving them purposeful names would be good... report_health
and HealthReportToken
or something maybe?
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, could also mark the token "#[must_use]".
fb9e442
to
48905b0
Compare
This round-robins between a number of backends, skipping any which are observed to be bad, with expontential backoff and ease-in. This will be used in remote process execution for CAS operations.
48905b0
to
a60e644
Compare
This round-robins between a number of backends, skipping any which are
observed to be bad, with exponential backoff and ease-in.
This will be used in remote process execution for CAS operations.