Skip to content

Harden TimePredictor pace_factor: positive-pace guard and clamp #2238

Description

@moveson

Background

Split out of #2169 (implemented in #2237) during plan review. #2237 gates the statistics pool on use_for_projections and makes a degenerate limits band evaluate as "unknown", but two pace-factor weaknesses in TimePredictor (app/services/time_predictor.rb) remain:

1. Latent crash: zero typical completed time

measurable_pace? checks truthiness only:

defmeasurable_pace?completed_lap_split.distance_from_start.positive? && actual_completed_time && typical_completed_timeend

typical_completed_time == 0.0 is truthy, so pace_factor = actual_completed_time / typical_completed_time is float division by zero → Infinity, and limits' (limit * pace_factor).to_i raises FloatDomainError (0 * InfinityNaN does too). Fix: require actual_completed_time&.positive? && typical_completed_time&.positive?, falling back to the existing unmeasurable-pace convention of 1.

2. Unbounded pace factor

In the #2169 incident, a poisoned pool produced pace_factor ≈ 1e-4, collapsing the limits band to zero width so every real time flagged "bad". The pool gating in #2237 makes this far less likely but not impossible (bad data in flag-true events — mis-keyed year, timezone error). Clamping bounds the blast radius:

MIN_PACE_FACTOR=0.1MAX_PACE_FACTOR=10.0(actual_completed_time / typical_completed_time).clamp(MIN_PACE_FACTOR,MAX_PACE_FACTOR)

Legitimate pace factors sit roughly in 0.3–3, so [0.1, 10] never binds on real data; clamp-then-use beats treat-as-unmeasurable because an extreme pace signal still carries direction, just not magnitude.

Suggested specs

Build a predictor with calc_model: :stats and stub SplitTimeQuery.typical_segment_time (arity-preserving idiom already used in segment_time_calculator_spec):

  • huge average (anachronistic pool): data_status(sane_seconds) is not "bad"; pace_factor == 0.1
  • average: 0.0: no FloatDomainError; status nil; pace_factor == 1
  • negative average: status nil, pace_factor == 1

Relates to #2169

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions