feat(api): solver service Phase 3b — server endpoints, job definitions, and result cache
Add server-side solver service module with REST API endpoints, database
schema, job definitions, and runner result caching.
New files:
- migrations/021_solver_results.sql: solver_results table with upsert constraint
- internal/db/solver_results.go: SolverResultRepository (Upsert, GetByItem, GetByItemRevision)
- internal/api/solver_handlers.go: solver API handlers and maybeCacheSolverResult hook
- jobdefs/assembly-solve.yaml: manual solve job definition
- jobdefs/assembly-validate.yaml: auto-validate on revision creation
- jobdefs/assembly-kinematic.yaml: manual kinematic simulation job
Modified:
- internal/config/config.go: SolverConfig struct with max_context_size_mb, default_timeout
- internal/modules/modules.go, loader.go: register solver module (depends on jobs)
- internal/db/jobs.go: ListSolverJobs helper with definition_name prefix filter
- internal/api/handlers.go: wire SolverResultRepository into Server
- internal/api/routes.go: /api/solver/* routes + /api/items/{partNumber}/solver/results
- internal/api/runner_handlers.go: async result cache hook on job completion
API endpoints:
- POST /api/solver/jobs — submit solver job (editor)
- GET /api/solver/jobs — list solver jobs with filters
- GET /api/solver/jobs/{id} — get solver job status
- POST /api/solver/jobs/{id}/cancel — cancel solver job (editor)
- GET /api/solver/solvers — registry of available solvers
- GET /api/items/{pn}/solver/results — cached results for item
Also fixes pre-existing test compilation errors (missing workflows param
in NewServer calls across 6 test files).
This commit is contained in:
121
internal/db/solver_results.go
Normal file
121
internal/db/solver_results.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// SolverResult represents a row in the solver_results table.
|
||||
type SolverResult struct {
|
||||
ID string
|
||||
ItemID string
|
||||
RevisionNumber int
|
||||
JobID *string
|
||||
Operation string // solve, diagnose, kinematic
|
||||
SolverName string
|
||||
Status string // SolveStatus string (Success, Failed, etc.)
|
||||
DOF *int
|
||||
Diagnostics []byte // raw JSONB
|
||||
Placements []byte // raw JSONB
|
||||
NumFrames int
|
||||
SolveTimeMS *float64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// SolverResultRepository provides solver_results database operations.
|
||||
type SolverResultRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewSolverResultRepository creates a new solver result repository.
|
||||
func NewSolverResultRepository(db *DB) *SolverResultRepository {
|
||||
return &SolverResultRepository{db: db}
|
||||
}
|
||||
|
||||
// Upsert inserts or updates a solver result. The UNIQUE(item_id, revision_number, operation)
|
||||
// constraint means each revision has at most one result per operation type.
|
||||
func (r *SolverResultRepository) Upsert(ctx context.Context, s *SolverResult) error {
|
||||
err := r.db.pool.QueryRow(ctx, `
|
||||
INSERT INTO solver_results (item_id, revision_number, job_id, operation,
|
||||
solver_name, status, dof, diagnostics, placements,
|
||||
num_frames, solve_time_ms)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT (item_id, revision_number, operation) DO UPDATE SET
|
||||
job_id = EXCLUDED.job_id,
|
||||
solver_name = EXCLUDED.solver_name,
|
||||
status = EXCLUDED.status,
|
||||
dof = EXCLUDED.dof,
|
||||
diagnostics = EXCLUDED.diagnostics,
|
||||
placements = EXCLUDED.placements,
|
||||
num_frames = EXCLUDED.num_frames,
|
||||
solve_time_ms = EXCLUDED.solve_time_ms,
|
||||
created_at = now()
|
||||
RETURNING id, created_at
|
||||
`, s.ItemID, s.RevisionNumber, s.JobID, s.Operation,
|
||||
s.SolverName, s.Status, s.DOF, s.Diagnostics, s.Placements,
|
||||
s.NumFrames, s.SolveTimeMS,
|
||||
).Scan(&s.ID, &s.CreatedAt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upserting solver result: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByItem returns all solver results for an item, ordered by revision descending.
|
||||
func (r *SolverResultRepository) GetByItem(ctx context.Context, itemID string) ([]*SolverResult, error) {
|
||||
rows, err := r.db.pool.Query(ctx, `
|
||||
SELECT id, item_id, revision_number, job_id, operation,
|
||||
solver_name, status, dof, diagnostics, placements,
|
||||
num_frames, solve_time_ms, created_at
|
||||
FROM solver_results
|
||||
WHERE item_id = $1
|
||||
ORDER BY revision_number DESC, operation
|
||||
`, itemID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing solver results: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanSolverResults(rows)
|
||||
}
|
||||
|
||||
// GetByItemRevision returns a single solver result for an item/revision/operation.
|
||||
func (r *SolverResultRepository) GetByItemRevision(ctx context.Context, itemID string, revision int, operation string) (*SolverResult, error) {
|
||||
s := &SolverResult{}
|
||||
err := r.db.pool.QueryRow(ctx, `
|
||||
SELECT id, item_id, revision_number, job_id, operation,
|
||||
solver_name, status, dof, diagnostics, placements,
|
||||
num_frames, solve_time_ms, created_at
|
||||
FROM solver_results
|
||||
WHERE item_id = $1 AND revision_number = $2 AND operation = $3
|
||||
`, itemID, revision, operation).Scan(
|
||||
&s.ID, &s.ItemID, &s.RevisionNumber, &s.JobID, &s.Operation,
|
||||
&s.SolverName, &s.Status, &s.DOF, &s.Diagnostics, &s.Placements,
|
||||
&s.NumFrames, &s.SolveTimeMS, &s.CreatedAt,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting solver result: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func scanSolverResults(rows pgx.Rows) ([]*SolverResult, error) {
|
||||
var results []*SolverResult
|
||||
for rows.Next() {
|
||||
s := &SolverResult{}
|
||||
if err := rows.Scan(
|
||||
&s.ID, &s.ItemID, &s.RevisionNumber, &s.JobID, &s.Operation,
|
||||
&s.SolverName, &s.Status, &s.DOF, &s.Diagnostics, &s.Placements,
|
||||
&s.NumFrames, &s.SolveTimeMS, &s.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scanning solver result: %w", err)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
return results, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user