Skip to content

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

Merged
merged 1 commit into from
May 5, 2025
Merged

Conversation

geetanshjuneja
Copy link
Contributor

Closes #266.

Copy link
Member

@Kobzol Kobzol left a 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.

@@ -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>> {
Copy link
Member

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).

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 2, 2025

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.

Copy link
Member

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.

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 2, 2025

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.

Copy link
Member

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);
Copy link
Member

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.

@@ -447,22 +472,25 @@ impl BorsTester {
&mut self,
repo_name: GithubRepoName,
pr_number: u64,
send_webhook: bool,
Copy link
Member

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?

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 2, 2025

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@Kobzol Kobzol May 5, 2025

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?;

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 5, 2025

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.

Copy link
Member

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.

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 5, 2025

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.

let prs = repo.client.fetch_prs().await?;
let pr_to_status = db.get_all_pull_request_status(repo_name).await?;

for pr in prs {
Copy link
Member

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.

@Kobzol
Copy link
Member

Kobzol commented Apr 30, 2025

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() {
Copy link
Member

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.

Copy link
Contributor Author

@geetanshjuneja geetanshjuneja May 5, 2025

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.

Copy link
Member

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.

@@ -139,6 +208,15 @@ mod tests {
.await;
}

#[sqlx::test]
async fn refresh_without_out_of_sync_prs(pool: sqlx::PgPool) {
Copy link
Member

Choose a reason for hiding this comment

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

refresh_pr_state

@@ -447,22 +472,25 @@ impl BorsTester {
&mut self,
repo_name: GithubRepoName,
pr_number: u64,
send_webhook: bool,
Copy link
Member

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.

@@ -447,22 +472,25 @@ impl BorsTester {
&mut self,
repo_name: GithubRepoName,
pr_number: u64,
send_webhook: bool,
Copy link
Member

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
});

@@ -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>> {
Copy link
Member

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! :)

Copy link
Member

@Kobzol Kobzol left a 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.

@@ -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)
Copy link
Member

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() {
Copy link
Member

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.

@geetanshjuneja geetanshjuneja force-pushed the sync branch 2 times, most recently from 27e8902 to 97350ba Compare May 5, 2025 11:35
cargo sqlx prepare

Add `with_blocked_webhooks`

remove send_webhook flag
Copy link
Member

@Kobzol Kobzol left a comment

Choose a reason for hiding this comment

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

Thank you! :)

@Kobzol Kobzol added this pull request to the merge queue May 5, 2025
Merged via the queue into rust-lang:main with commit 48c30e3 May 5, 2025
2 checks passed
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.

Synchronize state after (re)start and periodically
2 participants