@@ -15,6 +15,8 @@ import {
1515 HackParserIntegrationEnabled ,
1616 listHackParserCodeKeys ,
1717} from '@/services/HackParserService' ;
18+ import { reviewLockManager } from '@utils/reviewLockManager' ;
19+ import { lockedExecute } from '@utils/lockedExecute' ;
1820
1921export const acceptReviewRequest = {
2022 app : undefined as unknown as App ,
@@ -36,72 +38,81 @@ export const acceptReviewRequest = {
3638 throw new Error ( 'No message exists on body - unable to accept review' ) ;
3739 }
3840
39- log . d ( 'acceptReviewRequest.handleAccept' , `${ user . name } accepted review ${ threadId } ` ) ;
40-
41- // Quick check: If user already responded, ignore duplicate clicks
42- const existingReview = await activeReviewRepo . getReviewByThreadIdOrFail ( threadId ) ;
43- const isPending = existingReview . pendingReviewers . some ( r => r . userId === user . id ) ;
44-
45- if ( ! isPending ) {
46- log . d (
47- 'acceptReviewRequest.handleAccept' ,
48- `User ${ user . id } already responded to review ${ threadId } , ignoring duplicate click` ,
49- ) ;
50- return ;
51- }
41+ const messageTimestamp = body . message . ts ;
5242
53- // Try to add user to accepted reviewers - this will throw if they're not in pending list
54- // (race condition protection in case of simultaneous clicks)
55- try {
56- await addUserToAcceptedReviewers ( user . id , threadId ) ;
57- } catch ( err ) {
58- log . d (
59- 'acceptReviewRequest.handleAccept' ,
60- `User ${ user . id } already responded to review ${ threadId } (race condition), ignoring duplicate click` ,
61- ) ;
62- return ;
63- }
43+ log . d ( 'acceptReviewRequest.handleAccept' , `${ user . name } accepted review ${ threadId } ` ) ;
6444
65- // remove accept/decline buttons from original message and update it
66- const blocks = blockUtils . removeBlock ( body , BlockId . REVIEWER_DM_BUTTONS ) ;
67- blocks . push ( textBlock ( 'You accepted this review.' ) ) ;
45+ // Use a per-threadId lock to prevent race conditions when multiple users accept simultaneously
46+ await lockedExecute ( reviewLockManager . getLock ( threadId ) , async ( ) => {
47+ // Quick check: If user already responded, ignore duplicate clicks
48+ const existingReview = await activeReviewRepo . getReviewByThreadIdOrFail ( threadId ) ;
49+ const isPending = existingReview . pendingReviewers . some ( r => r . userId === user . id ) ;
50+
51+ if ( ! isPending ) {
52+ log . d (
53+ 'acceptReviewRequest.handleAccept' ,
54+ `User ${ user . id } already responded to review ${ threadId } , ignoring duplicate click` ,
55+ ) ;
56+ return ;
57+ }
6858
69- // if HackParser integration is enabled, add link to the PDF and any code results that are found
70- if ( HackParserIntegrationEnabled ( ) ) {
59+ // Try to add user to accepted reviewers - this will throw if they're not in pending list
60+ // (race condition protection in case of simultaneous clicks)
7161 try {
72- const review = await activeReviewRepo . getReviewByThreadIdOrFail ( threadId ) ;
73- if ( review . pdfIdentifier ) {
74- const url = await generateHackParserPresignedURL ( review . pdfIdentifier ) ;
75- blocks . push ( textBlock ( `HackerRank PDF: <${ url } |${ review . pdfIdentifier } >` ) ) ;
76-
77- const codeKeys = await listHackParserCodeKeys ( review . pdfIdentifier ) ;
78- if ( codeKeys . length ) {
79- blocks . push ( textBlock ( `Code results from above PDF via HackParser:` ) ) ;
80- for ( const key of codeKeys ) {
81- blocks . push (
82- textBlock (
83- ` • <${ await generateHackParserPresignedURL ( key ) } |${ key . split ( '/' ) . slice ( 1 ) . join ( '/' ) } >` ,
84- ) ,
85- ) ;
62+ await addUserToAcceptedReviewers ( user . id , threadId ) ;
63+ } catch ( err ) {
64+ log . d (
65+ 'acceptReviewRequest.handleAccept' ,
66+ `User ${ user . id } already responded to review ${ threadId } (race condition), ignoring duplicate click` ,
67+ ) ;
68+ return ;
69+ }
70+
71+ // remove accept/decline buttons from original message and update it
72+ const blocks = blockUtils . removeBlock ( body , BlockId . REVIEWER_DM_BUTTONS ) ;
73+ blocks . push ( textBlock ( 'You accepted this review.' ) ) ;
74+
75+ // if HackParser integration is enabled, add link to the PDF and any code results that are found
76+ if ( HackParserIntegrationEnabled ( ) ) {
77+ try {
78+ const review = await activeReviewRepo . getReviewByThreadIdOrFail ( threadId ) ;
79+ if ( review . pdfIdentifier ) {
80+ const url = await generateHackParserPresignedURL ( review . pdfIdentifier ) ;
81+ blocks . push ( textBlock ( `HackerRank PDF: <${ url } |${ review . pdfIdentifier } >` ) ) ;
82+
83+ const codeKeys = await listHackParserCodeKeys ( review . pdfIdentifier ) ;
84+ if ( codeKeys . length ) {
85+ blocks . push ( textBlock ( `Code results from above PDF via HackParser:` ) ) ;
86+ for ( const key of codeKeys ) {
87+ blocks . push (
88+ textBlock (
89+ ` • <${ await generateHackParserPresignedURL ( key ) } |${ key . split ( '/' ) . slice ( 1 ) . join ( '/' ) } >` ,
90+ ) ,
91+ ) ;
92+ }
8693 }
8794 }
95+ } catch ( err ) {
96+ log . e (
97+ 'acceptReviewRequest.handleAccept' ,
98+ 'Error generating HackParser text blocks' ,
99+ err ,
100+ ) ;
88101 }
89- } catch ( err ) {
90- log . e ( 'acceptReviewRequest.handleAccept' , 'Error generating HackParser text blocks' , err ) ;
91102 }
92- }
93103
94- await chatService . updateDirectMessage ( client , user . id , body . message . ts , blocks ) ;
104+ await chatService . updateDirectMessage ( client , user . id , messageTimestamp , blocks ) ;
95105
96- await userRepo . markNowAsLastReviewedDate ( user . id ) ;
106+ await userRepo . markNowAsLastReviewedDate ( user . id ) ;
97107
98- await chatService . replyToReviewThread (
99- client ,
100- threadId ,
101- `${ mention ( user ) } has agreed to review this submission.` ,
102- ) ;
108+ await chatService . replyToReviewThread (
109+ client ,
110+ threadId ,
111+ `${ mention ( user ) } has agreed to review this submission.` ,
112+ ) ;
103113
104- await reviewCloser . closeReviewIfComplete ( this . app , threadId ) ;
114+ await reviewCloser . closeReviewIfComplete ( this . app , threadId ) ;
115+ } ) ;
105116
106117 // eslint-disable-next-line @typescript-eslint/no-explicit-any
107118 } catch ( err : any ) {
0 commit comments