alt/services/worker/internal/jobs/runner.go
toki bdbfe8228e feat: scheduled market data refresh - parser_map, refresh status model, CLI output, scheduler backfill, socket handlers
- Add parser_map for market data refresh configuration propagation (cli/worker/api)
- Implement refresh status model with SQLite persistence (status_store.go)
- Add scheduler refresh status headless output to CLI operator
- Extend backfill scheduler with status tracking (start/complete/fail)
- Add socket events for scheduler refresh status (start/complete/fail)
- Update proto definitions for refresh status
- Add generated PB files for client
2026-06-22 18:46:45 +09:00

152 lines
4.8 KiB
Go

package jobs
import (
"context"
"encoding/json"
"fmt"
"sync"
)
// Handler defines the function signature for executing a job.
type Handler func(ctx context.Context, payload json.RawMessage) error
// WithResultHandler defines the function signature for executing a job and
// returning import evidence when available.
type WithResultHandler func(ctx context.Context, payload json.RawMessage) (Result, error)
// Result carries the import outcome from a job handler. It is the single
// source of truth for import evidence; scheduler.ItemResult.ImportSummary
// wraps this same struct so the types align across packages.
type Result struct {
Bars int
Instruments int
MissingBars int
GapBars int
DuplicateBars int
ProviderDelay int
}
// Runner manages the registration and execution of worker job handlers.
type Runner struct {
mu sync.RWMutex
handlers map[Kind]Handler
handlersWithResult map[Kind]WithResultHandler
}
// NewRunner creates a new Runner instance.
func NewRunner() *Runner {
return &Runner{
handlers: make(map[Kind]Handler),
handlersWithResult: make(map[Kind]WithResultHandler),
}
}
// Register registers a handler for a specific job kind.
func (r *Runner) Register(kind Kind, handler Handler) {
r.mu.Lock()
defer r.mu.Unlock()
r.handlers[kind] = handler
}
// RegisterWithResult registers a handler that can also return import evidence.
// Both Register and RegisterWithResult must be called for the same kind
// so that Execute and ExecuteWithResult both work.
func (r *Runner) RegisterWithResult(kind Kind, handler WithResultHandler) {
r.mu.Lock()
defer r.mu.Unlock()
r.handlersWithResult[kind] = handler
}
// Len returns the number of registered handlers.
func (r *Runner) Len() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.handlers)
}
// Execute dispatches the job to its registered handler and executes it.
// It checks context cancellation and handles panics gracefully.
func (r *Runner) Execute(ctx context.Context, job Job) error {
r.mu.RLock()
handler, exists := r.handlers[job.Kind]
r.mu.RUnlock()
if !exists {
return fmt.Errorf("unknown job kind: %s", job.Kind)
}
// Check context before starting
if err := ctx.Err(); err != nil {
return err
}
return r.executeWithPanicRecovery(ctx, handler, job.Payload)
}
// ExecuteWithResult dispatches the job to its registered handler and tries to
// obtain an import Result. If a WithResultHandler is registered it returns the
// evidence along with any error; otherwise it returns (Result{}, false, nil)
// so callers can distinguish "no result handler" (hasResult=false, err=nil)
// from a real execution error (hasResult=false, err!=nil).
//
// Contract: the three-value return semantics are:
// - (res, true, nil) → handler ran and returned result
// - (Result{}, false, nil) → no WithResultHandler registered (legacy fallback)
// - (Result{}, false, err) → handler ran but returned an error
//
// Callers that only have the two-value Executor interface should use Execute()
// instead. ExecuteWithResult is the sole interface for result-returning paths.
func (r *Runner) ExecuteWithResult(ctx context.Context, job Job) (Result, bool, error) {
r.mu.RLock()
wr, hasWithResult := r.handlersWithResult[job.Kind]
r.mu.RUnlock()
if !hasWithResult {
return Result{}, false, nil
}
// Check context before starting
if err := ctx.Err(); err != nil {
return Result{}, false, err
}
res, err := r.executeWithResultPanicRecovery(ctx, wr, job.Payload)
if err != nil {
return Result{}, false, err
}
return res, true, nil
}
// ExecuteWithResultFallback is a convenience wrapper that provides Execute()-level
// fallback for two-value callers. It is NOT part of the Executor interface and
// should only be used internally where the old pattern (res, ok) was consumed.
// Prefer the three-value ExecuteWithResult to preserve error information.
func (r *Runner) ExecuteWithResultFallback(ctx context.Context, job Job) (Result, bool) {
res, hasResult, err := r.ExecuteWithResult(ctx, job)
if err != nil {
// Error path: treat as no-result so legacy callers can fallback to Execute.
return Result{}, false
}
return res, hasResult
}
// executeWithResultPanicRecovery wraps a WithResultHandler with panic
// recovery so that panics surface as typed errors rather than collapsing
// into (Result{}, false, nil).
func (r *Runner) executeWithResultPanicRecovery(ctx context.Context, handler WithResultHandler, payload json.RawMessage) (res Result, err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("job panicked: %v", rec)
}
}()
return handler(ctx, payload)
}
func (r *Runner) executeWithPanicRecovery(ctx context.Context, handler Handler, payload json.RawMessage) (err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("job panicked: %v", rec)
}
}()
return handler(ctx, payload)
}