Skip to content
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

ref(projects): Jitter project state requests #277

Merged
merged 4 commits into from
Oct 25, 2019

Conversation

jan-auer
Copy link
Member

This semi-deterministically jitters project state requests so that projects are not all requested in the same interval. This should help to spread out project state requests over the general 5 minute timeout interval.

The jitter is computed from a standard deviated random seed that is assigned to every unique project state. The seed changes with every fetch so that certain projects are not systematically discriminated.

@jan-auer jan-auer requested a review from untitaker October 24, 2019 15:25
@jan-auer jan-auer self-assigned this Oct 24, 2019
Copy link
Member

@untitaker untitaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have questions but this seems about right.

@@ -334,8 +342,13 @@ impl ProjectState {

/// Returns whether this state is outdated and needs to be refetched.
pub fn outdated(&self, config: &Config) -> bool {
let jitter = config.cache_timeout_jitter();
let factor = jitter * (self.seed * 2.0 - self.seed);
debug_assert!(factor >= -1.0 * self.seed && factor <= 1.0 * self.seed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't factor never be negative? Otherwise you get an elapsed timedelta in the negative range.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can we add an assert with less variable deps?

debug_assert!(factor >= 0.0 && factor <= 1.0);

Since both seed and jitter are capped at 1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, was refactoring this at the end and ended up with wrong math. The idea here is to apply the jitter in percent to the interval. So for a 6 minute interval with 50% jitter, the actual interval can be anywhere between 3 and 9 minutes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need to double-check this again tomorrow. Now that I'm looking at this, random() returns a standard deviation, so multiplying with jitter this way will generate skew. I shouldn't code when jet-lagged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine.

The deviation will change from x to x * jitter, but that's what you intended to do.

The distribution is still a standard distribution, just with more spread.

Generally I believe that multiplying your random value with a constant (jitter) is fine. You will create holes in theory, but floats already have holes. You just add more holes.

@@ -277,6 +281,9 @@ pub struct ProjectState {
/// The organization id.
#[serde(default)]
pub organization_id: Option<u64>,
/// Random seed for jitter and related operations.
#[serde(skip, default = "project_state_seed")]
pub seed: f64,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intent that the dice are rolled once per project over the uptime of a relay, not once per fetch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is once per fetch (as stated in the description):

The seed changes with every fetch so that certain projects are not systematically discriminated.

... when Relay has long uptimes.

@@ -334,11 +342,15 @@ impl ProjectState {

/// Returns whether this state is outdated and needs to be refetched.
pub fn outdated(&self, config: &Config) -> bool {
// Jitter is used to compute a random interval between [0; 2 * expiry_interval]
let jitter = config.cache_timeout_jitter();
let factor = jitter * (self.seed * 2.0);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Double check this when less tired. Math is hard.

seed is standard deviated, meaning it centers around 0.5. This makes seed * 2 center around 1.0, but jitter will make it center around something smaller. This actually needs to be something more like:

1 + jitter * (self.seed * 2.0 - 1)

@jan-auer jan-auer requested a review from mitsuhiko October 25, 2019 08:55
@jan-auer
Copy link
Member Author

Implemented a completely new approach suggested by @mitsuhiko, which is closer to how we shift quotas as well. We now shift each project to a deterministic slot within a grid of expiry intervals (e.g. 5 minutes).

Each project state is still requested immediately when it is seen for the first time. Then it is updated within the next 5 minutes at the end of the current window, and then again at each window boundary. This helps to spread out requests while maintaining a deterministic order.

(thinking about it, we can probably refactor some common code for this)

@jan-auer jan-auer merged commit e37cf6b into master Oct 25, 2019
@jan-auer jan-auer deleted the ref/jitter-project-requests branch October 25, 2019 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants