github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/db/report_repo.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 db
     5  
     6  import (
     7  	"context"
     8  	"crypto/rand"
     9  	"encoding/hex"
    10  	"fmt"
    11  
    12  	"cloud.google.com/go/spanner"
    13  )
    14  
    15  type ReportRepository struct {
    16  	client *spanner.Client
    17  	*genericEntityOps[SessionReport, string]
    18  }
    19  
    20  func NewReportRepository(client *spanner.Client) *ReportRepository {
    21  	return &ReportRepository{
    22  		client: client,
    23  		genericEntityOps: &genericEntityOps[SessionReport, string]{
    24  			client:   client,
    25  			keyField: "ID",
    26  			table:    "SessionReports",
    27  		},
    28  	}
    29  }
    30  
    31  func (repo *ReportRepository) Insert(ctx context.Context, rep *SessionReport) error {
    32  	if rep.ID != "" {
    33  		return repo.genericEntityOps.Insert(ctx, rep)
    34  	}
    35  	const attempts = 3
    36  	for i := 0; i < attempts; i++ {
    37  		var err error
    38  		rep.ID, err = randomReportID()
    39  		if err != nil {
    40  			return err
    41  		}
    42  		err = repo.genericEntityOps.Insert(ctx, rep)
    43  		if err == errEntityExists {
    44  			continue
    45  		}
    46  		return err
    47  	}
    48  	// We shouldn't be getting here until we have sent out billions of reports.
    49  	// But let's return some error to still exit gracefully.
    50  	return fmt.Errorf("failed to pick a non-existing report ID")
    51  }
    52  
    53  // nolint: dupl
    54  func (repo *ReportRepository) ListNotReported(ctx context.Context, reporter string,
    55  	limit int) ([]*SessionReport, error) {
    56  	stmt := spanner.Statement{
    57  		SQL: "SELECT * FROM `SessionReports` WHERE `Reporter` = @reporter AND `ReportedAt` IS NULL",
    58  		Params: map[string]interface{}{
    59  			"reporter": reporter,
    60  		},
    61  	}
    62  	addLimit(&stmt, limit)
    63  	return repo.readEntities(ctx, stmt)
    64  }
    65  
    66  // As report ID may be included in the email address, we'd prefer it to be shorter than a typical UUID.
    67  // A 16 byte hex ID should be good enough.
    68  func randomReportID() (string, error) {
    69  	data := make([]byte, 8)
    70  	if _, err := rand.Read(data); err != nil {
    71  		return "", err
    72  	}
    73  	return hex.EncodeToString(data), nil
    74  }