- 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
152 lines
4.8 KiB
Go
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)
|
|
}
|