Skip to content

Java-parity for arrivals/departures: deviation + distance + tripStatus#1000

Draft
Ahmedhossamdev wants to merge 5 commits into
mainfrom
fix/arrival-departure-java-parity
Draft

Java-parity for arrivals/departures: deviation + distance + tripStatus#1000
Ahmedhossamdev wants to merge 5 commits into
mainfrom
fix/arrival-departure-java-parity

Conversation

@Ahmedhossamdev
Copy link
Copy Markdown
Member

@Ahmedhossamdev Ahmedhossamdev commented Jun 2, 2026

Java parity for arrival / departure trip status

Summary

This PR makes Maglev's arrival/departure responses follow Java OneBusAway's
block-location algorithm for:

  • distanceFromStop
  • numberOfStopsAway
  • tripStatus.scheduleDeviation
  • tripStatus.scheduledDistanceAlongTrip
  • scheduled-only tripStatus position/orientation fields

The previous implementation mixed GPS projection, per-trip GTFS-RT delay,
and publisher-specific current_stop_sequence semantics. That produced
zeros for scheduled-only rows, large loop-route distance errors, unit
mismatches when shape_dist_traveled was not metres, and schedule-deviation
differences on multi-trip blocks.

The new path mirrors Java's model: compute a block-wide schedule deviation,
interpolate the scheduled block at currentTime - scheduleDeviation, then
derive stop metrics from block distance and block stop sequence.

When GTFS-RT is unavailable for an arrival, Maglev now still computes the bus's
scheduled position by interpolating the static GTFS block at the request time.
That gives scheduled-only rows meaningful distanceFromStop,
numberOfStopsAway, and tripStatus fields instead of leaving them at zero.

Scheduled interpolation math

With GTFS-RT, the scheduled snapshot is evaluated at:

effectiveTime = currentTime - scheduleDeviation

Without GTFS-RT, effectiveTime is just the request time.

The snapshot builder finds the two scheduled block stops around that time:

from = previous scheduled block stop
to   = next scheduled block stop

Then it computes how far the effective time is through that scheduled segment:

ratio =
  (effectiveSeconds - from.effectiveStopSeconds)
  / (to.effectiveStopSeconds - from.effectiveStopSeconds)

Finally, it linearly interpolates the block distance:

snapshot.distanceAlongBlock =
  from.distanceAlongBlock
  + ratio * (to.distanceAlongBlock - from.distanceAlongBlock)

If the request time is halfway between two scheduled stops, the snapshot
position is halfway along the scheduled block distance between those stops.
Before the first stop it clamps to the first stop distance; after the last stop
it clamps to the last stop distance.

The arrival metrics are then:

distanceFromStop =
  targetStop.distanceAlongBlock - snapshot.distanceAlongBlock

Positive means the target stop is still ahead; negative means the scheduled
position has already passed it.

numberOfStopsAway =
  targetStop.blockSequence - nextStop.blockSequence

Positive means the target stop is that many stops ahead; zero means it is the
next/current stop; negative means it is behind.

Worked example

Assume the scheduled block has these stops:

Stop Time Distance along block Block sequence
A 12:00:00 1,000 m 10
B 12:10:00 2,000 m 11
C 12:20:00 3,500 m 12

If the request time is 12:05:00 and there is no GTFS-RT, then:

effectiveTime = 12:05:00
from = A
to   = B

ratio =
  (12:05 - 12:00)
  / (12:10 - 12:00)
  = 5 minutes / 10 minutes
  = 0.5

snapshot.distanceAlongBlock =
  1,000 + 0.5 * (2,000 - 1,000)
  = 1,500 m

For target stop B:

distanceFromStop =
  2,000 - 1,500
  = 500 m

numberOfStopsAway =
  11 - 11
  = 0

The bus's scheduled position is 500 m before B, and B is the next scheduled
stop.

For target stop C:

distanceFromStop =
  3,500 - 1,500
  = 2,000 m

numberOfStopsAway =
  12 - 11
  = 1

C is 2,000 m ahead of the scheduled position and one stop after the next stop.

If GTFS-RT says the bus is 2 minutes late at 12:05:00, then:

effectiveTime = 12:05:00 - 2 minutes = 12:03:00
ratio = 3 minutes / 10 minutes = 0.3

