blob: cb5209140d0d0c1edb47925052b7ca4171e848b5 [file]
package jobstore
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"strconv"
"github.com/jackc/pgx/v4"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sql/pool"
"go.skia.org/infra/pinpoint/go/sql/schema"
"go.skia.org/infra/pinpoint/go/workflows"
pinpointpb "go.skia.org/infra/pinpoint/proto/v1"
)
const (
JobStatusIntial = "Pending"
JobType = "Pairwise"
)
type JobStore interface {
// Store intial job parameters to database
AddInitialJob(ctx context.Context, request *pinpointpb.SchedulePairwiseRequest, id string) error
// UpdateJobStatus updates the status of a job.
UpdateJobStatus(ctx context.Context, jobID string, status string, duration int64) error
// Return all elements of a Job
GetJob(ctx context.Context, jobID string) (*schema.JobSchema, error)
// Store final statistics from pairwise job
AddResults(ctx context.Context, jobID string, results map[string]*pinpointpb.PairwiseExecution_WilcoxonResult) error
// Store error received from pairwise execution
SetErrors(ctx context.Context, jobID string, err error) error
// Store results from pairiwse commit runner including CAS references for builds and tests
AddCommitRuns(ctx context.Context, jobID string, left, right *CommitRunData) error
}
// JobStore provides methods to access job data from the database.
type jobStoreImpl struct {
db pool.Pool
}
// NewJobStore creates a new JobStore with the given database connection.
func NewJobStore(db pool.Pool) JobStore {
return &jobStoreImpl{
db: db,
}
}
// CommitRunData contains the build and test run data for a commit.
type CommitRunData struct {
// Build contains the build data for a commit.
Build *workflows.Build
// Runs contains the test run data for a commit.
Runs []*workflows.TestRun
}
func (js *jobStoreImpl) AddInitialJob(ctx context.Context, request *pinpointpb.SchedulePairwiseRequest, id string) error {
if request == nil {
return skerr.Fmt("SchedulePairwiseRequest cannot be nil")
}
additionalParams := make(map[string]string)
if request.StartCommit != nil && request.StartCommit.Main != nil && request.StartCommit.Main.GitHash != "" {
additionalParams["start_commit_githash"] = request.StartCommit.Main.GitHash
}
if request.EndCommit != nil && request.EndCommit.Main != nil && request.EndCommit.Main.GitHash != "" {
additionalParams["end_commit_githash"] = request.EndCommit.Main.GitHash
}
params := []struct {
key string
value string
}{
{"story", request.Story},
{"story_tags", request.StoryTags},
{"initial_attempt_count", request.InitialAttemptCount},
{"aggregation_method", request.AggregationMethod},
{"target", request.Target},
{"project", request.Project},
{"bug_id", request.BugId},
{"chart", request.Chart},
}
for _, p := range params {
if p.value != "" {
additionalParams[p.key] = p.value
}
}
jobName := "default"
submittedBy := "default"
jobError := ""
query := `
INSERT INTO jobs (
job_id,
job_name,
job_status,
job_type,
submitted_by,
benchmark,
bot_name,
additional_request_parameters,
error_message
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
var err error
_, err = js.db.Exec(
ctx,
query,
id,
jobName,
JobStatusIntial,
JobType,
submittedBy,
request.Benchmark,
request.Configuration,
additionalParams,
jobError,
)
if err != nil {
return skerr.Fmt("failed to add job: %w", err)
}
return nil
}
func (js *jobStoreImpl) GetJob(ctx context.Context, jobID string) (*schema.JobSchema, error) {
var job schema.JobSchema
// Construct the SQL SELECT query to retrieve all columns for a given job_id.
query := `SELECT
job_id,
job_name,
job_status,
job_type,
createdat,
submitted_by,
benchmark,
bot_name,
additional_request_parameters,
metric_summary,
error_message
FROM jobs
WHERE job_id = $1`
err := js.db.QueryRow(
ctx,
query,
jobID,
).Scan(
&job.JobID,
&job.JobName,
&job.JobStatus,
&job.JobType,
&job.CreatedDate,
&job.SubmittedBy,
&job.Benchmark,
&job.BotName,
&job.AdditionalRequestParameters,
&job.MetricSummary,
&job.ErrorMessage,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, skerr.Fmt("job with ID %s not found", jobID)
}
return nil, skerr.Fmt("failed to query or scan job with ID %s: %w", jobID, err)
}
return &job, nil
}
func (js *jobStoreImpl) UpdateJobStatus(ctx context.Context, jobID string, status string, workflowDuration int64) error {
// If not positive, then the job is not complete
if workflowDuration <= 0 {
query := `UPDATE jobs SET job_status = $2 WHERE job_id = $1`
_, err := js.db.Exec(ctx, query, jobID, status)
if err != nil {
return skerr.Fmt("failed to update job status for job_id %s: %w", jobID, err)
}
return nil
}
// Update duration parameter
tx, err := js.db.Begin(ctx)
if err != nil {
return skerr.Fmt("failed to begin transaction: %w", err)
}
params, err := js.getAdditionalParams(ctx, jobID, tx)
if err != nil {
return err
}
params["duration"] = strconv.FormatInt(workflowDuration, 10)
query := `
UPDATE jobs SET
job_status = $2,
additional_request_parameters = $3
WHERE job_id = $1
`
if _, err = tx.Exec(ctx, query, jobID, status, params); err != nil {
return skerr.Fmt("failed to update job status and duration for job_id %s: %w", jobID, err)
}
return tx.Commit(ctx)
}
func (js *jobStoreImpl) AddResults(ctx context.Context, jobID string, results map[string]*pinpointpb.PairwiseExecution_WilcoxonResult) error {
query := `UPDATE jobs SET metric_summary = $2 WHERE job_id = $1`
_, err := js.db.Exec(ctx, query, jobID, results)
if err != nil {
return skerr.Fmt("failed to add results for job_id %s: %w", jobID, err)
}
return nil
}
func (js *jobStoreImpl) SetErrors(ctx context.Context, jobID string, err error) error {
query := `UPDATE jobs SET error_message = $2 WHERE job_id = $1`
var errMsg string
if err != nil {
errMsg = err.Error()
}
_, dbErr := js.db.Exec(ctx, query, jobID, errMsg)
if dbErr != nil {
return skerr.Fmt("failed to add error for job_id %s: %w", jobID, dbErr)
}
return nil
}
func (js *jobStoreImpl) AddCommitRuns(ctx context.Context, jobID string, left, right *CommitRunData) error {
// We want to pull additional_request_parameters, combine, then update
tx, err := js.db.Begin(ctx)
if err != nil {
return skerr.Fmt("failed to begin transaction: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
params, err := js.getAdditionalParams(ctx, jobID, tx)
if err != nil {
return err
}
commitRunsData := map[string]any{
"left": left,
"right": right,
}
commitRunsJSON, err := json.Marshal(commitRunsData)
if err != nil {
return skerr.Fmt("failed to marshal commit runs to JSON for job_id %s: %w", jobID, err)
}
params["commit_runs"] = string(commitRunsJSON)
query := `
UPDATE jobs SET
additional_request_parameters = $2
WHERE job_id = $1
`
if _, err = tx.Exec(ctx, query, jobID, params); err != nil {
return skerr.Fmt("failed to update commit runs for job_id %s: %w", jobID, err)
}
// Commit the transaction
return tx.Commit(ctx)
}
// Helper function that retrieves the additional parameters for a given job ID.
func (js *jobStoreImpl) getAdditionalParams(ctx context.Context, jobID string, tx pgx.Tx,
) (map[string]string, error) {
var existingParams []byte
err := tx.QueryRow(ctx,
`SELECT additional_request_parameters FROM jobs WHERE job_id = $1`,
jobID).Scan(&existingParams)
if err != nil {
return nil, skerr.Fmt("failed to query existing params for job %s: %w", jobID, err)
}
var params map[string]string
if err := json.NewDecoder(bytes.NewReader(existingParams)).Decode(&params); err != nil {
return nil, skerr.Fmt("failed to unmarshal existing params for job %s: %w", jobID, err)
}
return params, nil
}