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  }