Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d931077
restore scheduled timetables to TransitLayer when buffer is cleared
miklcct Nov 26, 2024
74c86d9
update test to handle the case when multiple clear calls are invoked …
miklcct Dec 2, 2024
82a9c29
the restored timetables should only be cleared after updating Transit…
miklcct Dec 2, 2024
e09e5ba
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Dec 4, 2024
ac93e1b
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Dec 17, 2024
00f86bc
no need to put the restored timetables in the snapshot
miklcct Dec 17, 2024
3c6b9f4
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Jan 9, 2025
7f1fd5c
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Jan 30, 2025
5952d9a
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Feb 11, 2025
c5300ba
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Feb 21, 2025
516e3a8
fix test
miklcct Feb 21, 2025
624f341
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Mar 7, 2025
75434b3
formatting
miklcct Mar 7, 2025
488e8c7
Merge tag 'v2.7.0' into fix-update-stale-service
miklcct Mar 12, 2025
d0c3cff
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Apr 15, 2025
84e382a
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct May 6, 2025
578c994
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct May 20, 2025
21b3650
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Jun 4, 2025
d315867
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Jun 19, 2025
9a21d30
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Aug 21, 2025
5502f2f
Merge branch 'dev-2.x' into fix-update-stale-service
miklcct Oct 9, 2025
85f356b
remove unused imports
miklcct Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,16 @@ private static TripTimes getRepresentativeTripTimes(
return null;
}
}

