10
10
11
11
from django .conf import settings
12
12
13
- from datetime import datetime
13
+ from datetime import datetime , timezone
14
14
from email .mime .text import MIMEText
15
15
from email .utils import formatdate , make_msgid
16
16
import json
19
19
from pgcommitfest .mailqueue .util import send_mail , send_simple_mail
20
20
from pgcommitfest .userprofile .util import UserWrapper
21
21
22
- from .models import CommitFest , Patch , PatchOnCommitFest , PatchHistory , Committer
22
+ from .models import CommitFest , Patch , PatchOnCommitFest , PatchHistory , Committer , CfbotBranch , CfbotTask
23
23
from .models import MailThread
24
24
from .forms import PatchForm , NewPatchForm , CommentForm , CommitFestFilterForm
25
25
from .forms import BulkEmailForm
@@ -209,11 +209,27 @@ def commitfest(request, cfid):
209
209
210
210
# Let's not overload the poor django ORM
211
211
curs = connection .cursor ()
212
- curs .execute ("""SELECT p.id, p.name, poc.status, v.version AS targetversion, p.created, p.modified, p.lastmail, committer.username AS committer, t.topic,
212
+ curs .execute ("""
213
+ SELECT p.id, p.name, poc.status, v.version AS targetversion, p.created, p.modified, p.lastmail, committer.username AS committer, t.topic,
213
214
(poc.status=ANY(%(openstatuses)s)) AS is_open,
214
215
(SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_authors cpa ON cpa.user_id=auth_user.id WHERE cpa.patch_id=p.id) AS author_names,
215
216
(SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_reviewers cpr ON cpr.user_id=auth_user.id WHERE cpr.patch_id=p.id) AS reviewer_names,
216
- (SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs
217
+ (SELECT count(1) FROM commitfest_patchoncommitfest pcf WHERE pcf.patch_id=p.id) AS num_cfs,
218
+ (
219
+ SELECT row_to_json(t) as cfbot_results
220
+ from (
221
+ SELECT
222
+ sum((task.status = 'COMPLETED')::int) as completed,
223
+ sum((task.status in ('CREATED', 'SCHEDULED', 'EXECUTING'))::int) running,
224
+ sum((task.status in ('ABORTED', 'ERRORED', 'FAILED'))::int) failed,
225
+ string_agg(task.task_name, ', ') FILTER (WHERE task.status in ('ABORTED', 'ERRORED', 'FAILED')) as failed_task_names,
226
+ branch.commit_id IS NULL as needs_rebase
227
+ FROM commitfest_cfbotbranch branch
228
+ LEFT JOIN commitfest_cfbottask task ON task.branch_id = branch.branch_id
229
+ WHERE branch.patch_id=p.id
230
+ GROUP BY branch.commit_id
231
+ ) t
232
+ )
217
233
FROM commitfest_patch p
218
234
INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id
219
235
INNER JOIN commitfest_topic t ON t.id=p.topic_id
@@ -311,6 +327,12 @@ def patch(request, cfid, patchid):
311
327
patch_commitfests = PatchOnCommitFest .objects .select_related ('commitfest' ).filter (patch = patch ).order_by ('-commitfest__startdate' )
312
328
committers = Committer .objects .filter (active = True ).order_by ('user__last_name' , 'user__first_name' )
313
329
330
+ try :
331
+ cfbot_branch = patch .cfbot_branch
332
+ except CfbotBranch .DoesNotExist :
333
+ cfbot_branch = None
334
+ cfbot_tasks = patch .cfbot_tasks .order_by ('position' ) if cfbot_branch else []
335
+
314
336
# XXX: this creates a session, so find a smarter way. Probably handle
315
337
# it in the callback and just ask the user then?
316
338
if request .user .is_authenticated :
@@ -333,6 +355,8 @@ def patch(request, cfid, patchid):
333
355
'cf' : cf ,
334
356
'patch' : patch ,
335
357
'patch_commitfests' : patch_commitfests ,
358
+ 'cfbot_branch' : cfbot_branch ,
359
+ 'cfbot_tasks' : cfbot_tasks ,
336
360
'is_committer' : is_committer ,
337
361
'is_this_committer' : is_this_committer ,
338
362
'is_reviewer' : is_reviewer ,
@@ -788,6 +812,97 @@ def _user_and_mail(u):
788
812
})
789
813
790
814
815
+ @transaction .atomic
816
+ def cfbot_ingest (message ):
817
+ """Ingest a single message status update message receive from cfbot. It
818
+ should be a Python dictionary, decoded from JSON already."""
819
+
820
+ cursor = connection .cursor ()
821
+
822
+ # Every message should have a shared_secret, and it should match.
823
+ if message ["shared_secret" ] != settings .CFBOT_SECRET :
824
+ raise Exception ("Invalid shared_secret from CFbot" )
825
+
826
+ branch_status = message ["branch_status" ]
827
+ patch_id = branch_status ["submission_id" ]
828
+ branch_id = branch_status ["branch_id" ]
829
+ created = datetime .fromisoformat (branch_status ["created" ]).replace (tzinfo = timezone .utc )
830
+
831
+ try :
832
+ Patch .objects .get (pk = patch_id )
833
+ except Patch .DoesNotExist :
834
+ # If the patch doesn't exist, there's nothing to do. This should never
835
+ # happen in production, but on the test system it's possible because
836
+ # not it doesn't contain the newest patches that the CFBot knows about.
837
+ return
838
+
839
+ old_branch = CfbotBranch .objects .select_for_update ().filter (patch_id = patch_id ).first ()
840
+ if old_branch and old_branch .branch_id != branch_id and old_branch .created .replace (tzinfo = timezone .utc ) > created :
841
+ # This is a message for an old branch, ignore it.
842
+ return
843
+
844
+ # Every message should have a branch_status, which we will INSERT
845
+ # or UPDATE. We do this first, because cfbot_task refers to it.
846
+ cursor .execute ("""INSERT INTO commitfest_cfbotbranch (patch_id, branch_id,
847
+ branch_name, commit_id,
848
+ apply_url, status,
849
+ created, modified)
850
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
851
+ ON CONFLICT (patch_id) DO UPDATE
852
+ SET status = EXCLUDED.status,
853
+ modified = EXCLUDED.modified,
854
+ branch_id = EXCLUDED.branch_id,
855
+ branch_name = EXCLUDED.branch_name,
856
+ commit_id = EXCLUDED.commit_id,
857
+ apply_url = EXCLUDED.apply_url,
858
+ created = EXCLUDED.created
859
+ WHERE commitfest_cfbotbranch.modified < EXCLUDED.modified
860
+ """ ,
861
+ (patch_id ,
862
+ branch_id ,
863
+ branch_status ["branch_name" ],
864
+ branch_status ["commit_id" ],
865
+ branch_status ["apply_url" ],
866
+ branch_status ["status" ],
867
+ branch_status ["created" ],
868
+ branch_status ["modified" ]))
869
+
870
+ # Most messages have a task_status. It might be missing in rare cases, like
871
+ # when cfbot decides that a whole branch has timed out. We INSERT or
872
+ # UPDATE.
873
+ if "task_status" in message :
874
+ task_status = message ["task_status" ]
875
+ cursor .execute ("""INSERT INTO commitfest_cfbottask (id, task_name, patch_id, branch_id,
876
+ position, status,
877
+ created, modified)
878
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
879
+ ON CONFLICT (id) DO UPDATE
880
+ SET status = EXCLUDED.status,
881
+ modified = EXCLUDED.modified
882
+ WHERE commitfest_cfbottask.modified < EXCLUDED.modified""" ,
883
+ (task_status ["task_id" ],
884
+ task_status ["task_name" ],
885
+ patch_id ,
886
+ branch_id ,
887
+ task_status ["position" ],
888
+ task_status ["status" ],
889
+ task_status ["created" ],
890
+ task_status ["modified" ]))
891
+
892
+ cursor .execute ("DELETE FROM commitfest_cfbottask WHERE patch_id=%s AND branch_id != %s" , (patch_id , branch_id ))
893
+
894
+ @csrf_exempt
895
+ def cfbot_notify (request ):
896
+ if request .method != 'POST' :
897
+ return HttpResponseForbidden ("Invalid method" )
898
+
899
+ j = json .loads (request .body )
900
+ if j ['shared_secret' ] != settings .CFBOT_SECRET :
901
+ return HttpResponseForbidden ("Invalid API key" )
902
+
903
+ cfbot_ingest (j )
904
+ return HttpResponse (status = 200 )
905
+
791
906
@csrf_exempt
792
907
def thread_notify (request ):
793
908
if request .method != 'POST' :
0 commit comments