snapshot.distanceAlongBlock =
  1,000 + 0.3 * (2,000 - 1,000)
  = 1,300 m

The same formulas still apply; the only difference is that the scheduled
snapshot is evaluated 2 minutes earlier because the bus is late.

What Changed

  • Added internal/restapi/scheduled_block_helper.go.

    • Resolves the active block/shift for a trip.
    • Projects stops monotonically through route shapes, including loop routes.
    • Builds a scheduled block snapshot with DistanceAlongBlock,
      NextStopIndex, active trip, and active-trip scheduled distance.
    • Computes Java-equivalent distanceFromStop and numberOfStopsAway.
  • Reworked GetScheduleDeviationForBlock.

    • Iterates all TripUpdates for trips in the block, in block start order.
    • Preserves Java's "trip-level delay wins" behavior.
    • Chooses the closest StopTimeUpdate only when there is no trip-level delay.
    • Applies Java's blockNotActive guard by discarding deviations over 1 hour.
  • Updated both arrivals handlers.

    • Scheduled-only rows now get a tripStatus.
    • Stop distance/count metrics now come from the scheduled block snapshot.
    • minutesBefore now caps at 240 minutes, matching minutesAfter.
    • Unknown/wrong-case agencies return 404 instead of 500.
  • Updated BuildTripStatus.

    • Uses block-wide deviation.
    • Fills scheduledDistanceAlongTrip from the active block trip.
    • Emits raw GTFS-RT vehicleId instead of double-prefixing it.
    • Uses the shared scheduled-position fallback when no RT vehicle is present.
  • Removed superseded helpers.

    • block_distance_helper.go
    • block_sequence_helper.go
    • dead vehicle current-stop/active-trip helpers
  • Refreshed testdata/openapi.yml from upstream.

Java References

The implementation was checked against these Java paths:

  • GtfsRealtimeTripLibrary.applyTripUpdatesToRecord
    • block trip iteration
    • trip-level delay short-circuit
    • per-stop closest-in-time deviation selection
  • GtfsRealtimeSource.blockNotActive
    • ignores records whose schedule deviation is over 1 hour
  • ArrivalsAndDeparturesBeanServiceImpl.applyBlockLocationToBean
    • distanceFromStop = stopTime.distanceAlongBlock - blockLocation.distanceAlongBlock
    • numberOfStopsAway = stopTime.blockSequence - nextStop.blockSequence
  • TripStatusBeanServiceImpl
    • scheduledDistanceAlongTrip = scheduledDistanceAlongBlock - activeBlockTrip.distanceAlongBlock
  • DistanceAlongShapeLibrary.computeBestAssignment
    • monotonic stop-to-shape assignment for loop routes

Validation

  • Added focused tests for:

    • scheduled block snapshot interpolation
    • stop metrics
    • active-trip scheduled position
    • monotonic loop-route projection
    • shape-distance unit scaling
    • block-wide schedule deviation rules
    • Java's 1-hour stale-deviation guard
  • Live comparison against Java showed exact schedule-deviation parity on the
    sampled Unitrans arrivals, with remaining differences limited to stop-boundary
    and independent-feed-snapshot timing effects.

Follow-Ups

  • Cache scheduled block snapshots per request / RT update to reduce DB query
    amplification.
  • Decide whether Maglev should keep smooth per-request interpolation or match
    Java's stepped GTFS-RT-poll cadence.
  • Make GTFS-RT agency-ids filtering case-insensitive so config casing cannot
    silently drop feeds.
  • Work in TO-DOS

Summary by CodeRabbit

  • New Features

    • Extended historical data query window from 60 to 240 minutes for arrival and departure information.
    • Improved arrival and departure predictions with advanced scheduled block analysis.
  • Bug Fixes

    • Better error responses for invalid agency requests (404 instead of 500).
    • Enhanced prediction fallback logic for more reliable trip timing updates.
  • Tests

    • Comprehensive test coverage for block-based position and distance calculations.

Align distanceFromStop, numberOfStopsAway, and tripStatus with Java
OneBusAway behavior.

* Add scheduled_block_helper.go implementing Java BlockLocation logic
* Port block snapshot and traversal algorithms from Java
* Refactor GetScheduleDeviation to use block-wide last-wins iteration
* Add 1-hour blockNotActive discard guard from GtfsRealtimeSource
* Resolve response differences between Go and Java arrivals/departures APIs
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 418622ec-b20c-4d70-a93d-9fb93698ee33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/arrival-departure-java-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 336
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 334
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/restapi/trips_helper.go (1)

127-167: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't switch ActiveTripID after deriving stop fields from another trip.

In the no-vehicle flow, Lines 153-166 compute ClosestStop / NextStop from dbTripID, then Lines 223-239 can rewrite status.ActiveTripID, distance, position, and orientation to snapshot.ActiveTripID. For blocks where a different trip is currently active, the response mixes stop IDs from one trip with geometry from another. Resolve the snapshot trip first, or recompute the stop fields after the active trip is known.

Also applies to: 223-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trips_helper.go` around lines 127 - 167, The code currently
computes ClosestStop/NextStop from dbTripID (via
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays and
GetStopDelaysFromTripUpdates) before resolving snapshot.ActiveTripID and later
overwrites status.ActiveTripID (status.ActiveTripID/snapshot.ActiveTripID),
which can mix stop IDs from one trip with geometry from another; fix by
resolving which trip is active first (determine snapshot.ActiveTripID) and then
compute ClosestStop/NextStop for that active trip (or, if you must keep the
existing dbTripID computation, recompute stop fields after you set
status.ActiveTripID), updating calls to
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays or the other
stop-finding helpers accordingly so stop offsets and IDs always come from the
final ActiveTripID used in the status.
🧹 Nitpick comments (3)
internal/restapi/scheduled_block_helper_test.go (1)

436-473: ⚡ Quick win

Add coverage for mixed authoritative/geometric stop projection.

The suite covers all-geometric and unusable-shape paths, but it still misses the important case where one stop uses shape_dist_traveled and the next one falls back to geometry. That mixed path is exactly where the monotonic cursor can regress on loop routes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/scheduled_block_helper_test.go` around lines 436 - 473, Add
a new unit test exercising the mixed authoritative/geometric projection case so
projectStopsInSequence is exercised when one StopTime has ShapeDistTraveled and
the next falls back to geometry; create stopTimes where one element sets
ShapeDistTraveled (e.g., 10.0) and the next has no ShapeDistTraveled but has
resolvable stop coordinates, supply a simple shapePoints/cumDistances and a
matching stopByID map (or use api.fetchStopCoordsForStopTimes pattern used
elsewhere), call projectStopsInSequence and assert the returned distances
length, non-negative values, and monotonic non-decreasing order (distances[i] >=
distances[i-1]) to ensure the cursor does not regress on loop routes.
internal/restapi/trip_updates_helper_test.go (1)

12-17: ⚡ Quick win

The new tests still bypass the primary static-schedule path.

Because these cases use synthetic trip IDs with no stop times, GetScheduleDeviationForBlock only exercises the trip-level-delay branch or the reverse-walk fallback. The changed “closest-in-time against scheduled arrivals/departures” logic never runs here, so the main block-level selection path is still unpinned. Please add at least one case backed by real stop times that goes through the scheduled/STU comparison branch.

Also applies to: 23-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper_test.go` around lines 12 - 17, Tests in
trip_updates_helper_test.go still use synthetic trip IDs (devDate/devNow) with
no stop times so GetScheduleDeviationForBlock only exercises trip-level-delay or
reverse-walk fallback and never hits the "closest-in-time against scheduled
arrivals/departures" block-level selection path; add at least one test case that
uses a real trip/block with actual stop_times (a known static DB trip) and
assert behavior that exercises the scheduled vs STU comparison branch inside
GetScheduleDeviationForBlock by creating a mock update for that real trip ID and
timestamps that force the code to evaluate scheduled arrivals/departures rather
than falling back to reverse-walk. Ensure the new test references the real trip
ID you added and uses devDate/devNow or equivalent to place the update in time
so the scheduled/STU comparison logic is executed.
internal/restapi/trip_updates_helper.go (1)

33-179: 🏗️ Heavy lift

Split the deviation-selection branches into helpers.

This helper now mixes trip-update collection, trip-level overwrite semantics, static-schedule candidate ranking, and reverse-walk fallback in one path. That is already showing up as 73 cognitive complexity, and it will make future Java-parity fixes harder to reason about and test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper.go` around lines 33 - 179,
GetScheduleDeviationForBlock is doing four distinct responsibilities (collecting
tripUpdates, computing trip-level overwrite, ranking scheduled-stop-time
candidates, and reverse-walk fallback), making it too complex; refactor by
extracting helpers: 1) collectTripUpdates(ctx, tripIDs) to build
[]tripUpdateForTrip (used instead of the inline loops), 2)
selectTripLevelDeviation(tripUpdates) to implement the "LAST trip-level delay
wins" logic and threshold check, 3) selectBestSTUDeviation(tripUpdates,
serviceDate, currentTime, loadScheduledForTrip) that contains the
scheduled-stop-time candidate ranking and the per-trip scheduled cache (move
loadScheduled into loadScheduledForTrip helper), and 4)
selectFallbackDelay(tripUpdates) to do the reverse-walk per-stop delay fallback;
then make GetScheduleDeviationForBlock simply call these helpers in order and
return early when a helper finds a value. Use existing symbols tripUpdates,
tripUpdateForTrip, GetScheduleDeviationForBlock, and the Java threshold constant
so callers/logic remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/restapi/arrival_and_departure_for_stop_handler.go`:
- Around line 357-365: The code currently applies the schedule shift only when a
vehicle is present (guarded by "vehicle != nil && vehicle.Trip != nil &&
vehicle.Trip.ID.ID != \"\""), which causes predicted times (Delay) to be applied
but distanceFromStop/numberOfStopsAway to be computed from an unshifted
schedule; remove that vehicle-presence guard so the deviation is applied
whenever api.GetScheduleDeviationForBlock returns hasRT. Specifically, keep the
blockTripIDs retrieval (api.blockTripIDsForServiceDate and
api.blockTripIDsSortedByStartTime) and call
api.GetScheduleDeviationForBlock(ctx, blockTripIDs, serviceDate, currentTime)
unconditionally; if hasRT is true set effectiveTime =
currentTime.Add(-time.Duration(dev) * time.Second).

In `@internal/restapi/arrivals_and_departures_for_stop_handler.go`:
- Around line 373-380: The effectiveTime is only adjusted when a vehicle is
present, so trips with delay-only TripUpdates still use schedule-based
distanceFromStop/numberOfStopsAway; call api.GetScheduleDeviationForBlock and
apply the same shift to effectiveTime even when vehicle == nil but a trip-level
delay is present (i.e., treat delay-only TripUpdates like RT vehicles).
Specifically, after computing blockTripIDs via api.blockTripIDsForServiceDate
and api.blockTripIDsSortedByStartTime, check for a trip-level delay (the
TripUpdate delay for st.TripID) and if present use dev from
api.GetScheduleDeviationForBlock to set effectiveTime =
params.Time.Add(-time.Duration(dev) * time.Second) just as in the
vehicle-present branch so arrivals, distanceFromStop and numberOfStopsAway use
the same shifted time.

In `@internal/restapi/scheduled_block_helper_test.go`:
- Around line 418-433: The test must not hard-code noon; instead derive
currentTime from the loaded snapshot data: call computeScheduledBlockSnapshot to
obtain the block/trip stop times, pick a deterministic stop timestamp from that
snapshot (e.g. use the last stop's ScheduledDeparture or a stop time guaranteed
after the trip start via snap.Stops[len(snap.Stops)-1].ScheduledDeparture), then
recreate the snapshot using that derived currentTime before calling
metricsForStop on firstStop (referencing computeScheduledBlockSnapshot,
snap.Stops, firstStop, and metricsForStop) so the sign assertions are
deterministic.

In `@internal/restapi/scheduled_block_helper.go`:
- Around line 69-71: computeScheduledBlockSnapshot is causing O(rows ×
blockSize) DB round-trips because it queries block/service-date once then calls
GetStopTimesForTrip, GetShapePointsByTripID, and GetStopsByIDs per trip and is
invoked per arrival row; batch these lookups before the handlers by collecting
all needed (blockID, serviceDate) pairs (and all tripIDs/stopIDs) and performing
a single batched query for stop times, shape points, and stops (or add a
memoized cache keyed by blockID+serviceDate) and then have
computeScheduledBlockSnapshot accept or read from that batch/cache so handlers
call it with pre-fetched results, reducing DB calls to O(1) per unique block
rather than per row.
- Around line 333-343: The code currently swallows errors from
GetShapePointsByTripID and appends trips with totalDist==0 which causes
downstream distance corruption; change the logic in the block using
GetShapePointsByTripID/shapeRowsToPoints/preCalculateCumulativeDistances so that
if GetShapePointsByTripID returns an error or shapePoints has fewer than 2
points you do NOT append the trip as a zero-length trip — either skip this trip
entirely or implement a dedicated stop-only fallback (call path:
projectStopsInSequence) before appending; specifically check and handle the
error from api.GtfsManager.GtfsDB.Queries.GetShapePointsByTripID, only compute
cumDistances/totalDist when len(shapePoints) >= 2 and otherwise branch to skip
or invoke the stop-only projection, and ensure any appending code uses the
validated totalDist/cumulative data.
- Around line 427-433: In the stopTimes loop inside the helper, when the branch
that uses st.ShapeDistTraveled.Valid writes distances[i] and continues, advance
the monotonic scan cursor by updating lastMatchedIndex (e.g., set
lastMatchedIndex = i) so subsequent geometry-based scans start from the
most-recent matched stop; this preserves the monotonicity guarantee for the
function that fills the distances slice.

In `@internal/restapi/trip_updates_helper.go`:
- Around line 78-93: The scheduledByTrip cache and loadScheduled function
currently collapse stop times by StopID, losing duplicate visits; update
loadScheduled (and any lookup logic that later matches STUs) to store multiple
scheduled entries per trip per stop by keying on stop_sequence when available
and otherwise appending occurrences (e.g., map[string][]{arr,dep,stop_sequence}
or similar) instead of a single struct; then change the matching logic that uses
scheduledByTrip to first attempt a match by stop_sequence (if STU has a
stop_sequence) and fall back to checking all stored occurrences for that StopID
to find the correct arrival/departure for loop/lasso trips.
- Around line 135-148: The code computes predicted using Time.Unix() -
serviceDate.Unix(), which uses epoch seconds and breaks across DST; instead
compute predicted as the service-day (wall-clock) seconds offset consistent with
schedArr/schedDep. Replace the two branches that set predicted from
stu.Arrival.Time and stu.Departure.Time so they convert the Time value into
seconds since the service-day start (the same basis used to compute
schedArr/schedDep) before subtracting the service-day start, and then pass that
value to considerCandidate; update the Arrival case (stu.Arrival.Time) and
Departure case (stu.Departure.Time) accordingly (keep other branches that use
Delay unchanged).

---

Outside diff comments:
In `@internal/restapi/trips_helper.go`:
- Around line 127-167: The code currently computes ClosestStop/NextStop from
dbTripID (via findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays and
GetStopDelaysFromTripUpdates) before resolving snapshot.ActiveTripID and later
overwrites status.ActiveTripID (status.ActiveTripID/snapshot.ActiveTripID),
which can mix stop IDs from one trip with geometry from another; fix by
resolving which trip is active first (determine snapshot.ActiveTripID) and then
compute ClosestStop/NextStop for that active trip (or, if you must keep the
existing dbTripID computation, recompute stop fields after you set
status.ActiveTripID), updating calls to
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays or the other
stop-finding helpers accordingly so stop offsets and IDs always come from the
final ActiveTripID used in the status.

---

Nitpick comments:
In `@internal/restapi/scheduled_block_helper_test.go`:
- Around line 436-473: Add a new unit test exercising the mixed
authoritative/geometric projection case so projectStopsInSequence is exercised
when one StopTime has ShapeDistTraveled and the next falls back to geometry;
create stopTimes where one element sets ShapeDistTraveled (e.g., 10.0) and the
next has no ShapeDistTraveled but has resolvable stop coordinates, supply a
simple shapePoints/cumDistances and a matching stopByID map (or use
api.fetchStopCoordsForStopTimes pattern used elsewhere), call
projectStopsInSequence and assert the returned distances length, non-negative
values, and monotonic non-decreasing order (distances[i] >= distances[i-1]) to
ensure the cursor does not regress on loop routes.

In `@internal/restapi/trip_updates_helper_test.go`:
- Around line 12-17: Tests in trip_updates_helper_test.go still use synthetic
trip IDs (devDate/devNow) with no stop times so GetScheduleDeviationForBlock
only exercises trip-level-delay or reverse-walk fallback and never hits the
"closest-in-time against scheduled arrivals/departures" block-level selection
path; add at least one test case that uses a real trip/block with actual
stop_times (a known static DB trip) and assert behavior that exercises the
scheduled vs STU comparison branch inside GetScheduleDeviationForBlock by
creating a mock update for that real trip ID and timestamps that force the code
to evaluate scheduled arrivals/departures rather than falling back to
reverse-walk. Ensure the new test references the real trip ID you added and uses
devDate/devNow or equivalent to place the update in time so the scheduled/STU
comparison logic is executed.

In `@internal/restapi/trip_updates_helper.go`:
- Around line 33-179: GetScheduleDeviationForBlock is doing four distinct
responsibilities (collecting tripUpdates, computing trip-level overwrite,
ranking scheduled-stop-time candidates, and reverse-walk fallback), making it
too complex; refactor by extracting helpers: 1) collectTripUpdates(ctx, tripIDs)
to build []tripUpdateForTrip (used instead of the inline loops), 2)
selectTripLevelDeviation(tripUpdates) to implement the "LAST trip-level delay
wins" logic and threshold check, 3) selectBestSTUDeviation(tripUpdates,
serviceDate, currentTime, loadScheduledForTrip) that contains the
scheduled-stop-time candidate ranking and the per-trip scheduled cache (move
loadScheduled into loadScheduledForTrip helper), and 4)
selectFallbackDelay(tripUpdates) to do the reverse-walk per-stop delay fallback;
then make GetScheduleDeviationForBlock simply call these helpers in order and
return early when a helper finds a value. Use existing symbols tripUpdates,
tripUpdateForTrip, GetScheduleDeviationForBlock, and the Java threshold constant
so callers/logic remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65ddc6c0-59fd-4329-9988-a1d7a41b0f35

📥 Commits

Reviewing files that changed from the base of the PR and between f5bf005 and 3b189f5.

📒 Files selected for processing (12)
  • internal/restapi/arrival_and_departure_for_stop_handler.go
  • internal/restapi/arrival_and_departure_for_stop_handler_test.go
  • internal/restapi/arrivals_and_departures_for_stop_handler.go
  • internal/restapi/block_distance_helper.go
  • internal/restapi/block_sequence_helper.go
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/scheduled_block_helper_test.go
  • internal/restapi/trip_updates_helper.go
  • internal/restapi/trip_updates_helper_test.go
  • internal/restapi/trips_helper.go
  • internal/restapi/trips_helper_test.go
  • internal/restapi/vehicles_helper.go
💤 Files with no reviewable changes (3)
  • internal/restapi/block_sequence_helper.go
  • internal/restapi/block_distance_helper.go
  • internal/restapi/vehicles_helper.go

Comment thread internal/restapi/arrival_and_departure_for_stop_handler.go
Comment thread internal/restapi/arrivals_and_departures_for_stop_handler.go
Comment thread internal/restapi/scheduled_block_helper_test.go
Comment thread internal/restapi/scheduled_block_helper.go
Comment thread internal/restapi/scheduled_block_helper.go Outdated
Comment thread internal/restapi/scheduled_block_helper.go
Comment thread internal/restapi/trip_updates_helper.go Outdated
Comment thread internal/restapi/trip_updates_helper.go Outdated
…ling

- Update `projectStopsInSequence` to advance `lastMatchedIndex` after an authoritative `shape_dist_traveled` match, preventing subsequent geometric matches from regressing before the previous confirmed stop.
- Fix `GetScheduleDeviationForBlock` for loop trips by preserving all `stop_time` occurrences for a given `stop_id` instead of collapsing them, and matching STUs using `stop_sequence` to select the correct scheduled arrival.
- Add `TestProjectStopsInSequence_MixedAuthoritativeAndGeometric` to verify correct ordering when authoritative and geometric stop matching are combined.
- Add `TestGetScheduleDeviationForBlock_ClosestInTimeAgainstRealSchedule` to verify schedule deviation calculations against real loop-trip schedules.
- Add a TODO note documenting a potential DST edge case in time-based prediction calculations where UTC epoch timestamps are compared with service-day-relative seconds.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.0 ms
Error rate 0.00%
Total requests 330
Req/sec 10.9

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/restapi/trip_updates_helper.go (1)

119-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict the future-over-past preference to equal-distance candidates.

At Line 119, (!isInPast && bestIsInPast) runs even when the future candidate is farther from currentTime than the current best past candidate. That can change scheduleDeviation away from the true closest event, and the handlers then shift block interpolation time by the wrong amount.

Suggested fix
-		if delta < bestDelta || (!isInPast && bestIsInPast) {
+		if delta < bestDelta || (delta == bestDelta && !isInPast && bestIsInPast) {
 			bestDelta = delta
 			bestDeviation = int(deviation)
 			bestIsInPast = isInPast
 			found = true
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper.go` around lines 119 - 123, The current
selection compares future vs past with "(!isInPast && bestIsInPast)"
unconditionally, allowing a farther future candidate to override a closer past
candidate; change the selection logic in the loop that updates
bestDelta/bestDeviation/bestIsInPast/found so the future-over-past tie-breaker
only applies when distances are equal (i.e., only prefer a future candidate if
delta == bestDelta and !isInPast && bestIsInPast); keep the primary comparison
delta < bestDelta unchanged and update the condition accordingly where
bestDelta, delta, isInPast, and bestIsInPast are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/restapi/scheduled_block_helper.go`:
- Around line 436-444: The loop that advances lastMatchedIndex stops too early
when distances[i] exactly equals a cumulative breakpoint: change the check so
equality advances to the segment on the right. In the loop that iterates over
cumulativeDistances (using variables cumulativeDistances, distances and
lastMatchedIndex), replace the current if cumulativeDistances[j+1] >=
distances[i] logic with logic that treats equality as belonging to the
right-hand segment (e.g., use if cumulativeDistances[j+1] > distances[i] {
lastMatchedIndex = j; break } else if cumulativeDistances[j+1] == distances[i] {
lastMatchedIndex = j+1; break }) so the monotonic cursor moves to the correct
segment.

In `@internal/restapi/trip_updates_helper_test.go`:
- Around line 250-258: The mocked StopTimeUpdates in MockAddTripUpdate only set
StopID which breaks tests when mustGetTrip returns a trip with repeated stop
IDs; update the test to set unique StopSequence values on the
gtfs.StopTimeUpdate entries (e.g., set StopSequence for nearStop and farStop) so
the code that resolves updates by stop sequence (GetScheduleDeviationForBlock /
mustGetTrip) can disambiguate duplicate StopIDs, or alternatively detect when
the chosen stop IDs collide and skip the test case; modify the calls to
Api.GtfsManager.MockAddTripUpdate in this fixture to include StopSequence on the
gtfs.StopTimeUpdate structs (and mirror the same fix at the other block around
lines referenced).

---

Outside diff comments:
In `@internal/restapi/trip_updates_helper.go`:
- Around line 119-123: The current selection compares future vs past with
"(!isInPast && bestIsInPast)" unconditionally, allowing a farther future
candidate to override a closer past candidate; change the selection logic in the
loop that updates bestDelta/bestDeviation/bestIsInPast/found so the
future-over-past tie-breaker only applies when distances are equal (i.e., only
prefer a future candidate if delta == bestDelta and !isInPast && bestIsInPast);
keep the primary comparison delta < bestDelta unchanged and update the condition
accordingly where bestDelta, delta, isInPast, and bestIsInPast are used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 761b6b85-5a0b-42ff-9399-d3503871787c

📥 Commits

Reviewing files that changed from the base of the PR and between 3b189f5 and b0096bb.

📒 Files selected for processing (5)
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/scheduled_block_helper_test.go
  • internal/restapi/trip_updates_helper.go
  • internal/restapi/trip_updates_helper_test.go
  • testdata/openapi.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/restapi/scheduled_block_helper_test.go

Comment thread internal/restapi/scheduled_block_helper.go Outdated
Comment thread internal/restapi/trip_updates_helper_test.go
…lexity threshold without changing behavior.

- Refactor `GetScheduleDeviationForBlock` into smaller helper functions for trip update collection, schedule matching, and deviation calculation.
- Refactor `projectStopsInSequence` by extracting shape-distance scaling, cursor advancement, and geometric stop projection helpers.
- Refactor `computeScheduledBlockSnapshot` by moving stop emission logic into a dedicated helper.

All tests continue to pass and functionality remains unchanged.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.7 ms
Error rate 0.00%
Total requests 324
Req/sec 10.7

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 3, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.9 ms
Error rate 0.00%
Total requests 346
Req/sec 11.3

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant