engine refactor
This commit is contained in:
parent
9e663db9dc
commit
81b8e763bd
37 changed files with 3532 additions and 1639 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"predictor-refactored/internal/api/httpjson"
|
||||
"predictor-refactored/internal/datasets"
|
||||
"predictor-refactored/internal/elevation"
|
||||
"predictor-refactored/internal/engine"
|
||||
|
|
@ -46,85 +47,109 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateRequest(req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
field := h.mgr.Active()
|
||||
if field == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
resp, err := Run(h.mgr, h.elev, req)
|
||||
if err != nil {
|
||||
if perr, ok := err.(*PredictionError); ok {
|
||||
writeError(w, perr.Status, perr.Description)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
h.metrics.Prediction("v2", resp.CompletedAt.Sub(resp.StartedAt), nil)
|
||||
h.log.Info("v2 prediction complete",
|
||||
zap.Int("stages", len(resp.Stages)),
|
||||
zap.Duration("elapsed", resp.CompletedAt.Sub(resp.StartedAt)))
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// PredictionError carries an HTTP status alongside the message so async
|
||||
// callers can map the failure back to a useful HTTP response.
|
||||
type PredictionError struct {
|
||||
Status int
|
||||
Description string
|
||||
}
|
||||
|
||||
func (e *PredictionError) Error() string { return e.Description }
|
||||
|
||||
// Run executes a PredictionRequest against the manager's active wind field.
|
||||
// Shared between the sync /api/v2/prediction handler and the async
|
||||
// /api/v1/predictions worker.
|
||||
func Run(mgr *datasets.Manager, elev *elevation.Dataset, req PredictionRequest) (*PredictionResponse, error) {
|
||||
field := mgr.Active()
|
||||
if field == nil {
|
||||
return nil, &PredictionError{Status: http.StatusServiceUnavailable, Description: "no dataset loaded, service is starting up"}
|
||||
}
|
||||
|
||||
// Normalize longitude to [0, 360) for internal use.
|
||||
lng := req.Launch.Longitude
|
||||
if lng < 0 {
|
||||
lng += 360
|
||||
}
|
||||
|
||||
warnings := &engine.Warnings{}
|
||||
var terrain engine.TerrainProvider
|
||||
if h.elev != nil {
|
||||
terrain = h.elev
|
||||
events := engine.NewEventSink()
|
||||
deps := engine.BuildDeps{Wind: field, Events: events}
|
||||
if elev != nil {
|
||||
deps.Terrain = elev
|
||||
}
|
||||
|
||||
prof, err := buildProfile(req, field, terrain, warnings)
|
||||
prof, err := buildProfile(req, deps)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
return nil, &PredictionError{Status: http.StatusBadRequest, Description: err.Error()}
|
||||
}
|
||||
|
||||
started := time.Now().UTC()
|
||||
results := prof.Run(float64(req.Launch.Time.Unix()), engine.State{
|
||||
Lat: req.Launch.Latitude,
|
||||
Lng: lng,
|
||||
Altitude: req.Launch.Altitude,
|
||||
})
|
||||
Lat: req.Launch.Latitude, Lng: lng, Altitude: req.Launch.Altitude,
|
||||
}, events)
|
||||
completed := time.Now().UTC()
|
||||
h.metrics.Prediction("v2", completed.Sub(started), nil)
|
||||
|
||||
resp := PredictionResponse{
|
||||
resp := &PredictionResponse{
|
||||
Stages: make([]StageResult, 0, len(results)),
|
||||
Events: events.Snapshot(),
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Dataset: DatasetInfo{
|
||||
Source: field.Source(),
|
||||
Epoch: field.Epoch(),
|
||||
},
|
||||
Dataset: DatasetInfo{Source: field.Source(), Epoch: field.Epoch()},
|
||||
}
|
||||
for _, r := range results {
|
||||
stage := StageResult{
|
||||
Name: r.Propagator,
|
||||
Outcome: outcomeString(r.Outcome),
|
||||
}
|
||||
if r.Constraint != nil {
|
||||
stage.Constraint = r.Constraint.Name()
|
||||
}
|
||||
stage.Trajectory = make([]TrajectoryPoint, len(r.Points))
|
||||
for i, pt := range r.Points {
|
||||
ptLng := pt.Lng
|
||||
if ptLng > 180 {
|
||||
ptLng -= 360
|
||||
}
|
||||
stage.Trajectory[i] = TrajectoryPoint{
|
||||
Time: time.Unix(int64(pt.Time), 0).UTC(),
|
||||
Latitude: pt.Lat,
|
||||
Longitude: ptLng,
|
||||
Altitude: pt.Altitude,
|
||||
}
|
||||
}
|
||||
resp.Stages = append(resp.Stages, stage)
|
||||
}
|
||||
if warns := warnings.ToMap(); len(warns) > 0 {
|
||||
resp.Warnings = warns
|
||||
resp.Stages = append(resp.Stages, toStageResult(r))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
h.log.Info("v2 prediction complete",
|
||||
zap.Int("stages", len(results)),
|
||||
zap.Duration("elapsed", completed.Sub(started)))
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
func toStageResult(r engine.Result) StageResult {
|
||||
stage := StageResult{
|
||||
Name: r.Propagator,
|
||||
Outcome: r.Outcome.String(),
|
||||
Events: r.Events,
|
||||
}
|
||||
if r.Constraint != nil {
|
||||
stage.Constraint = r.ConstraintName
|
||||
stage.Termination = &TerminationInfo{
|
||||
ViolationTime: time.Unix(int64(r.ViolationTime), 0).UTC(),
|
||||
ViolationState: r.ViolationState,
|
||||
RefinedTime: time.Unix(int64(r.RefinedTime), 0).UTC(),
|
||||
RefinedState: r.RefinedState,
|
||||
}
|
||||
}
|
||||
stage.Trajectory = make([]TrajectoryPoint, len(r.Points))
|
||||
for i, pt := range r.Points {
|
||||
ptLng := pt.Lng
|
||||
if ptLng > 180 {
|
||||
ptLng -= 360
|
||||
}
|
||||
stage.Trajectory[i] = TrajectoryPoint{
|
||||
Time: time.Unix(int64(pt.Time), 0).UTC(),
|
||||
Latitude: pt.Lat,
|
||||
Longitude: ptLng,
|
||||
Altitude: pt.Altitude,
|
||||
}
|
||||
}
|
||||
return stage
|
||||
}
|
||||
|
||||
func validateRequest(req PredictionRequest) error {
|
||||
|
|
@ -148,26 +173,5 @@ func validateRequest(req PredictionRequest) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func outcomeString(o engine.Outcome) string {
|
||||
switch o {
|
||||
case engine.OutcomeStopped:
|
||||
return "stopped"
|
||||
case engine.OutcomeFallback:
|
||||
return "fallback"
|
||||
default:
|
||||
return "continued"
|
||||
}
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, description string) {
|
||||
writeJSON(w, status, ErrorResponse{Error: ErrorBody{
|
||||
Type: http.StatusText(status),
|
||||
Description: description,
|
||||
}})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
var writeJSON = httpjson.Write
|
||||
var writeError = httpjson.Error
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue