github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/service/discussion.go (about) 1 // Copyright 2025 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package service 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 11 "github.com/google/syzkaller/syz-cluster/pkg/api" 12 "github.com/google/syzkaller/syz-cluster/pkg/app" 13 "github.com/google/syzkaller/syz-cluster/pkg/db" 14 ) 15 16 // DiscussionService implements the functionality necessary for tracking replies under the bug reports. 17 // Each report is assumed to have an ID and have an InReplyTo ID that either points to another reply or 18 // to the original bug report. 19 // DiscussionService offers the methods to record such replies and, for each reply, to determine the original 20 // discussed bug report. 21 type DiscussionService struct { 22 reportRepo *db.ReportRepository 23 reportReplyRepo *db.ReportReplyRepository 24 } 25 26 func NewDiscussionService(env *app.AppEnvironment) *DiscussionService { 27 return &DiscussionService{ 28 reportRepo: db.NewReportRepository(env.Spanner), 29 reportReplyRepo: db.NewReportReplyRepository(env.Spanner), 30 } 31 } 32 33 func (d *DiscussionService) RecordReply(ctx context.Context, req *api.RecordReplyReq) (*api.RecordReplyResp, error) { 34 reportID, err := d.identifyReport(ctx, req) 35 if err != nil { 36 return nil, err 37 } else if reportID == "" { 38 // We could not find the related report. 39 return &api.RecordReplyResp{}, nil 40 } 41 err = d.reportReplyRepo.Insert(ctx, &db.ReportReply{ 42 ReportID: reportID, 43 MessageID: req.MessageID, 44 Time: req.Time, 45 }) 46 if errors.Is(err, db.ErrReportReplyExists) { 47 return &api.RecordReplyResp{ 48 ReportID: reportID, 49 }, nil 50 } else if err != nil { 51 return nil, fmt.Errorf("failed to save the reply: %w", err) 52 } 53 return &api.RecordReplyResp{ 54 ReportID: reportID, 55 New: true, 56 }, nil 57 } 58 59 func (d *DiscussionService) LastReply(ctx context.Context, reporter string) (*api.LastReplyResp, error) { 60 reply, err := d.reportReplyRepo.LastForReporter(ctx, reporter) 61 if err != nil { 62 return nil, fmt.Errorf("failed to query the last report: %w", err) 63 } 64 if reply != nil { 65 return &api.LastReplyResp{Time: reply.Time}, nil 66 } 67 return &api.LastReplyResp{}, nil 68 } 69 70 func (d *DiscussionService) identifyReport(ctx context.Context, req *api.RecordReplyReq) (string, error) { 71 // If the report ID was passed explicitly, just verify it. 72 if req.ReportID != "" { 73 report, err := d.reportRepo.GetByID(ctx, req.ReportID) 74 if err != nil { 75 return "", fmt.Errorf("failed to query the report: %w", err) 76 } else if report != nil { 77 return report.ID, nil 78 } 79 return "", nil 80 } 81 // Now try to find a matching reply. 82 reportID, err := d.reportReplyRepo.FindParentReportID(ctx, req.Reporter, req.InReplyTo) 83 if err != nil { 84 return "", fmt.Errorf("search among the replies failed: %w", err) 85 } 86 return reportID, nil 87 }