/**
* Get a copy of the scheduled timetable valid for the specified service date only
*/
public Timetable copyForServiceDate(LocalDate date) {
if (serviceDate != null) {
throw new RuntimeException(
"Can only copy scheduled timetable for a specific date if a date hasn't been specified yet."
);
}
return copyOf().withServiceDate(date).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Predicate;
Expand Down Expand Up @@ -100,7 +102,7 @@ public class TimetableSnapshot {
* include ones from the scheduled GTFS, as well as ones added by realtime messages and
* tracked by the TripPatternCache. <p>
* Note that the keys do not include all scheduled TripPatterns, only those for which we have at
* least one update.<p>
* least one update, and those for which we had updates before but just recently cleared.<p>
* The members of the SortedSet (the Timetable for a particular day) are treated as copy-on-write
* when we're updating them. If an update will modify the timetable for a particular day, that
* timetable is replicated before any modifications are applied to avoid affecting any previous
Expand All @@ -111,6 +113,7 @@ public class TimetableSnapshot {
* TripPattern and date.
*/
private final Map<TripPattern, SortedSet<Timetable>> timetables;
private final Set<TripPatternAndServiceDate> patternAndServiceDatesToBeRestored = new HashSet<>();

/**
* For cases where the trip pattern (sequence of stops visited) has been changed by a realtime
Expand Down Expand Up @@ -401,9 +404,21 @@ public TimetableSnapshot commit(
);

if (realtimeRaptorTransitDataUpdater != null) {
for (var patternAndServiceDate : patternAndServiceDatesToBeRestored) {
if (!dirtyTimetables.containsKey(patternAndServiceDate)) {
var pattern = patternAndServiceDate.tripPattern();
var scheduledTimetable = pattern.getScheduledTimetable();
dirtyTimetables.put(
patternAndServiceDate,
scheduledTimetable.copyForServiceDate(patternAndServiceDate.serviceDate)
);
}
}

realtimeRaptorTransitDataUpdater.update(dirtyTimetables.values(), timetables);
}

patternAndServiceDatesToBeRestored.clear();
this.dirtyTimetables.clear();
this.dirty = false;

Expand Down Expand Up @@ -568,7 +583,25 @@ public boolean isEmpty() {
* @return true if the timetable changed as a result of the call
*/
private boolean clearTimetables(String feedId) {
return timetables.keySet().removeIf(tripPattern -> feedId.equals(tripPattern.getFeedId()));
var entriesToBeRemoved = timetables
.entrySet()
.stream()
.filter(entry -> feedId.equals(entry.getKey().getFeedId()))
.collect(Collectors.toSet());
patternAndServiceDatesToBeRestored.addAll(
entriesToBeRemoved
.stream()
.flatMap(entry ->
entry
.getValue()
.stream()
.map(timetable ->
new TripPatternAndServiceDate(entry.getKey(), timetable.getServiceDate())
)
)
.toList()
);
return timetables.entrySet().removeAll(entriesToBeRemoved);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opentripplanner.model;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand All @@ -14,6 +15,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -24,7 +26,10 @@
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RealTimeRaptorTransitDataUpdater;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.StopPattern;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.timetable.RealTimeState;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate;
import org.opentripplanner.transit.model.timetable.TripOnServiceDate;
Expand All @@ -37,7 +42,7 @@ public class TimetableSnapshotTest {
private static final ZoneId timeZone = ZoneIds.GMT;
public static final LocalDate SERVICE_DATE = LocalDate.of(2024, 1, 1);
private static Map<FeedScopedId, TripPattern> patternIndex;
static String feedId;
private static String feedId;

@BeforeAll
public static void setUp() throws Exception {
Expand Down Expand Up @@ -200,6 +205,132 @@ void testClear() {
assertNotNull(snapshot.getRealtimeAddedRoute(pattern.getRoute().getId()));
}

/**
* This test checks that the original timetable is given to RaptorTransitDataUpdater for previously
* added patterns after the buffer is cleared.
* <p>
* Refer to bug #6197 for details.
*/
@Test
void testRaptorTransitDataUpdaterAfterClear() {
var resolver = new TimetableSnapshot();

// make an updated trip
var pattern = patternIndex.get(new FeedScopedId(feedId, "1.1"));
var trip = pattern.scheduledTripsAsStream().findFirst().orElseThrow();
var scheduledTimetable = pattern.getScheduledTimetable();
var realTimeTripTimesBuilder = Objects.requireNonNull(
scheduledTimetable.getTripTimes(trip)
).createRealTimeFromScheduledTimes();
for (var i = 0; i < realTimeTripTimesBuilder.numberOfStops(); ++i) {
realTimeTripTimesBuilder.withArrivalDelay(i, 30);
realTimeTripTimesBuilder.withDepartureDelay(i, 30);
}
realTimeTripTimesBuilder.withRealTimeState(RealTimeState.UPDATED);
var realTimeTripUpdate = new RealTimeTripUpdate(
pattern,
realTimeTripTimesBuilder.build(),
SERVICE_DATE,
null,
false,
false
);

var addedDepartureStopTime = new StopTime();
var addedArrivalStopTime = new StopTime();
addedDepartureStopTime.setDepartureTime(0);
addedDepartureStopTime.setArrivalTime(0);
addedDepartureStopTime.setStop(RegularStop.of(new FeedScopedId(feedId, "XX"), () -> 0).build());
addedArrivalStopTime.setDepartureTime(300);
addedArrivalStopTime.setArrivalTime(300);
addedArrivalStopTime.setStop(RegularStop.of(new FeedScopedId(feedId, "YY"), () -> 1).build());
var addedStopTimes = List.of(addedDepartureStopTime, addedArrivalStopTime);
var addedStopPattern = new StopPattern(addedStopTimes);
var route = patternIndex.values().stream().findFirst().orElseThrow().getRoute();
var addedTripPattern = TripPattern.of(new FeedScopedId(feedId, "1.1"))
.withRoute(route)
.withStopPattern(addedStopPattern)
.withCreatedByRealtimeUpdater(true)
.build();
var addedTripTimes = TripTimesFactory.tripTimes(
Trip.of(new FeedScopedId(feedId, "addedTrip")).withRoute(route).build(),
addedStopTimes,
new Deduplicator()
);
var addedTripUpdate = new RealTimeTripUpdate(
addedTripPattern,
addedTripTimes,
SERVICE_DATE,
null,
true,
false
);

var raptorTransitDataUpdater = new RealTimeRaptorTransitDataUpdater(null) {
int count = 0;

/**
* Test that the TransitLayerUpdater receives correct updated timetables upon commit
* <p>
* This method is called 3 times.
* When count = 0, the buffer contains one added and one updated trip, and the timetables
* should reflect this fact.
* When count = 1, the buffer is empty, however, this method should still receive the
* timetables of the previous added and updated patterns in order to restore them to the
* initial scheduled timetable.
* When count = 2, the buffer is still empty, and no changes should be made.
*/
@Override
public void update(
Collection<Timetable> updatedTimetables,
Map<TripPattern, SortedSet<Timetable>> timetables
) {
assertThat(updatedTimetables).hasSize(count == 2 ? 0 : 2);

updatedTimetables.forEach(timetable -> {
var timetablePattern = timetable.getPattern();
assertEquals(SERVICE_DATE, timetable.getServiceDate());
if (timetablePattern == pattern) {
if (count == 1) {
// the timetable for the existing pattern should revert to the original
assertEquals(scheduledTimetable.getTripTimes(), timetable.getTripTimes());
} else {
// the timetable for the existing pattern should contain the modified times
assertEquals(
RealTimeState.UPDATED,
Objects.requireNonNull(timetable.getTripTimes(trip)).getRealTimeState()
);
}
} else if (timetablePattern == addedTripPattern) {
if (count == 1) {
// the timetable for the added pattern should be empty after clearing
assertThat(timetable.getTripTimes()).isEmpty();
} else {
// the timetable for the added pattern should contain the times for 1 added trip
assertThat(timetable.getTripTimes()).hasSize(1);
}
} else {
throw new RuntimeException("unknown pattern passed to transit layer updater");
}
});
++count;
}
};

resolver.update(realTimeTripUpdate);
resolver.update(addedTripUpdate);
resolver.commit(raptorTransitDataUpdater, true);

resolver.clear(feedId);
resolver.clear(feedId);
resolver.clear(feedId);
assertTrue(resolver.commit(raptorTransitDataUpdater, true).isEmpty());

resolver.clear(feedId);
resolver.clear(feedId);
assertTrue(resolver.commit(raptorTransitDataUpdater, true).isEmpty());
}

private static TimetableSnapshot createCommittedSnapshot() {
TimetableSnapshot timetableSnapshot = new TimetableSnapshot();
return timetableSnapshot.commit(null, true);
Expand Down
Loading