154 lines
4.6 KiB
Go
154 lines
4.6 KiB
Go
package usecase
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/domain"
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/reducer"
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/workloads"
|
|
)
|
|
|
|
// ReduceJob turns completed coordinator-owned partial artifacts into one final
|
|
// artifact. It performs no worker I/O and never trusts a worker URI or path.
|
|
type ReduceJob struct {
|
|
jobs JobRepository
|
|
tasks TaskRepository
|
|
artifacts ArtifactRepository
|
|
blobs BlobStore
|
|
tx TxManager
|
|
clock Clock
|
|
catalog *workloads.Catalog
|
|
}
|
|
|
|
func NewReduceJob(jobs JobRepository, tasks TaskRepository, artifacts ArtifactRepository,
|
|
blobs BlobStore, tx TxManager, clock Clock, catalog *workloads.Catalog) *ReduceJob {
|
|
return &ReduceJob{jobs: jobs, tasks: tasks, artifacts: artifacts, blobs: blobs, tx: tx, clock: clock, catalog: catalog}
|
|
}
|
|
|
|
// Execute is idempotent for jobs that are not currently reducing. The worker
|
|
// completion path may call it after every result; only the last task changes a
|
|
// similarity-search job into reducing state.
|
|
func (uc *ReduceJob) Execute(ctx context.Context, jobID uuid.UUID) error {
|
|
claimed, err := uc.jobs.ClaimReduction(ctx, jobID, uc.clock.Now())
|
|
if err != nil || !claimed {
|
|
return err
|
|
}
|
|
job, err := uc.jobs.Get(ctx, jobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if job.Status != domain.JobReducing {
|
|
return nil
|
|
}
|
|
if uc.catalog == nil {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
reduction := uc.catalog.Reduction(job.Workload)
|
|
if reduction == "" {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
|
|
completed, err := uc.tasks.ListCompleted(ctx, jobID)
|
|
if err != nil {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
if len(completed) == 0 {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
readers := make([]io.Reader, 0, len(completed))
|
|
closers := make([]io.Closer, 0, len(completed))
|
|
for _, task := range completed {
|
|
if task.ResultArtifactID == nil {
|
|
closeAll(closers)
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
artifact, err := uc.artifacts.Get(ctx, *task.ResultArtifactID)
|
|
if err != nil || artifact.JobID != jobID || artifact.TaskID == nil || *artifact.TaskID != task.ID || artifact.Kind != domain.ArtifactPartialResult {
|
|
closeAll(closers)
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
body, err := uc.blobs.Open(ctx, artifact.StorageKey)
|
|
if err != nil {
|
|
closeAll(closers)
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
readers = append(readers, body)
|
|
closers = append(closers, body)
|
|
}
|
|
output, reduceErr := reducePartials(reduction, readers, job.Parameters)
|
|
closeAll(closers)
|
|
if reduceErr != nil {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
|
|
final, err := domain.NewArtifact(jobID, nil, domain.ArtifactFinalResult, job.Workload+".csv", "text/csv", uc.clock.Now())
|
|
if err != nil {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
sum, size, err := uc.blobs.Put(ctx, final.StorageKey, bytes.NewReader(output))
|
|
if err != nil {
|
|
return uc.fail(ctx, jobID)
|
|
}
|
|
final.SetContent(sum, size)
|
|
if err := uc.tx.WithinTx(ctx, func(ctx context.Context) error {
|
|
if err := uc.artifacts.Insert(ctx, final); err != nil {
|
|
return err
|
|
}
|
|
return uc.jobs.CompleteWithResult(ctx, jobID, final.ID, uc.clock.Now())
|
|
}); err != nil {
|
|
_ = uc.blobs.Delete(ctx, final.StorageKey)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reducePartials(reduction string, readers []io.Reader, parameters map[string]any) ([]byte, error) {
|
|
switch reduction {
|
|
case "top-k":
|
|
return reducer.ReduceSimilaritySearch(readers, parameters)
|
|
case "ordered-concat":
|
|
return reducer.ReduceOrderedConcat(readers)
|
|
default:
|
|
return nil, domain.ErrInvalidInput
|
|
}
|
|
}
|
|
|
|
func (uc *ReduceJob) fail(ctx context.Context, jobID uuid.UUID) error {
|
|
// The public state carries a stable sanitized failure, never parser/storage
|
|
// internals that may include local paths or implementation details.
|
|
return uc.jobs.FailReduction(ctx, jobID, "reducer_failed", "final result reduction failed", uc.clock.Now())
|
|
}
|
|
|
|
func closeAll(closers []io.Closer) {
|
|
for _, closer := range closers {
|
|
_ = closer.Close()
|
|
}
|
|
}
|
|
|
|
type GetJobResult struct {
|
|
jobs JobRepository
|
|
download *DownloadArtifact
|
|
}
|
|
|
|
func NewGetJobResult(jobs JobRepository, download *DownloadArtifact) *GetJobResult {
|
|
return &GetJobResult{jobs: jobs, download: download}
|
|
}
|
|
|
|
func (uc *GetJobResult) Execute(ctx context.Context, jobID uuid.UUID) (*domain.Artifact, io.ReadCloser, error) {
|
|
job, err := uc.jobs.Get(ctx, jobID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := authorizeJobAccess(ctx, job); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if job.Status != domain.JobCompleted || job.ResultArtifactID == nil {
|
|
return nil, nil, domain.ErrArtifactNotFound
|
|
}
|
|
return uc.download.Execute(ctx, *job.ResultArtifactID)
|
|
}
|