Skip to content

Commit b1778f4

Browse files
authored
GH-241 harden review module flows and repair mention/reminder consistency (#241)
* fix: harden review module flows and repair mention/reminder consistency * Follow review. * fix(review-reminders): avoid immediate follow-up reminder after reviewer assignment * feat(review-forum): Auto-remove merger or closed pullrequest from review forum.
1 parent c97649c commit b1778f4

15 files changed

+792
-174
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
.gradle
55
build/
6-
database/
6+
/database/
77
data/
88
logs/
99
config/

src/main/java/com/eternalcode/discordapp/feature/review/GitHubPullRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
public class GitHubPullRequest {
99

1010
private static final Pattern GITHUB_PULL_REQUEST_REGEX =
11-
Pattern.compile("^https://github\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)/pull/([0-9]+)$");
11+
Pattern.compile("^https://github\\.com/([a-zA-Z0-9_.-]+)/([a-zA-Z0-9_.-]+)/pull/([0-9]+)$");
1212

1313
private static final String GITHUB_PULL_REQUEST_URL = "https://github.com/%s/%s/pull/%s";
1414
private static final String GITHUB_PULL_REQUEST_API_URL = "https://api.github.com/repos/%s/%s/pulls/%d";

src/main/java/com/eternalcode/discordapp/feature/review/GitHubReviewReminderService.java

Lines changed: 103 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
import io.sentry.Sentry;
88
import java.io.IOException;
99
import java.time.Duration;
10-
import java.time.Instant;
1110
import java.util.List;
1211
import java.util.concurrent.CompletableFuture;
1312
import java.util.concurrent.atomic.AtomicBoolean;
1413
import net.dv8tion.jda.api.JDA;
1514
import net.dv8tion.jda.api.entities.User;
1615
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
17-
import net.dv8tion.jda.api.entities.channel.forums.ForumTagSnowflake;
1816
import org.slf4j.Logger;
1917
import org.slf4j.LoggerFactory;
2018

@@ -28,7 +26,6 @@ public class GitHubReviewReminderService {
2826
private final Duration reminderInterval;
2927
private final AppConfig appConfig;
3028
private final AtomicBoolean isRunning = new AtomicBoolean(false);
31-
private volatile Instant lastGitHubApiCall = Instant.MIN;
3229

3330
public GitHubReviewReminderService(
3431
JDA jda,
@@ -98,11 +95,14 @@ private CompletableFuture<Void> processReminders(List<GitHubReviewMentionReposit
9895

9996
LOGGER.info("Processing " + reminders.size() + " reminders");
10097

101-
List<CompletableFuture<Void>> reminderFutures = reminders.stream()
102-
.map(this::sendReminderAsync)
103-
.toList();
98+
CompletableFuture<Void> chain = CompletableFuture.completedFuture(null);
99+
for (GitHubReviewMentionRepository.ReviewerReminder reminder : reminders) {
100+
chain = chain
101+
.thenCompose(unused -> this.sendReminder(reminder))
102+
.thenCompose(unused -> this.delay(GITHUB_API_RATE_LIMIT));
103+
}
104104

105-
return CompletableFuture.allOf(reminderFutures.toArray(new CompletableFuture[0]))
105+
return chain
106106
.thenRun(() -> LOGGER.info("Completed processing all reminders"))
107107
.exceptionally(throwable -> {
108108
Sentry.captureException(throwable);
@@ -111,128 +111,130 @@ private CompletableFuture<Void> processReminders(List<GitHubReviewMentionReposit
111111
});
112112
}
113113

114-
private CompletableFuture<Void> sendReminderAsync(GitHubReviewMentionRepository.ReviewerReminder reminder) {
115-
return CompletableFuture.runAsync(() -> this.scheduler.schedule(
116-
() -> {
117-
try {
118-
this.sendReminder(reminder);
119-
}
120-
catch (Exception exception) {
121-
Sentry.captureException(exception);
122-
LOGGER.error("Error sending individual reminder", exception);
123-
}
124-
}, this.calculateDelayForRateLimit()));
114+
private CompletableFuture<Void> delay(Duration delay) {
115+
CompletableFuture<Void> future = new CompletableFuture<>();
116+
this.scheduler.schedule(() -> future.complete(null), delay);
117+
return future;
125118
}
126119

127-
private Duration calculateDelayForRateLimit() {
128-
Instant now = Instant.now();
129-
Instant nextAllowed = this.lastGitHubApiCall.plus(GITHUB_API_RATE_LIMIT);
130-
131-
if (now.isBefore(nextAllowed)) {
132-
return Duration.between(now, nextAllowed);
133-
}
134-
135-
return Duration.ZERO;
136-
}
137-
138-
private void sendReminder(GitHubReviewMentionRepository.ReviewerReminder reminder) {
139-
if (!isRunning.get()) {
140-
return;
120+
private CompletableFuture<Void> sendReminder(GitHubReviewMentionRepository.ReviewerReminder reminder) {
121+
if (!this.isRunning.get()) {
122+
return CompletableFuture.completedFuture(null);
141123
}
142124

143125
String reviewUrl = reminder.pullRequestUrl();
144126
GitHubPullRequest pullRequest = GitHubPullRequest.fromUrl(reviewUrl).orNull();
145127
if (pullRequest == null) {
146128
LOGGER.warn("Invalid pull request URL: {}", reviewUrl);
147-
return;
129+
return CompletableFuture.completedFuture(null);
148130
}
149131

150-
lastGitHubApiCall = Instant.now();
151-
152132
try {
153-
boolean isMerged = GitHubReviewUtil.isPullRequestMerged(pullRequest, appConfig.githubToken);
133+
GitHubReviewUtil.PullRequestState pullRequestState =
134+
GitHubReviewUtil.getPullRequestState(pullRequest, this.appConfig.githubToken);
154135

155-
scheduler.schedule(
156-
() -> {
157-
try {
158-
boolean isClosed = GitHubReviewUtil.isPullRequestClosed(pullRequest, appConfig.githubToken);
159-
handlePRStatusCheck(reminder, pullRequest, isMerged, isClosed);
160-
}
161-
catch (IOException exception) {
162-
Sentry.captureException(exception);
163-
LOGGER.error("Error checking PR closed status: {}", reviewUrl, exception);
164-
}
165-
}, GITHUB_API_RATE_LIMIT);
136+
return this.handlePRStatusCheck(reminder, pullRequest, pullRequestState.merged(), pullRequestState.closed());
166137
}
167138
catch (IOException exception) {
168139
Sentry.captureException(exception);
169140
LOGGER.error("Error checking PR merged status: {}", reviewUrl, exception);
141+
return CompletableFuture.completedFuture(null);
170142
}
171143
}
172144

173-
private void handlePRStatusCheck(
145+
private CompletableFuture<Void> handlePRStatusCheck(
174146
GitHubReviewMentionRepository.ReviewerReminder reminder,
175147
GitHubPullRequest pullRequest, boolean isMerged, boolean isClosed) {
176148
if (isMerged || isClosed) {
177-
this.handleClosedOrMergedPR(reminder.threadId(), isMerged);
149+
this.handleClosedOrMergedPR(reminder.threadId(), pullRequest, isMerged);
178150
LOGGER.info(
179151
"PR is {}, skipping reminder for thread: {}",
180152
isMerged ? "merged" : "closed",
181153
reminder.threadId());
182-
return;
154+
return CompletableFuture.completedFuture(null);
183155
}
184156

185157
String githubUsername = this.findGithubUsernameByDiscordId(reminder.userId());
186158
if (githubUsername == null) {
187159
LOGGER.warn("Could not find GitHub username for Discord userId {}", reminder.userId());
188-
return;
160+
return CompletableFuture.completedFuture(null);
161+
}
162+
163+
List<String> requestedReviewers = GitHubReviewUtil.getReviewers(pullRequest, this.appConfig.githubToken);
164+
if (!this.isStillRequestedReviewer(requestedReviewers, githubUsername)) {
165+
LOGGER.info(
166+
"Skipping reminder for {} on PR {} because user is no longer in requested reviewers",
167+
githubUsername,
168+
pullRequest.toUrl()
169+
);
170+
return CompletableFuture.completedFuture(null);
189171
}
190172

191-
this.scheduler.schedule(
192-
() -> {
193-
try {
194-
boolean alreadyReviewed =
195-
GitHubReviewUtil.hasUserReviewed(pullRequest, this.appConfig.githubToken, githubUsername);
196-
if (alreadyReviewed) {
197-
LOGGER.info("User " + githubUsername + " already reviewed PR, skipping reminder.");
173+
try {
174+
boolean alreadyReviewed =
175+
GitHubReviewUtil.hasUserReviewed(pullRequest, this.appConfig.githubToken, githubUsername);
176+
if (alreadyReviewed) {
177+
LOGGER.info("User {} already reviewed PR, skipping reminder.", githubUsername);
178+
return CompletableFuture.completedFuture(null);
179+
}
180+
181+
CompletableFuture<Void> reminderFuture = new CompletableFuture<>();
182+
this.jda.retrieveUserById(reminder.userId()).queue(
183+
user -> this.handleUserRetrieved(
184+
user,
185+
reminder.threadId(),
186+
reminder.pullRequestUrl(),
187+
pullRequest
188+
).whenComplete((unused, throwable) -> {
189+
if (throwable != null) {
190+
reminderFuture.completeExceptionally(throwable);
198191
return;
199192
}
200-
201-
this.jda.retrieveUserById(reminder.userId()).queue(
202-
user -> this.handleUserRetrieved(
203-
user,
204-
reminder.threadId(),
205-
reminder.pullRequestUrl(),
206-
pullRequest),
207-
throwable -> {
208-
Sentry.captureException(throwable);
209-
LOGGER.error("Error retrieving user: {}", reminder.userId(), throwable);
210-
}
211-
);
212-
}
213-
catch (Exception exception) {
214-
Sentry.captureException(exception);
215-
LOGGER.warn("Error checking if user reviewed PR: {}", reminder.pullRequestUrl(), exception);
193+
reminderFuture.complete(null);
194+
}),
195+
throwable -> {
196+
Sentry.captureException(throwable);
197+
LOGGER.error("Error retrieving user: {}", reminder.userId(), throwable);
198+
reminderFuture.completeExceptionally(throwable);
216199
}
217-
}, GITHUB_API_RATE_LIMIT);
200+
);
201+
return reminderFuture.exceptionally(throwable -> null);
202+
}
203+
catch (Exception exception) {
204+
Sentry.captureException(exception);
205+
LOGGER.warn("Error checking if user reviewed PR: {}", reminder.pullRequestUrl(), exception);
206+
return CompletableFuture.completedFuture(null);
207+
}
218208
}
219209

220-
private void handleClosedOrMergedPR(long threadId, boolean isMerged) {
210+
private boolean isStillRequestedReviewer(List<String> requestedReviewers, String githubUsername) {
211+
if (githubUsername == null || githubUsername.trim().isEmpty()) {
212+
return false;
213+
}
214+
215+
if (requestedReviewers == null || requestedReviewers.isEmpty()) {
216+
return false;
217+
}
218+
219+
return requestedReviewers.stream()
220+
.anyMatch(requestedReviewer -> requestedReviewer != null &&
221+
requestedReviewer.equalsIgnoreCase(githubUsername));
222+
}
223+
224+
private void handleClosedOrMergedPR(long threadId, GitHubPullRequest pullRequest, boolean isMerged) {
225+
GitHubReviewStatus status = isMerged ? GitHubReviewStatus.MERGED : GitHubReviewStatus.CLOSED;
226+
this.mentionRepository.updateReviewStatus(pullRequest, status)
227+
.exceptionally(FutureHandler::handleException);
228+
221229
ThreadChannel thread = this.jda.getThreadChannelById(threadId);
222230
if (thread != null) {
223-
AppConfig.ReviewSystem reviewSystem = this.appConfig.reviewSystem;
224-
long tagId = isMerged ? reviewSystem.mergedTagId : reviewSystem.closedTagId;
225-
226-
thread.getManager()
227-
.setAppliedTags(ForumTagSnowflake.fromId(tagId))
228-
.setLocked(true)
229-
.setArchived(true)
231+
thread.delete()
230232
.queue(
231233
success -> LOGGER.info(
232-
"Successfully archived {} thread: {}",
234+
"Successfully deleted {} thread: {}",
233235
isMerged ? "merged" : "closed",
234236
threadId),
235-
failure -> LOGGER.warn("Failed to archive thread: {}", threadId, failure)
237+
failure -> LOGGER.warn("Failed to delete thread: {}", threadId, failure)
236238
);
237239
}
238240
}
@@ -245,22 +247,24 @@ private String findGithubUsernameByDiscordId(long userId) {
245247
.orElse(null);
246248
}
247249

248-
private void handleUserRetrieved(User user, long threadId, String pullRequestUrl, GitHubPullRequest pullRequest) {
250+
private CompletableFuture<Void> handleUserRetrieved(
251+
User user, long threadId, String pullRequestUrl, GitHubPullRequest pullRequest
252+
) {
249253
if (user == null) {
250254
LOGGER.warn("User is null for thread: {}", threadId);
251-
return;
255+
return CompletableFuture.completedFuture(null);
252256
}
253257

254258
ThreadChannel thread = this.jda.getThreadChannelById(threadId);
255259
if (thread == null) {
256260
LOGGER.warn("Could not find thread with ID {}", threadId);
257-
return;
261+
return CompletableFuture.completedFuture(null);
258262
}
259263

260-
this.sendReminderMessage(user, thread, pullRequestUrl, pullRequest);
264+
return this.sendReminderMessage(user, thread, pullRequestUrl, pullRequest);
261265
}
262266

263-
private void sendReminderMessage(
267+
private CompletableFuture<Void> sendReminderMessage(
264268
User user,
265269
ThreadChannel thread,
266270
String pullRequestUrl,
@@ -271,17 +275,25 @@ private void sendReminderMessage(
271275
pullRequestUrl
272276
);
273277

278+
CompletableFuture<Void> messageFuture = new CompletableFuture<>();
274279
thread.sendMessage(message).queue(
275280
success -> {
276281
LOGGER.info("Reminder sent to {} for PR: {}", user.getName(), pullRequestUrl);
277282
this.mentionRepository.recordReminderSent(pullRequest, user.getIdLong())
278-
.exceptionally(FutureHandler::handleException);
283+
.thenRun(() -> messageFuture.complete(null))
284+
.exceptionally(throwable -> {
285+
FutureHandler.handleException(throwable);
286+
messageFuture.complete(null);
287+
return null;
288+
});
279289
},
280290
throwable -> {
281291
Sentry.captureException(throwable);
282292
LOGGER.error("Error sending reminder message to {}", user.getName(), throwable);
293+
messageFuture.completeExceptionally(throwable);
283294
}
284295
);
296+
return messageFuture.exceptionally(throwable -> null);
285297
}
286298
}
287299

0 commit comments

Comments
 (0)