-
Notifications
You must be signed in to change notification settings - Fork 30
Synchorize pr state periodically #276
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
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.
Thank you! Left a few comments. I'll take a look at refactoring the refresh handlers, to see if it makes sense.
src/github/api/client.rs
Outdated
@@ -319,6 +320,27 @@ impl GithubRepositoryClient { | |||
run_ids.map(|workflow_id| self.get_workflow_url(workflow_id)) | |||
} | |||
|
|||
pub async fn fetch_prs(&self) -> anyhow::Result<Vec<PullRequest>> { |
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 will download all PRs in the repo, which is more than 80 thousand PRs on rust-lang/rust. We should only download the open/draft (if it's possible to filter for both in one API call) PRs.
Then when we find a PR that is not amongst the open ones, we assume that it is closed (in theory it could also be merged, but bors does the merging, so it should not miss that event).
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 could also happen that open and close webhook of a pr is lost and therefore this pr has no trace of it in db. Also while refreshing pr state we will only download open/draft prs, so still there is no trace of it in the db.
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.
That's fine. If we miss some PR that was opened and then closed while bors wasn't running, it doesn't really matter, we don't care about closed PRs. We are only interested in open PRs waiting to be merged.
These situations should be quite rare anyway.
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 need some help in implementation of this logic. So we have two vectors nonclosed_gh_pull_request
and nonclosed_db_pull_requests
.
For prs that are in nonclosed_gh_pull_request
and not in nonclosed_db_pull_requests
. I have to upsert prs in the db.
For prs that are in nonclosed_db_pull_request
and not in nonclosed_gh_pull_requests
. I have to mark their state close in db.
I was trying to implement this using BTreeset
and use set difference. Is this fine or can I do better.
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.
You implemented the logic well, thank you! :)
src/bin/bors.rs
Outdated
@@ -18,6 +18,8 @@ use tracing_subscriber::filter::EnvFilter; | |||
|
|||
/// How often should the bot check DB state, e.g. for handling timeouts. | |||
const PERIODIC_REFRESH: Duration = Duration::from_secs(120); | |||
/// How often should the bot synchronize PR state. | |||
const PR_STATE_PERIODIC_REFRESH: Duration = Duration::from_secs(60 * 60); |
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 not sure if we need to separate this from the normal periodic refresh, because PRs are also per repository. So it's weird to have a separate event for it.
That being said, it should have a different update frequency than the refresh of configs and permissions. We could have a single outside trigger for the refresh logic, that would "tick" e.g. every minute, and the refresh code could then remember the most recent timestamps for the individual refresh logic handlers. But this would be a bit annoying in the tests, as we'd need to override the period between refreshes, rather than just triggering the refresh with a mocked event. Or maybe we could do something like Event::RefreshPrs { force: bool }
to enable us to force the refresh in tests.
I will split the refresh handlers into individual events in a separate PR, and then we'll rebase this one on top of that.
src/tests/mocks/bors.rs
Outdated
@@ -447,22 +472,25 @@ impl BorsTester { | |||
&mut self, | |||
repo_name: GithubRepoName, | |||
pr_number: u64, | |||
send_webhook: bool, |
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 should this ever be false
?
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.
Will fix test refresh_pr_with_status_closed
in which a pr's status is closed in gh but not in db. So false
here is req for that 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.
Then let's just modify the mock GH state in the test directly, to avoid having to pass true/false everywhere.
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.
Can you elaborate more on this? Which specific struct in test you are talking about?
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 mean to either copy-paste the logic for creating a PR in the mock GH database into the test, or creating a separate function that will only create the PR on GH, but not send the webhook. It's quite hard to read the false, false
etc. parameters to open_pr
in tests.
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.
But sometimes in tests I need only open webhook to work.
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.
let pr = tester.open_pr(default_repo_name(), false).await?;
tester.block_webhooks(async move {
tester
.close_pr(default_repo_name(), pr.number.0)
.await
}).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.
But this would require BorsTester
to be cloned.
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, right, this was a bit tricky until recently. Luckily, we now finally have async closures! :) I pushed an implementation of with_blocked_webhooks
.
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.
Thanks for the help. I didn't knew about async closures and now will look into it.
src/bors/handlers/refresh.rs
Outdated
let prs = repo.client.fetch_prs().await?; | ||
let pr_to_status = db.get_all_pull_request_status(repo_name).await?; | ||
|
||
for pr in prs { |
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.
As noted in my other comment, and in the original issue, we cannot simply download all PRs from GitHub, there's too many. We only have to fetch the open ones and synchronize smartly based on that.
I split the handlers in #278. |
} | ||
// PRs that are closed in GitHub but not in the DB. In theory PR could also be merged | ||
// but bors does the merging so it should not happen. | ||
for pr_num in nonclosed_db_prs_num.keys() { |
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 is a potential race condition here, where if the PR just got merged on GH in between the GH API and DB calls, we would mark it as closed even though it was merged. But let's not deal with that now.
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 the PR just got merged b/w GH API and DB calls, then GH would send a webhook which will be processed after it changing the status to merged.
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 was thinking something like this:
Bors merges a PR. Before setting its state to merged, this refresh handler runs and loads the PR as open from GH. Then bors sets the local state as merged, but it gets overwritten by this refresh handler. This is possible because the refresh handler can run concurrently with command handlers.
That being said, this is incredibly unlikely to happen, and even if we did, the state should be correctly synchronized during the next refresh, or during the next comment sent on the PR, so it should be fine.
src/bors/handlers/refresh.rs
Outdated
@@ -139,6 +208,15 @@ mod tests { | |||
.await; | |||
} | |||
|
|||
#[sqlx::test] | |||
async fn refresh_without_out_of_sync_prs(pool: sqlx::PgPool) { |
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.
refresh_pr_state
src/tests/mocks/bors.rs
Outdated
@@ -447,22 +472,25 @@ impl BorsTester { | |||
&mut self, | |||
repo_name: GithubRepoName, | |||
pr_number: u64, | |||
send_webhook: bool, |
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 mean to either copy-paste the logic for creating a PR in the mock GH database into the test, or creating a separate function that will only create the PR on GH, but not send the webhook. It's quite hard to read the false, false
etc. parameters to open_pr
in tests.
src/tests/mocks/bors.rs
Outdated
@@ -447,22 +472,25 @@ impl BorsTester { | |||
&mut self, | |||
repo_name: GithubRepoName, | |||
pr_number: u64, | |||
send_webhook: bool, |
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.
Actually, I think that we could solve this in a more general way, by creating something like this:
tester.block_webhooks(async move {
// While this future runs, no webhooks will be sent
});
src/github/api/client.rs
Outdated
@@ -319,6 +320,27 @@ impl GithubRepositoryClient { | |||
run_ids.map(|workflow_id| self.get_workflow_url(workflow_id)) | |||
} | |||
|
|||
pub async fn fetch_prs(&self) -> anyhow::Result<Vec<PullRequest>> { |
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.
You implemented the logic well, thank you! :)
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.
Thanks! Left one nit, once you remove the send_webhook
parameter, please squash the commits and we can merge this.
src/bors/handlers/pr_events.rs
Outdated
@@ -466,7 +466,9 @@ mod tests { | |||
pr.pr_status == PullRequestStatus::Open | |||
}) | |||
.await?; | |||
tester.merge_pr(default_repo_name(), pr.number.0).await?; | |||
tester | |||
.merge_pr(default_repo_name(), pr.number.0, true) |
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 missed this function, let's also remove send_webhook: bool,
from its parameters, now that we have a better way of blocking webhooks.
} | ||
// PRs that are closed in GitHub but not in the DB. In theory PR could also be merged | ||
// but bors does the merging so it should not happen. | ||
for pr_num in nonclosed_db_prs_num.keys() { |
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 was thinking something like this:
Bors merges a PR. Before setting its state to merged, this refresh handler runs and loads the PR as open from GH. Then bors sets the local state as merged, but it gets overwritten by this refresh handler. This is possible because the refresh handler can run concurrently with command handlers.
That being said, this is incredibly unlikely to happen, and even if we did, the state should be correctly synchronized during the next refresh, or during the next comment sent on the PR, so it should be fine.
27e8902
to
97350ba
Compare
cargo sqlx prepare Add `with_blocked_webhooks` remove send_webhook flag
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.
Thank you! :)
Closes #266.