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,28 @@ 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
+ count(*) FILTER (WHERE task.status = 'COMPLETED') as completed,
223
+ count(*) FILTER (WHERE task.status in ('CREATED', 'SCHEDULED', 'EXECUTING')) running,
224
+ count(*) FILTER (WHERE task.status in ('ABORTED', 'ERRORED', 'FAILED')) failed,
225
+ count(*) total,
226
+ string_agg(task.task_name, ', ') FILTER (WHERE task.status in ('ABORTED', 'ERRORED', 'FAILED')) as failed_task_names,
227
+ branch.commit_id IS NULL as needs_rebase
228
+ FROM commitfest_cfbotbranch branch
229
+ LEFT JOIN commitfest_cfbottask task ON task.branch_id = branch.branch_id
230
+ WHERE branch.patch_id=p.id
231
+ GROUP BY branch.commit_id
232
+ ) t
233
+ )
217
234
FROM commitfest_patch p
218
235
INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id
219
236
INNER JOIN commitfest_topic t ON t.id=p.topic_id
@@ -311,6 +328,12 @@ def patch(request, cfid, patchid):
311
328
patch_commitfests = PatchOnCommitFest .objects .select_related ('commitfest' ).filter (patch = patch ).order_by ('-commitfest__startdate' )
312
329
committers = Committer .objects .filter (active = True ).order_by ('user__last_name' , 'user__first_name' )
313
330
331
+ try :
332
+ cfbot_branch = patch .cfbot_branch
333
+ except CfbotBranch .DoesNotExist :
334
+ cfbot_branch = None
335
+ cfbot_tasks = patch .cfbot_tasks .order_by ('position' ) if cfbot_branch else []
336
+
314
337
# XXX: this creates a session, so find a smarter way. Probably handle
315
338
# it in the callback and just ask the user then?
316
339
if request .user .is_authenticated :
@@ -333,6 +356,8 @@ def patch(request, cfid, patchid):
333
356
'cf' : cf ,
334
357
'patch' : patch ,
335
358
'patch_commitfests' : patch_commitfests ,
359
+ 'cfbot_branch' : cfbot_branch ,
360
+ 'cfbot_tasks' : cfbot_tasks ,
336
361
'is_committer' : is_committer ,
337
362
'is_this_committer' : is_this_committer ,
338
363
'is_reviewer' : is_reviewer ,
@@ -788,6 +813,97 @@ def _user_and_mail(u):
788
813
})
789
814
790
815
816
+ @transaction .atomic
817
+ def cfbot_ingest (message ):
818
+ """Ingest a single message status update message receive from cfbot. It
819
+ should be a Python dictionary, decoded from JSON already."""
820
+
821
+ cursor = connection .cursor ()
822
+
823
+ # Every message should have a shared_secret, and it should match.
824
+ if message ["shared_secret" ] != settings .CFBOT_SECRET :
825
+ raise Exception ("Invalid shared_secret from CFbot" )
826
+
827
+ branch_status = message ["branch_status" ]
828
+ patch_id = branch_status ["submission_id" ]
829
+ branch_id = branch_status ["branch_id" ]
830
+ created = datetime .fromisoformat (branch_status ["created" ]).replace (tzinfo = timezone .utc )
831
+
832
+ try :
833
+ Patch .objects .get (pk = patch_id )
834
+ except Patch .DoesNotExist :
835
+ # If the patch doesn't exist, there's nothing to do. This should never
836
+ # happen in production, but on the test system it's possible because
837
+ # not it doesn't contain the newest patches that the CFBot knows about.
838
+ return
839
+
840
+ old_branch = CfbotBranch .objects .select_for_update ().filter (patch_id = patch_id ).first ()
841
+ if old_branch and old_branch .branch_id != branch_id and old_branch .created .replace (tzinfo = timezone .utc ) > created :
842
+ # This is a message for an old branch, ignore it.
843
+ return
844
+
845
+ # Every message should have a branch_status, which we will INSERT
846
+ # or UPDATE. We do this first, because cfbot_task refers to it.
847
+ cursor .execute ("""INSERT INTO commitfest_cfbotbranch (patch_id, branch_id,
848
+ branch_name, commit_id,
849
+ apply_url, status,
850
+ created, modified)
851
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
852
+ ON CONFLICT (patch_id) DO UPDATE
853
+ SET status = EXCLUDED.status,
854
+ modified = EXCLUDED.modified,
855
+ branch_id = EXCLUDED.branch_id,
856
+ branch_name = EXCLUDED.branch_name,
857
+ commit_id = EXCLUDED.commit_id,
858
+ apply_url = EXCLUDED.apply_url,
859
+ created = EXCLUDED.created
860
+ WHERE commitfest_cfbotbranch.modified < EXCLUDED.modified
861
+ """ ,
862
+ (patch_id ,
863
+ branch_id ,
864
+ branch_status ["branch_name" ],
865
+ branch_status ["commit_id" ],
866
+ branch_status ["apply_url" ],
867
+ branch_status ["status" ],
868
+ branch_status ["created" ],
869
+ branch_status ["modified" ]))
870
+
871
+ # Most messages have a task_status. It might be missing in rare cases, like
872
+ # when cfbot decides that a whole branch has timed out. We INSERT or
873
+ # UPDATE.
874
+ if "task_status" in message :
875
+ task_status = message ["task_status" ]
876
+ cursor .execute ("""INSERT INTO commitfest_cfbottask (id, task_name, patch_id, branch_id,
877
+ position, status,
878
+ created, modified)
879
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
880
+ ON CONFLICT (id) DO UPDATE
881
+ SET status = EXCLUDED.status,
882
+ modified = EXCLUDED.modified
883
+ WHERE commitfest_cfbottask.modified < EXCLUDED.modified""" ,
884
+ (task_status ["task_id" ],
885
+ task_status ["task_name" ],
886
+ patch_id ,
887
+ branch_id ,
888
+ task_status ["position" ],
889
+ task_status ["status" ],
890
+ task_status ["created" ],
891
+ task_status ["modified" ]))
892
+
893
+ cursor .execute ("DELETE FROM commitfest_cfbottask WHERE patch_id=%s AND branch_id != %s" , (patch_id , branch_id ))
894
+
895
+ @csrf_exempt
896
+ def cfbot_notify (request ):
897
+ if request .method != 'POST' :
898
+ return HttpResponseForbidden ("Invalid method" )
899
+
900
+ j = json .loads (request .body )
901
+ if j ['shared_secret' ] != settings .CFBOT_SECRET :
902
+ return HttpResponseForbidden ("Invalid API key" )
903
+
904
+ cfbot_ingest (j )
905
+ return HttpResponse (status = 200 )
906
+
791
907
@csrf_exempt
792
908
def thread_notify (request ):
793
909
if request .method != 'POST' :
0 commit comments