77import io .sentry .Sentry ;
88import java .io .IOException ;
99import java .time .Duration ;
10- import java .time .Instant ;
1110import java .util .List ;
1211import java .util .concurrent .CompletableFuture ;
1312import java .util .concurrent .atomic .AtomicBoolean ;
1413import net .dv8tion .jda .api .JDA ;
1514import net .dv8tion .jda .api .entities .User ;
1615import net .dv8tion .jda .api .entities .channel .concrete .ThreadChannel ;
17- import net .dv8tion .jda .api .entities .channel .forums .ForumTagSnowflake ;
1816import org .slf4j .Logger ;
1917import 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