diff --git a/.github/workflows/close_stale_pr_and_issues.yml b/.github/workflows/close_stale_pr_and_issues.yml index 8fc0612a8d3..2d62107da37 100644 --- a/.github/workflows/close_stale_pr_and_issues.yml +++ b/.github/workflows/close_stale_pr_and_issues.yml @@ -13,7 +13,7 @@ jobs: if: github.repository_owner == 'opentripplanner' runs-on: ubuntu-latest steps: - - uses: actions/stale@v10.3.0 + - uses: actions/stale@v10 id: stale with: stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days' diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java index 47205c17bf3..08e8ed38cbc 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java @@ -88,7 +88,6 @@ static void setup() { transferRepository, model.fareServiceFactory().makeFareService(), null, - null, FLEX_PARAMETERS ).routingService(); } diff --git a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureETUpdater.java b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureETUpdater.java index d5a5b02f375..3b4c006a092 100644 --- a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureETUpdater.java +++ b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureETUpdater.java @@ -22,7 +22,6 @@ public class SiriAzureETUpdater implements SiriAzureMessageHandler { private final SiriRealTimeTripUpdateAdapter adapter; private final Consumer recordMetrics; - private final boolean fuzzyTripMatching; private final String feedId; private WriteToGraphCallback writeToGraphCallback; @@ -33,7 +32,6 @@ public SiriAzureETUpdater( ) { this.adapter = adapter; this.recordMetrics = TripUpdateMetrics.streaming(config); - this.fuzzyTripMatching = config.isFuzzyTripMatching(); this.feedId = Objects.requireNonNull(config.feedId(), "feedId must not be null"); } @@ -56,13 +54,14 @@ public Future handleMessage(ServiceDelivery serviceDelivery, String messageId private Future processMessage(List updates) { return writeToGraphCallback.execute(context -> { - var result = adapter.applyEstimatedTimetable( - fuzzyTripMatching ? context.siriFuzzyTripMatcher() : null, - context.entityResolver(feedId), - feedId, - UpdateIncrementality.DIFFERENTIAL, - updates - ); + var result = adapter + .forUpdate(context.mutableSnapshot()) + .applyEstimatedTimetable( + context.entityResolver(feedId), + feedId, + UpdateIncrementality.DIFFERENTIAL, + updates + ); ResultLogger.logUpdateResultErrors(feedId, "siri-et", result); recordMetrics.accept(result); }); diff --git a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureSXUpdater.java b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureSXUpdater.java index 8aae56826f9..64812723b13 100644 --- a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureSXUpdater.java +++ b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureSXUpdater.java @@ -8,6 +8,7 @@ import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.alert.siri.SiriAlertsUpdateHandler; import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.org.siri.siri21.ServiceDelivery; @@ -22,13 +23,15 @@ public class SiriAzureSXUpdater implements SiriAzureMessageHandler { public SiriAzureSXUpdater( SiriAzureSXUpdaterParameters config, - TimetableRepository timetableRepository + TimetableRepository timetableRepository, + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache ) { this.transitAlertService = new TransitAlertServiceImpl(timetableRepository); this.updateHandler = new SiriAlertsUpdateHandler( config.feedId(), transitAlertService, - Duration.ZERO + Duration.ZERO, + siriFuzzyTripMatcherCache ); } diff --git a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureUpdater.java b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureUpdater.java index 85c08079b74..f28989295b3 100644 --- a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureUpdater.java +++ b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/SiriAzureUpdater.java @@ -38,6 +38,7 @@ import org.opentripplanner.updater.alert.TransitAlertProvider; import org.opentripplanner.updater.spi.GraphUpdater; import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.opentripplanner.updater.trip.siri.SiriRealTimeTripUpdateAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -133,9 +134,14 @@ public static SiriAzureUpdater createETUpdater( public static SiriAzureUpdater createSXUpdater( SiriAzureSXUpdaterParameters config, - TimetableRepository timetableRepository + TimetableRepository timetableRepository, + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache ) { - var messageHandler = new SiriAzureSXUpdater(config, timetableRepository); + var messageHandler = new SiriAzureSXUpdater( + config, + timetableRepository, + siriFuzzyTripMatcherCache + ); return new SxWrapper(config, messageHandler); } diff --git a/application/src/ext/java/org/opentripplanner/ext/siri/updater/mqtt/SiriETMqttUpdater.java b/application/src/ext/java/org/opentripplanner/ext/siri/updater/mqtt/SiriETMqttUpdater.java index 613c3ff2890..a059926bb51 100644 --- a/application/src/ext/java/org/opentripplanner/ext/siri/updater/mqtt/SiriETMqttUpdater.java +++ b/application/src/ext/java/org/opentripplanner/ext/siri/updater/mqtt/SiriETMqttUpdater.java @@ -26,11 +26,7 @@ public SiriETMqttUpdater( ) { configRef = parameters.configRef(); asyncEstimatedTimetableSource = new MqttEstimatedTimetableSource(parameters); - estimatedTimetableHandler = new EstimatedTimetableHandler( - updateAdapter, - parameters.fuzzyTripMatching(), - parameters.feedId() - ); + estimatedTimetableHandler = new EstimatedTimetableHandler(updateAdapter, parameters.feedId()); updateResultConsumer = TripUpdateMetrics.streaming(parameters); } diff --git a/application/src/main/java/org/opentripplanner/framework/transaction/internal/PeriodicCommitScheduler.java b/application/src/main/java/org/opentripplanner/framework/transaction/internal/PeriodicCommitScheduler.java index 5bb9a63b4bf..d211cf1e980 100644 --- a/application/src/main/java/org/opentripplanner/framework/transaction/internal/PeriodicCommitScheduler.java +++ b/application/src/main/java/org/opentripplanner/framework/transaction/internal/PeriodicCommitScheduler.java @@ -19,7 +19,8 @@ class PeriodicCommitScheduler { private final String name; private final ScheduledExecutorService scheduler; private final Supplier> performCommit; - private long warningLimitMillis; + private final long interval_ms; + private long warningLimit_ms; PeriodicCommitScheduler( String name, @@ -30,9 +31,9 @@ class PeriodicCommitScheduler { this.name = name; this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory); this.performCommit = performCommit; - var intervalMillis = commitInterval.toMillis(); - this.warningLimitMillis = intervalMillis; - scheduler.scheduleAtFixedRate(this::runCommit, intervalMillis, intervalMillis, MILLISECONDS); + this.interval_ms = commitInterval.toMillis(); + this.warningLimit_ms = interval_ms; + this.scheduler.scheduleAtFixedRate(this::runCommit, interval_ms, interval_ms, MILLISECONDS); } void shutdown() { @@ -50,13 +51,31 @@ private void runCommit() { LOG.error("Error during periodic commit", e); } long elapsedTime = System.currentTimeMillis() - startTime; - if (elapsedTime > warningLimitMillis) { - warningLimitMillis *= 2; - LOG.warn( - "Commit is taking a long time to complete (including queued wait), {} ms for {}.", - elapsedTime, - name - ); + if (elapsedTime > warningLimit_ms) { + if (warningLimit_ms == interval_ms) { + // We increase the limit after the first log event to: + // - avoid spamming the logs if the update is exceeding the limit by a small amount. + // - distinguish between the first (initialization) and the followups. + warningLimit_ms *= 2; + LOG.warn( + "Commit is taking a long time to complete (including queued wait), {} ms for {}. " + + "The update interval is {} ms. This is the first warning, increasing the warning limit " + + "to {} ms.", + elapsedTime, + name, + interval_ms, + warningLimit_ms + ); + } else { + LOG.warn( + "Commit is taking a long time to complete (including queued wait), {} ms for {}. " + + "The warning limit is {} ms. If this continues to happen, consider investigating why or " + + "increasing the update interval.", + elapsedTime, + name, + warningLimit_ms + ); + } } } } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index bf41fd0a1fc..3d43d352448 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -152,6 +152,12 @@ else if (pathLeg.isTransferLeg()) { // Map egress leg EgressPathLeg egressPathLeg = pathLeg.asEgressLeg(); + if ( + OTPFeature.ExtraTransferLegOnSameStop.isOn() && + isPathTransferAtSameStop(previousLeg, egressPathLeg) + ) { + legs.add(createTransferLegAtSameStop(previousLeg, pathLeg)); + } var egressLegs = mapEgressLeg(egressPathLeg); legs.addAll(egressLegs); @@ -199,6 +205,19 @@ private static boolean isPathTransferAtSameStop( ); } + private static boolean isPathTransferAtSameStop( + PathLeg previousLeg, + EgressPathLeg egressPathLeg + ) { + return ( + previousLeg != null && + previousLeg.isTransitLeg() && + egressPathLeg.egress().hasRides() && + !previousLeg.asTransitLeg().isStaySeatedOntoNextLeg() && + (previousLeg.asTransitLeg().toStop() == egressPathLeg.fromStop()) + ); + } + private List mapAccessLeg(AccessPathLeg accessPathLeg) { if (accessPathLeg.access().isFree()) { return List.of(); diff --git a/application/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java b/application/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java index 7ed79d8f0c2..8d48a38f1d9 100644 --- a/application/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java +++ b/application/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java @@ -11,8 +11,8 @@ import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.transit.model.timetable.Direction; -import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.alert.TransitAlertProvider; +import org.opentripplanner.updater.spi.GraphUpdater; /** * This class is used to combine alerts from multiple {@link TransitAlertService}s. Each @@ -35,16 +35,11 @@ public class DelegatingTransitAlertServiceImpl implements TransitAlertService { * This implies that these instances are expected to remain in use indefinitely (not be replaced * with new instances or taken out of service over time). */ - public DelegatingTransitAlertServiceImpl(TimetableRepository timetableRepository) { - if (timetableRepository.getUpdaterManager() != null) { - timetableRepository - .getUpdaterManager() - .getUpdaterList() - .stream() - .filter(TransitAlertProvider.class::isInstance) - .map(TransitAlertProvider.class::cast) - .map(TransitAlertProvider::getTransitAlertService) - .forEach(transitAlertServices::add); + public DelegatingTransitAlertServiceImpl(Iterable updaters) { + for (GraphUpdater updater : updaters) { + if (updater instanceof TransitAlertProvider alertProvider) { + transitAlertServices.add(alertProvider.getTransitAlertService()); + } } } diff --git a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 6b63ee29f59..1d45f9b2597 100644 --- a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -19,6 +19,7 @@ import org.opentripplanner.ext.sorlandsbanen.SorlandsbanenNorwayService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.framework.transaction.api.TransactionScope; import org.opentripplanner.place.NearbyPlaceFinder; import org.opentripplanner.place.NearbyStopFinder; import org.opentripplanner.place.nearbystopfinder.StraightLineNearbyStopFinder; @@ -79,6 +80,14 @@ public interface OtpServerRequestContext { DebugUiConfig debugUiConfig(); + /** + * The transaction scope that was captured at the start of this HTTP request. It holds a strong + * reference to the current {@link org.opentripplanner.framework.transaction.internal.Transaction}, + * preventing the {@link org.opentripplanner.framework.transaction.internal.RepositorySnapshotCache} + * from GC-ing any snapshot used during this request. + */ + TransactionScope transactionScope(); + /** * A RouteRequest containing default parameters that will be cloned when handling each request. */ diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index b9457738899..ba6faf76ecc 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -42,7 +42,6 @@ import org.opentripplanner.transfer.regular.TransferRepository; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.configure.UpdaterConfigurator; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.utils.logging.ProgressTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -216,7 +215,8 @@ private void setupTransitRoutingServer() { vehicleParkingRepository(), timetableRepository(), carpoolingRepository(), - snapshotManager(), + factory.updateManager(), + factory.timetableRepositoryHandle(), routerConfig().updaterConfig() ); @@ -305,10 +305,6 @@ public VehicleRentalRepository vehicleRentalRepository() { return factory.vehicleRentalRepository(); } - private TimetableSnapshotManager snapshotManager() { - return factory.timetableSnapshotManager(); - } - public VehicleParkingService vehicleParkingService() { return factory.vehicleParkingService(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index 1fa7c43182a..f9f42b0eb69 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -26,6 +26,9 @@ import org.opentripplanner.ext.sorlandsbanen.configure.SorlandsbanenNorwayModule; import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.stopconsolidation.configure.StopConsolidationServiceModule; +import org.opentripplanner.framework.transaction.UpdateManager; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; +import org.opentripplanner.framework.transaction.configure.TransactionModule; import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; @@ -65,9 +68,10 @@ import org.opentripplanner.transfer.regular.configure.TransferServiceModule; import org.opentripplanner.transit.configure.TransitModule; import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.warmup.WarmupLauncher; import org.opentripplanner.warmup.configure.WarmupModule; @@ -105,6 +109,7 @@ ViaModule.class, WarmupModule.class, WorldEnvelopeServiceModule.class, + TransactionModule.class, } ) public interface ConstructApplicationFactory { @@ -123,7 +128,8 @@ public interface ConstructApplicationFactory { VehicleRentalService vehicleRentalService(); VehicleParkingRepository vehicleParkingRepository(); VehicleParkingService vehicleParkingService(); - TimetableSnapshotManager timetableSnapshotManager(); + UpdateManager updateManager(); + RepositoryHandle timetableRepositoryHandle(); DataImportIssueSummary dataImportIssueSummary(); @Nullable diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 54316995b58..bf0d07b11ec 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -17,6 +17,8 @@ import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.sorlandsbanen.SorlandsbanenNorwayService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.framework.transaction.RepositoryRegistry; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.filterchain.ext.EmissionDecorator; import org.opentripplanner.routing.algorithm.filterchain.framework.spi.ItineraryDecorator; @@ -38,7 +40,10 @@ import org.opentripplanner.street.linking.VertexLinker; import org.opentripplanner.street.service.StreetLimitationParametersService; import org.opentripplanner.transfer.regular.RegularTransferService; -import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TimetableRepository; @Module public class ConstructApplicationModule { @@ -51,7 +56,9 @@ OtpServerRequestContext providesServerContext( Graph graph, LinkingContextFactory linkingContextFactory, VertexLinker vertexLinker, - TransitService transitService, + TimetableRepository timetableRepository, + RepositoryRegistry repositoryRegistry, + RepositoryHandle timetableRepositoryHandle, RegularTransferService transferService, WorldEnvelopeService worldEnvelopeService, RealtimeVehicleService realtimeVehicleService, @@ -83,6 +90,13 @@ OtpServerRequestContext providesServerContext( var flexParameters = routerConfig.flexParameters(); var transmodelAPIParameters = routerConfig.transmodelApi(); + // Create exactly one TransactionScope per request. The scope holds a strong reference to the + // current Transaction, preventing the RepositorySnapshotCache from GC-ing the snapshot while + // this request is in flight. The scope is stored on DefaultServerRequestContext for this reason. + var transactionScope = repositoryRegistry.scope(); + var timetableSnapshot = timetableRepositoryHandle.repositorySnapshot(transactionScope); + var transitService = new DefaultTransitService(timetableRepository, timetableSnapshot); + return new DefaultServerRequestContext( debugUiConfig, fareService, @@ -97,6 +111,7 @@ OtpServerRequestContext providesServerContext( defaultRequest, streetLimitationParametersService, transferService, + transactionScope, transitRoutingConfig, transitService, triasApiParameters, diff --git a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index a3d2bb8839b..545b2d60765 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -18,6 +18,7 @@ import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.sorlandsbanen.SorlandsbanenNorwayService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.framework.transaction.api.TransactionScope; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.filterchain.framework.spi.ItineraryDecorator; @@ -52,6 +53,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { // All 3 sections is sorted alphabetically. private final DebugUiConfig debugUiConfig; + private final TransactionScope transactionScope; private final FareService fareService; private final FlexParameters flexParameters; private final Graph graph; @@ -135,6 +137,7 @@ public DefaultServerRequestContext( RouteRequest routeRequestDefaults, StreetLimitationParametersService streetLimitationParametersService, RegularTransferService transferService, + TransactionScope transactionScope, TransitRoutingConfig transitRoutingConfig, TransitService transitService, TriasApiParameters triasApiParameters, @@ -170,6 +173,7 @@ public DefaultServerRequestContext( this.routeRequestDefaults = routeRequestDefaults; this.streetLimitationParametersService = streetLimitationParametersService; this.transferService = transferService; + this.transactionScope = transactionScope; this.transitRoutingConfig = transitRoutingConfig; this.transitService = transitService; this.transmodelSchema = transmodelSchema; @@ -200,6 +204,11 @@ public DebugUiConfig debugUiConfig() { return debugUiConfig; } + @Override + public TransactionScope transactionScope() { + return transactionScope; + } + @Override public RouteRequest defaultRouteRequest() { return routeRequestDefaults; diff --git a/application/src/main/java/org/opentripplanner/standalone/server/MetricsLogging.java b/application/src/main/java/org/opentripplanner/standalone/server/MetricsLogging.java index ef96664cade..f3aa685b505 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/MetricsLogging.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/MetricsLogging.java @@ -77,12 +77,6 @@ public MetricsLogging( "nonPollingGraphUpdaters", List.of(Tag.of("pool", "nonPollingGraphUpdaters")) ).bindTo(Metrics.globalRegistry); - - new ExecutorServiceMetrics( - timetableRepository.getUpdaterManager().getScheduler(), - "graphUpdateScheduler", - List.of(Tag.of("pool", "graphUpdateScheduler")) - ).bindTo(Metrics.globalRegistry); } if (raptorConfig.isMultiThreaded()) { diff --git a/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java b/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java index f9a845ee7b3..e0c286e06e4 100644 --- a/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java +++ b/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java @@ -5,15 +5,20 @@ import dagger.Provides; import jakarta.inject.Singleton; import java.time.LocalDate; +import org.opentripplanner.framework.transaction.RepositoryRegistry; +import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; import org.opentripplanner.standalone.api.HttpRequestScoped; import org.opentripplanner.standalone.config.ConfigModel; import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.repository.TimetableSnapshotLifecycle; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; @Module public abstract class TransitModule { @@ -24,25 +29,28 @@ public abstract class TransitModule { @Provides @Singleton - public static TimetableSnapshotManager timetableSnapshotManager( - ConfigModel config, + public static TimetableSnapshotParameters timetableSnapshotParameters(ConfigModel config) { + return config.routerConfig().updaterConfig().timetableSnapshotParameters(); + } + + @Provides + @Singleton + public static RepositoryHandle< + ReadOnlyTimetableSnapshot, + MutableTimetableSnapshot + > timetableRepositoryHandle( + TimetableSnapshotParameters parameters, TimetableRepository timetableRepository, + RepositoryRegistry repositoryRegistry, RaptorTransitData scheduledRaptorTransitData, - DefaultTripCalendars scheduledTripCalendars + DefaultTripCalendars tripCalendars ) { - return new TimetableSnapshotManager( - config.routerConfig().updaterConfig().timetableSnapshotParameters(), - () -> LocalDate.now(timetableRepository.getTimeZone()), - scheduledRaptorTransitData, - scheduledTripCalendars + var mutableBuffer = new TimetableSnapshot(scheduledRaptorTransitData, tripCalendars); + var timetableSnapshotLifecycle = new TimetableSnapshotLifecycle( + mutableBuffer, + parameters.purgeExpiredData(), + () -> LocalDate.now(timetableRepository.getTimeZone()) ); - } - - /** - * Provides the currently published, immutable {@link TimetableSnapshot}. - */ - @Provides - public static TimetableSnapshot timetableSnapshot(TimetableSnapshotManager manager) { - return manager.getTimetableSnapshot(); + return repositoryRegistry.registerRepository(mutableBuffer, timetableSnapshotLifecycle); } } diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/TimetableSnapshot.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/TimetableSnapshot.java index 9de2cc2ddf7..3018ae196f9 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/TimetableSnapshot.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/TimetableSnapshot.java @@ -26,6 +26,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.jspecify.annotations.NonNull; import org.opentripplanner.core.model.id.FeedScopedId; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TimetableUpdateMapper; @@ -34,6 +35,8 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,7 +81,7 @@ * guarantee of safe-publication without synchronization. * (see final Field Semantics) */ -public class TimetableSnapshot { +public class TimetableSnapshot implements ReadOnlyTimetableSnapshot, MutableTimetableSnapshot { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshot.class); @@ -416,6 +419,10 @@ public TimetableSnapshot commit(boolean force) { return null; } + return createReadOnlySnapshot(); + } + + public @NonNull TimetableSnapshot createReadOnlySnapshot() { RaptorTransitData updatedRaptorData = timetableUpdateMapper.map( realtimeRaptorTransitData, dirtyTimetables.values(), @@ -423,7 +430,7 @@ public TimetableSnapshot commit(boolean force) { new TripPatternForDateMapper(tripCalendars.getServiceCodesRunningForDate()) ); - TimetableSnapshot ret = new TimetableSnapshot( + var timetableSnapshot = new TimetableSnapshot( Map.copyOf(timetables), Map.copyOf(realTimeNewTripPatternsForModifiedTrips), Map.copyOf(realtimeAddedRoutes), @@ -445,7 +452,7 @@ public TimetableSnapshot commit(boolean force) { dirtyTimetables.clear(); dirty = false; - return ret; + return timetableSnapshot; } /** diff --git a/application/src/main/java/org/opentripplanner/transit/repository/MutableTimetableSnapshot.java b/application/src/main/java/org/opentripplanner/transit/repository/MutableTimetableSnapshot.java new file mode 100644 index 00000000000..e1eeb5d734b --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/repository/MutableTimetableSnapshot.java @@ -0,0 +1,17 @@ +package org.opentripplanner.transit.repository; + +import java.time.LocalDate; +import org.opentripplanner.core.model.id.FeedScopedId; +import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; + +public interface MutableTimetableSnapshot extends ReadOnlyTimetableSnapshot { + void update(RealTimeTripUpdate realTimeTripUpdate); + + ReadOnlyTimetableSnapshot createReadOnlySnapshot(); + + void clear(String feedId); + + boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate serviceDate); + + boolean purgeExpiredData(LocalDate serviceDate); +} diff --git a/application/src/main/java/org/opentripplanner/transit/repository/ReadOnlyTimetableSnapshot.java b/application/src/main/java/org/opentripplanner/transit/repository/ReadOnlyTimetableSnapshot.java new file mode 100644 index 00000000000..9d7bf2ae23b --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/repository/ReadOnlyTimetableSnapshot.java @@ -0,0 +1,62 @@ +package org.opentripplanner.transit.repository; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.core.model.id.FeedScopedId; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.Timetable; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; + +public interface ReadOnlyTimetableSnapshot { + Timetable resolve(TripPattern pattern, @Nullable LocalDate serviceDate); + + @Nullable + TripPattern getNewTripPatternForModifiedTrip(FeedScopedId tripId, LocalDate serviceDate); + + List listCanceledTrips(); + + boolean hasNewTripPatternsForModifiedTrips(); + + @Nullable + Route getRealtimeAddedRoute(FeedScopedId id); + + Collection listRealTimeAddedRoutes(); + + @Nullable + Trip getRealTimeAddedTrip(FeedScopedId id); + + Collection listRealTimeAddedTrips(); + + @Nullable + TripPattern getRealTimeAddedPatternForTrip(Trip trip); + + Collection getRealTimeAddedPatternForRoute(Route route); + + @Nullable + TripOnServiceDate getRealTimeAddedTripOnServiceDateById(FeedScopedId id); + + @Nullable + TripOnServiceDate getRealTimeAddedTripOnServiceDateForTripAndDay( + TripIdAndServiceDate tripIdAndServiceDate + ); + + Collection listRealTimeAddedTripOnServiceDate(); + + Collection getRealTimeReplacedByTripOnServiceDate(FeedScopedId id); + + Collection getPatternsForStop(StopLocation stop); + + RaptorTransitData getRealtimeRaptorTransitData(); + + /** + * Does this snapshot contain any realtime data or is it completely empty? + */ + boolean isEmpty(); +} diff --git a/application/src/main/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycle.java b/application/src/main/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycle.java new file mode 100644 index 00000000000..5b4fd86a275 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycle.java @@ -0,0 +1,43 @@ +package org.opentripplanner.transit.repository; + +import java.time.LocalDate; +import java.util.function.Supplier; +import org.opentripplanner.framework.transaction.api.RepositoryLifecycle; + +public class TimetableSnapshotLifecycle + implements RepositoryLifecycle { + + private final MutableTimetableSnapshot buffer; + private final boolean purgeExpiredData; + private final Supplier localDateNow; + private LocalDate lastPurgeDate = null; + + public TimetableSnapshotLifecycle( + MutableTimetableSnapshot buffer, + boolean purgeExpiredData, + Supplier localDateNow + ) { + this.buffer = buffer; + this.purgeExpiredData = purgeExpiredData; + this.localDateNow = localDateNow; + } + + @Override + public MutableTimetableSnapshot copyOnWrite(ReadOnlyTimetableSnapshot readOnlySnapshot) { + return buffer; + } + + @Override + public ReadOnlyTimetableSnapshot freeze(MutableTimetableSnapshot mutableSnapshot) { + if (purgeExpiredData) { + final LocalDate today = localDateNow.get(); + // Keep data for today and the previous day; purge anything older + final LocalDate previously = today.minusDays(2); + if (lastPurgeDate == null || lastPurgeDate.compareTo(previously) < 0) { + lastPurgeDate = previously; + buffer.purgeExpiredData(previously); + } + } + return buffer.createReadOnlySnapshot(); + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index f0b71dd7756..d2f273bb8e5 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -61,11 +61,11 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.site.StopLocationsGroup; import org.opentripplanner.transit.model.timetable.Timetable; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; import org.opentripplanner.updater.GraphUpdaterStatus; import org.opentripplanner.utils.collection.CollectionsView; import org.opentripplanner.utils.collection.SetUtils; @@ -92,7 +92,7 @@ public class DefaultTransitService implements TransitEditorService { * instance does not contain any real-time information. */ @Nullable - private final TimetableSnapshot timetableSnapshot; + private final ReadOnlyTimetableSnapshot timetableSnapshot; /** * Helper for fetching stop times for APIs. @@ -103,15 +103,17 @@ public class DefaultTransitService implements TransitEditorService { /** * Create a service without a real-time snapshot (and therefore without any real-time data). + * This is the constructor used by Dagger injection. Use the {@link TransitService} via the + * {@link org.opentripplanner.standalone.api.OtpServerRequestContext} if real-time data is needed. */ + @Inject public DefaultTransitService(TimetableRepository timetableRepository) { this(timetableRepository, null); } - @Inject public DefaultTransitService( TimetableRepository timetableRepository, - @Nullable TimetableSnapshot timetableSnapshot + @Nullable ReadOnlyTimetableSnapshot timetableSnapshot ) { this.timetableRepository = timetableRepository; this.timetableRepositoryIndex = timetableRepository.getTimetableRepositoryIndex(); diff --git a/application/src/main/java/org/opentripplanner/transit/service/ReplacementHelper.java b/application/src/main/java/org/opentripplanner/transit/service/ReplacementHelper.java index 4c90aac9e36..34f9995032e 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/ReplacementHelper.java +++ b/application/src/main/java/org/opentripplanner/transit/service/ReplacementHelper.java @@ -8,9 +8,9 @@ import org.opentripplanner.transit.model.network.ReplacedByRelation; import org.opentripplanner.transit.model.network.ReplacementForRelation; import org.opentripplanner.transit.model.network.Route; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; /** *

Encapsulates the part of Transit Service which deals with Route/Trip/TripOnServiceDate @@ -35,12 +35,12 @@ public class ReplacementHelper { private final TimetableRepository timetableRepository; @Nullable - private final TimetableSnapshot timetableSnapshot; + private final ReadOnlyTimetableSnapshot timetableSnapshot; public ReplacementHelper( TransitService transitService, TimetableRepository timetableRepository, - @Nullable TimetableSnapshot timetableSnapshot + @Nullable ReadOnlyTimetableSnapshot timetableSnapshot ) { this.transitService = transitService; this.timetableRepository = timetableRepository; diff --git a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java index d8f4e68d1b0..8ca46f0eb23 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java @@ -358,11 +358,13 @@ public void addNoticeAssignments(Multimap notices /** * Returns the alert service. If no updaters are configured an empty instance is returned. - * See {@link TimetableRepository#setUpdaterManager(GraphUpdaterManager)}. + * See {@link TimetableRepository#initUpdaterManager(GraphUpdaterManager)}. */ public TransitAlertService getTransitAlertService() { if (transitAlertService == null) { - transitAlertService = new DelegatingTransitAlertServiceImpl(this); + transitAlertService = new DelegatingTransitAlertServiceImpl( + this.updaterManager == null ? List.of() : this.updaterManager.getUpdaterList() + ); } return transitAlertService; } @@ -442,12 +444,32 @@ public Collection getAllTripsOnServiceDates() { return Collections.unmodifiableCollection(tripOnServiceDates.values()); } + /** + * Sets the updater manager for this repository and makes sure the configured updaters + * are correctly applied to {@code transitAlertService}. + *

+ * Note: before this method is called an empty {@code transitAlertService} is returned instead. + *

+ * TODO: This logic is unfortunate and quite brittle. We would like to improve it in the future. + * The UpdateManager should live in a DI context(Dagger), not here. + */ + public void initUpdaterManager(GraphUpdaterManager updaterManager) { + this.updaterManager = ObjectUtils.requireNotInitialized( + "updaterManager", + this.updaterManager, + updaterManager + ); + this.transitAlertService = null; + } + /** * Manages all updaters of this graph. Is created by the GraphUpdaterConfigurator when there are - * graph updaters defined in the configuration. + * graph updaters defined in the configuration. This is {@code null} if no updaters are + * configured or not yet initialized. * * @see UpdaterConfigurator */ + @Nullable public GraphUpdaterManager getUpdaterManager() { return updaterManager; } @@ -483,20 +505,6 @@ public void addFlexTrip(FeedScopedId id, FlexTrip flexTrip) { flexTripsById.put(id, flexTrip); } - /** - * Sets the updater manager for this repository and makes sure the configured updaters - * are correctly applied to {@code transitAlertService}. - *

- * Note: before this method is called an empty {@code transitAlertService} is returned instead. - *

- * This logic is unfortunate and quite brittle. We would like to improve it in the future. - */ - public void setUpdaterManager(GraphUpdaterManager updaterManager) { - assertModificationsAllowed(); - this.updaterManager = updaterManager; - this.transitAlertService = null; - } - /** * True if frequency-based services exist in this Graph (GTFS frequencies with exact_times = 0). */ diff --git a/application/src/main/java/org/opentripplanner/updater/DefaultRealTimeUpdateContext.java b/application/src/main/java/org/opentripplanner/updater/DefaultRealTimeUpdateContext.java index e3a7c8407d4..1cff4ae7450 100644 --- a/application/src/main/java/org/opentripplanner/updater/DefaultRealTimeUpdateContext.java +++ b/application/src/main/java/org/opentripplanner/updater/DefaultRealTimeUpdateContext.java @@ -1,26 +1,42 @@ package org.opentripplanner.updater; import org.opentripplanner.street.graph.Graph; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.trip.gtfs.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.trip.siri.EntityResolver; -import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcher; public class DefaultRealTimeUpdateContext implements RealTimeUpdateContext { private final Graph graph; + private final MutableTimetableSnapshot timetableSnapshotBuffer; private final TransitService transitService; - private SiriFuzzyTripMatcher siriFuzzyTripMatcher; + /** + * The context needs the mutable snapshot so that entity lookups (trips, routes, patterns) see + * all in-progress real-time additions that have not yet been committed to a published snapshot. + *

+ * A {@link MutableTimetableSnapshot} cannot be used directly for these lookups, because every + * lookup must also fall back to scheduled data in the {@link TimetableRepository} when an entity + * is not found in the real-time snapshot. The {@link DefaultTransitService} combines both: it + * checks the snapshot first, then falls back to the static index. + *

+ * {@link DefaultTransitService} accepts a {@link org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot}, + * because in request scope it must never receive a mutable snapshot. The cast here is safe as + * long as {@link MutableTimetableSnapshot} and {@link org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot} + * share a single implementation — which is enforced by {@link MutableTimetableSnapshot} extending + * {@link org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot}. A cleaner separation + * would require merging scheduled and real-time data into a single unified store - this is the end goal! + */ public DefaultRealTimeUpdateContext( Graph graph, TimetableRepository timetableRepository, - TimetableSnapshot timetableSnapshotBuffer + MutableTimetableSnapshot timetableSnapshotBuffer ) { this.graph = graph; + this.timetableSnapshotBuffer = timetableSnapshotBuffer; this.transitService = new DefaultTransitService(timetableRepository, timetableSnapshotBuffer); } @@ -31,6 +47,11 @@ public DefaultRealTimeUpdateContext(Graph graph, TimetableRepository timetableRe this(graph, timetableRepository, null); } + @Override + public MutableTimetableSnapshot mutableSnapshot() { + return timetableSnapshotBuffer; + } + @Override public Graph graph() { return graph; @@ -41,14 +62,6 @@ public TransitService transitService() { return transitService; } - @Override - public synchronized SiriFuzzyTripMatcher siriFuzzyTripMatcher() { - if (siriFuzzyTripMatcher == null) { - siriFuzzyTripMatcher = new SiriFuzzyTripMatcher(transitService); - } - return siriFuzzyTripMatcher; - } - @Override public GtfsRealtimeFuzzyTripMatcher gtfsRealtimeFuzzyTripMatcher() { return new GtfsRealtimeFuzzyTripMatcher(transitService); diff --git a/application/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java b/application/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java index 7d02f69ed09..33f7b4d82c6 100644 --- a/application/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java +++ b/application/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java @@ -7,7 +7,6 @@ import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -20,17 +19,14 @@ import org.slf4j.LoggerFactory; /** - * This class is attached to the graph: - * - *

- * GraphUpdaterManager updaterManager = graph.getUpdaterManager();
- * 
+ * Manages the lifecycle of all {@link GraphUpdater} instances: starts each updater on its own + * thread, shuts them down cleanly, and tracks readiness. *

- * Each updater will run in its own thread. When changes to the graph have to be made by these - * updaters, this should be done via the execute method of this manager to prevent race conditions - * between graph write operations. + * Write tasks submitted by updaters are serialised by the {@link WriteToGraphCallback} passed at + * construction — currently a {@link GraphWriterService}, which will be replaced by the new + * {@link org.opentripplanner.framework.transaction.UpdateManager} framework. */ -public class GraphUpdaterManager implements WriteToGraphCallback, GraphUpdaterStatus { +public class GraphUpdaterManager implements GraphUpdaterStatus { private static final Logger LOG = LoggerFactory.getLogger(GraphUpdaterManager.class); /** @@ -38,22 +34,11 @@ public class GraphUpdaterManager implements WriteToGraphCallback, GraphUpdaterSt */ private static final int MIN_POLLING_UPDATER_THREADS = 6; - /** - * OTP's multi-version concurrency control model for graph updating allows simultaneous reads, but - * never simultaneous writes. We ensure this policy is respected by having a single writer thread, - * which sequentially executes all graph updater tasks. Each task is a runnable that is scheduled - * with the ExecutorService to run at regular intervals. - * FIXME: In reality we're not using scheduleAtFixedInterval. - * We're scheduling for immediate execution from separate threads that sleep in a loop. - * We should perhaps switch to having polling GraphUpdaters call scheduleAtFixedInterval. - */ - private final ScheduledExecutorService scheduler; - private final ScheduledExecutorService pollingUpdaterPool; /** - * A pool of threads on which the non-polling updaters will run. This creates a pool that will auto-scale up - * to any size (maximum pool size is MAX_INT). + * A pool of threads on which the non-polling updaters will run. This creates a pool that will + * auto-scale up to any size (maximum pool size is MAX_INT). */ private final ExecutorService nonPollingUpdaterPool; @@ -62,30 +47,24 @@ public class GraphUpdaterManager implements WriteToGraphCallback, GraphUpdaterSt */ private final List updaterList = new ArrayList<>(); - /** - * The Graph that will be updated. - */ - private final RealTimeUpdateContext realtimeUpdateContext; + private final Runnable shutdownGraphWriter; - /** - * Constructor. - * - */ - public GraphUpdaterManager(RealTimeUpdateContext context, List updaters) { - this.realtimeUpdateContext = context; - // Thread factories used to create new threads, giving them more human-readable names. - var graphWriterThreadFactory = new ThreadFactoryBuilder().setNameFormat("graph-writer").build(); - this.scheduler = Executors.newSingleThreadScheduledExecutor(graphWriterThreadFactory); + public GraphUpdaterManager( + WriteToGraphCallback writeToGraphCallback, + Runnable shutdownGraphWriter, + List updaters + ) { var updaterThreadFactory = new ThreadFactoryBuilder().setNameFormat("updater-%d").build(); this.pollingUpdaterPool = Executors.newScheduledThreadPool( Math.max(MIN_POLLING_UPDATER_THREADS, Runtime.getRuntime().availableProcessors()), updaterThreadFactory ); this.nonPollingUpdaterPool = Executors.newCachedThreadPool(updaterThreadFactory); + this.shutdownGraphWriter = shutdownGraphWriter; for (GraphUpdater updater : updaters) { updaterList.add(updater); - updater.setup(this); + updater.setup(writeToGraphCallback); } } @@ -139,7 +118,6 @@ public void stop() { public void stop(boolean cancelRunningTasks) { // TODO: find a better way to stop these threads LOG.info("Stopping updater manager with {} updaters.", numberOfUpdaters()); - // Shutdown updaters if (cancelRunningTasks) { pollingUpdaterPool.shutdownNow(); nonPollingUpdaterPool.shutdownNow(); @@ -155,42 +133,18 @@ public void stop(boolean cancelRunningTasks) { if (!ok) { LOG.warn("Timeout waiting for updaters to finish."); } + shutdownGraphWriter.run(); } catch (InterruptedException e) { - // This should not happen LOG.warn("Interrupted while waiting for updaters to finish."); } - // Clean up updaters for (GraphUpdater updater : updaterList) { updater.teardown(); } updaterList.clear(); - - // Shutdown scheduler - scheduler.shutdownNow(); - try { - boolean ok = scheduler.awaitTermination(30, TimeUnit.SECONDS); - if (!ok) { - LOG.warn("Timeout waiting for scheduled task to finish."); - } - } catch (InterruptedException e) { - // This should not happen - LOG.warn("Interrupted while waiting for scheduled task to finish."); - } LOG.info("Stopped updater manager"); } - @Override - public Future execute(GraphWriterRunnable runnable) { - return scheduler.submit(() -> { - try { - runnable.run(realtimeUpdateContext); - } catch (Exception e) { - LOG.error("Error while running graph writer {}:", runnable.getClass().getName(), e); - } - }); - } - @Override public int numberOfUpdaters() { return updaterList.size(); @@ -210,10 +164,6 @@ public List listUnprimedUpdaters() { .collect(Collectors.toList()); } - /** - * Just an example of fetching status information from the graph updater manager to expose it in a - * web service. More useful stuff should be added later. - */ @Override public Map getUpdaterDescriptions() { Map ret = new TreeMap<>(); @@ -224,10 +174,6 @@ public Map getUpdaterDescriptions() { return ret; } - /** - * Just an example of fetching status information from the graph updater manager to expose it in a - * web service. More useful stuff should be added later. - */ public GraphUpdater getUpdater(int id) { if (id >= updaterList.size()) { return null; @@ -235,6 +181,7 @@ public GraphUpdater getUpdater(int id) { return updaterList.get(id); } + @Override public Class getUpdaterClass(int id) { GraphUpdater updater = getUpdater(id); return updater == null ? null : updater.getClass(); @@ -252,10 +199,6 @@ public ExecutorService getNonPollingUpdaterPool() { return nonPollingUpdaterPool; } - public ScheduledExecutorService getScheduler() { - return scheduler; - } - /** * This method start a task during startup and log a message when all updaters are initialized. * When all updaters are ready, then OTP is ready for processing routing requests. diff --git a/application/src/main/java/org/opentripplanner/updater/GraphWriterService.java b/application/src/main/java/org/opentripplanner/updater/GraphWriterService.java new file mode 100644 index 00000000000..7a5e1e53b6e --- /dev/null +++ b/application/src/main/java/org/opentripplanner/updater/GraphWriterService.java @@ -0,0 +1,61 @@ +package org.opentripplanner.updater; + +import java.util.concurrent.Future; +import org.opentripplanner.framework.transaction.UpdateManager; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; +import org.opentripplanner.street.graph.Graph; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Serialises all graph write operations by delegating to {@link UpdateManager}, which owns the + * single-threaded executor. Each write task receives a freshly-constructed + * {@link DefaultRealTimeUpdateContext} backed by the mutable timetable snapshot for that task. + *

+ * This class will eventually be removed once all updaters submit directly to {@link UpdateManager}. + */ +public class GraphWriterService implements WriteToGraphCallback { + + private static final Logger LOG = LoggerFactory.getLogger(GraphWriterService.class); + + private final UpdateManager updateManager; + private final RepositoryHandle< + ReadOnlyTimetableSnapshot, + MutableTimetableSnapshot + > timetableHandle; + private final Graph graph; + private final TimetableRepository timetableRepository; + + public GraphWriterService( + UpdateManager updateManager, + RepositoryHandle timetableHandle, + Graph graph, + TimetableRepository timetableRepository + ) { + this.updateManager = updateManager; + this.timetableHandle = timetableHandle; + this.graph = graph; + this.timetableRepository = timetableRepository; + } + + @Override + public Future execute(GraphWriterRunnable runnable) { + return updateManager.submit(ctx -> { + var mutableSnapshot = ctx.repository(timetableHandle); + var context = new DefaultRealTimeUpdateContext(graph, timetableRepository, mutableSnapshot); + try { + runnable.run(context); + } catch (Exception e) { + LOG.error("Error while running graph writer {}:", runnable.getClass().getName(), e); + } + }); + } + + public void stop() { + updateManager.shutdown(); + } +} diff --git a/application/src/main/java/org/opentripplanner/updater/RealTimeUpdateContext.java b/application/src/main/java/org/opentripplanner/updater/RealTimeUpdateContext.java index 42a1ec65ee8..f5f4ef8ef8e 100644 --- a/application/src/main/java/org/opentripplanner/updater/RealTimeUpdateContext.java +++ b/application/src/main/java/org/opentripplanner/updater/RealTimeUpdateContext.java @@ -1,16 +1,22 @@ package org.opentripplanner.updater; import org.opentripplanner.street.graph.Graph; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.trip.gtfs.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.trip.siri.EntityResolver; -import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcher; /** * Give access to the transit data and street model in the context of a real-time updater. * The services exposed should be used only from the GraphWriter thread. */ public interface RealTimeUpdateContext { + /** + * Return the mutable timetable snapshot (write buffer) for this update task. Callers must only + * use this from the single writer thread. + */ + MutableTimetableSnapshot mutableSnapshot(); + /** * Return the street model (graph). */ @@ -23,13 +29,6 @@ public interface RealTimeUpdateContext { */ TransitService transitService(); - /** - * Return a SIRI fuzzy trip matcher that can look up both scheduled and real-time data. - * The SIRI fuzzy trip matcher has access to all real-time updates applied so far, - * including those not yet committed in a published snapshot. - */ - SiriFuzzyTripMatcher siriFuzzyTripMatcher(); - /** * Return a GTFS-RT fuzzy trip matcher that can look up both scheduled and real-time data. * The GTFS-RT fuzzy trip matcher has access to all real-time updates applied so far, diff --git a/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandler.java b/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandler.java index ed86b694a9f..5e8bccfd76c 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandler.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; import org.opentripplanner.core.model.i18n.I18NString; import org.opentripplanner.core.model.i18n.NonLocalizedString; import org.opentripplanner.core.model.i18n.TranslatedString; @@ -21,6 +22,8 @@ import org.opentripplanner.updater.RealTimeUpdateContext; import org.opentripplanner.updater.alert.siri.mapping.AffectsMapper; import org.opentripplanner.updater.alert.siri.mapping.SiriSeverityMapper; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcher; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.org.siri.siri21.DefaultedTextStructure; @@ -54,17 +57,22 @@ public class SiriAlertsUpdateHandler { private final TransitAlertService transitAlertService; private final Duration earlyStart; + @Nullable + private final SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache; + /** * @param earlyStart display the alerts to users this long before their activePeriod begins */ public SiriAlertsUpdateHandler( String feedId, TransitAlertService transitAlertService, - Duration earlyStart + Duration earlyStart, + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache ) { this.feedId = feedId; this.transitAlertService = transitAlertService; this.earlyStart = earlyStart; + this.siriFuzzyTripMatcherCache = siriFuzzyTripMatcherCache; } public void update(ServiceDelivery delivery, RealTimeUpdateContext context) { @@ -181,12 +189,13 @@ private TransitAlert mapSituationToAlert( alert.withPriority(situation.getPriority().intValue()); } + var fuzzyTripMatcher = siriFuzzyTripMatcherCache != null + ? new SiriFuzzyTripMatcher(siriFuzzyTripMatcherCache, context.transitService()) + : null; alert.addEntites( - new AffectsMapper( - feedId, - context.siriFuzzyTripMatcher(), - context.transitService() - ).mapAffects(situation.getAffects()) + new AffectsMapper(feedId, fuzzyTripMatcher, context.transitService()).mapAffects( + situation.getAffects() + ) ); if (alert.entities().isEmpty()) { diff --git a/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriSXUpdater.java b/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriSXUpdater.java index bc2081239a8..8e1a89bd27c 100644 --- a/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriSXUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/alert/siri/SiriSXUpdater.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.Optional; import java.util.UUID; +import javax.annotation.Nullable; import org.opentripplanner.framework.io.OtpHttpClientException; import org.opentripplanner.framework.retry.OtpRetry; import org.opentripplanner.framework.retry.OtpRetryBuilder; @@ -15,6 +16,7 @@ import org.opentripplanner.updater.spi.PollingGraphUpdaterParameters; import org.opentripplanner.updater.support.siri.SiriLoader; import org.opentripplanner.updater.trip.UrlUpdaterParameters; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.opentripplanner.utils.tostring.ToStringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +49,7 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr public SiriSXUpdater( Parameters config, TimetableRepository timetableRepository, + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache, SiriLoader siriLoader ) { super(config); @@ -64,7 +67,8 @@ public SiriSXUpdater( this.updateHandler = new SiriAlertsUpdateHandler( config.feedId(), transitAlertService, - config.earlyStart() + config.earlyStart(), + siriFuzzyTripMatcherCache ); siriHttpLoader = siriLoader; diff --git a/application/src/main/java/org/opentripplanner/updater/configure/SiriUpdaterModule.java b/application/src/main/java/org/opentripplanner/updater/configure/SiriUpdaterModule.java index e7b0dee65fb..f5b58e252fa 100644 --- a/application/src/main/java/org/opentripplanner/updater/configure/SiriUpdaterModule.java +++ b/application/src/main/java/org/opentripplanner/updater/configure/SiriUpdaterModule.java @@ -1,6 +1,7 @@ package org.opentripplanner.updater.configure; import java.util.function.Consumer; +import javax.annotation.Nullable; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.alert.siri.SiriSXUpdater; import org.opentripplanner.updater.alert.siri.SiriSXUpdaterParameters; @@ -11,6 +12,7 @@ import org.opentripplanner.updater.support.siri.SiriHttpLoader; import org.opentripplanner.updater.support.siri.SiriLoader; import org.opentripplanner.updater.trip.metrics.TripUpdateMetrics; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.opentripplanner.updater.trip.siri.SiriRealTimeTripUpdateAdapter; import org.opentripplanner.updater.trip.siri.updater.DefaultSiriETUpdaterParameters; import org.opentripplanner.updater.trip.siri.updater.EstimatedTimetableSource; @@ -34,9 +36,15 @@ public static SiriETUpdater createSiriETUpdater( public static SiriSXUpdater createSiriSXUpdater( SiriSXUpdater.Parameters params, - TimetableRepository timetableRepository + TimetableRepository timetableRepository, + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache ) { - return new SiriSXUpdater(params, timetableRepository, createLoader(params)); + return new SiriSXUpdater( + params, + timetableRepository, + siriFuzzyTripMatcherCache, + createLoader(params) + ); } private static EstimatedTimetableSource createSource(SiriETUpdaterParameters params) { diff --git a/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index c4d57937c5f..02393a3bb9a 100644 --- a/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -3,7 +3,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; import org.opentripplanner.ext.carpooling.CarpoolingRepository; import org.opentripplanner.ext.carpooling.updater.SiriETCarpoolingUpdater; @@ -12,24 +12,26 @@ import org.opentripplanner.ext.vehiclerentalservicedirectory.VehicleRentalServiceDirectoryFetcher; import org.opentripplanner.ext.vehiclerentalservicedirectory.api.VehicleRentalServiceDirectoryFetcherParameters; import org.opentripplanner.framework.io.OtpHttpClientFactory; +import org.opentripplanner.framework.transaction.UpdateManager; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.vehicleparking.VehicleParkingRepository; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; import org.opentripplanner.street.graph.Graph; import org.opentripplanner.street.linking.VertexLinker; import org.opentripplanner.street.model.openinghours.OpeningHoursCalendarService; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; import org.opentripplanner.transit.service.TimetableRepository; -import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; +import org.opentripplanner.updater.GraphWriterService; import org.opentripplanner.updater.UpdatersParameters; import org.opentripplanner.updater.alert.gtfs.GtfsRealtimeAlertsUpdater; import org.opentripplanner.updater.spi.GraphUpdater; -import org.opentripplanner.updater.spi.TimetableSnapshotFlush; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.updater.trip.gtfs.GtfsRealTimeTripUpdateAdapter; import org.opentripplanner.updater.trip.gtfs.updater.http.PollingTripUpdater; import org.opentripplanner.updater.trip.gtfs.updater.mqtt.MqttGtfsRealtimeUpdater; +import org.opentripplanner.updater.trip.siri.SiriFuzzyTripMatcherCache; import org.opentripplanner.updater.trip.siri.SiriRealTimeTripUpdateAdapter; import org.opentripplanner.updater.trip.siri.updater.google.SiriETGooglePubsubUpdater; import org.opentripplanner.updater.vehicle_parking.AvailabilityDataSourceFactory; @@ -58,7 +60,14 @@ public class UpdaterConfigurator { private final VehicleRentalRepository vehicleRentalRepository; private final CarpoolingRepository carpoolingRepository; private final VehicleParkingRepository parkingRepository; - private final TimetableSnapshotManager snapshotManager; + private final UpdateManager updateManager; + private final RepositoryHandle< + ReadOnlyTimetableSnapshot, + MutableTimetableSnapshot + > timetableRepositoryHandle; + + @Nullable + private SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache; private UpdaterConfigurator( Graph graph, @@ -69,7 +78,8 @@ private UpdaterConfigurator( VehicleParkingRepository parkingRepository, TimetableRepository timetableRepository, CarpoolingRepository carpoolingRepository, - TimetableSnapshotManager snapshotManager, + UpdateManager updateManager, + RepositoryHandle timetableRepositoryHandle, UpdatersParameters updatersParameters ) { this.graph = graph; @@ -80,7 +90,8 @@ private UpdaterConfigurator( this.timetableRepository = timetableRepository; this.updatersParameters = updatersParameters; this.parkingRepository = parkingRepository; - this.snapshotManager = snapshotManager; + this.updateManager = updateManager; + this.timetableRepositoryHandle = timetableRepositoryHandle; this.carpoolingRepository = carpoolingRepository; } @@ -93,7 +104,8 @@ public static void configure( VehicleParkingRepository parkingRepository, TimetableRepository timetableRepository, CarpoolingRepository carpoolingRepository, - TimetableSnapshotManager snapshotManager, + UpdateManager updateManager, + RepositoryHandle timetableRepositoryHandle, UpdatersParameters updatersParameters ) { new UpdaterConfigurator( @@ -105,7 +117,8 @@ public static void configure( parkingRepository, timetableRepository, carpoolingRepository, - snapshotManager, + updateManager, + timetableRepositoryHandle, updatersParameters ).configure(); } @@ -122,14 +135,18 @@ private void configure() { ) ); - TimetableSnapshot timetableSnapshotBuffer = snapshotManager.getTimetableSnapshotBuffer(); - GraphUpdaterManager updaterManager = new GraphUpdaterManager( - new DefaultRealTimeUpdateContext(graph, timetableRepository, timetableSnapshotBuffer), + var graphWriterService = new GraphWriterService( + updateManager, + timetableRepositoryHandle, + graph, + timetableRepository + ); + var updaterManager = new GraphUpdaterManager( + graphWriterService, + graphWriterService::stop, updaters ); - configureTimetableSnapshotFlush(updaterManager, snapshotManager); - updaterManager.startUpdaters(); // Stop the updater manager if it contains nothing @@ -138,7 +155,7 @@ private void configure() { } // Otherwise add it to the graph else { - timetableRepository.setUpdaterManager(updaterManager); + timetableRepository.initUpdaterManager(updaterManager); } } @@ -197,22 +214,49 @@ private List createUpdatersFromConfig() { updaters.add(new PollingVehiclePositionUpdater(configItem, realtimeVehicleRepository)); } for (var configItem : updatersParameters.getSiriETUpdaterParameters()) { - updaters.add(SiriUpdaterModule.createSiriETUpdater(configItem, provideSiriAdapter())); + updaters.add( + SiriUpdaterModule.createSiriETUpdater( + configItem, + provideSiriAdapter(configItem.fuzzyTripMatching()) + ) + ); } for (var configItem : updatersParameters.getSiriETCarpoolingUpdaterParameters()) { updaters.add(new SiriETCarpoolingUpdater(configItem, carpoolingRepository)); } for (var configItem : updatersParameters.getSiriETLiteUpdaterParameters()) { - updaters.add(SiriUpdaterModule.createSiriETUpdater(configItem, provideSiriAdapter())); + updaters.add( + SiriUpdaterModule.createSiriETUpdater( + configItem, + provideSiriAdapter(configItem.fuzzyTripMatching()) + ) + ); } for (var configItem : updatersParameters.getSiriETGooglePubsubUpdaterParameters()) { - updaters.add(new SiriETGooglePubsubUpdater(configItem, provideSiriAdapter())); + updaters.add( + new SiriETGooglePubsubUpdater( + configItem, + provideSiriAdapter(configItem.fuzzyTripMatching()) + ) + ); } for (var configItem : updatersParameters.getSiriSXUpdaterParameters()) { - updaters.add(SiriUpdaterModule.createSiriSXUpdater(configItem, timetableRepository)); + updaters.add( + SiriUpdaterModule.createSiriSXUpdater( + configItem, + timetableRepository, + siriFuzzyTripMatcherCache() + ) + ); } for (var configItem : updatersParameters.getSiriSXLiteUpdaterParameters()) { - updaters.add(SiriUpdaterModule.createSiriSXUpdater(configItem, timetableRepository)); + updaters.add( + SiriUpdaterModule.createSiriSXUpdater( + configItem, + timetableRepository, + siriFuzzyTripMatcherCache() + ) + ); } for (var configItem : updatersParameters.getMqttGtfsRealtimeUpdaterParameters()) { updaters.add(new MqttGtfsRealtimeUpdater(configItem, provideGtfsAdapter())); @@ -235,46 +279,46 @@ private List createUpdatersFromConfig() { } } for (var configItem : updatersParameters.getSiriAzureETUpdaterParameters()) { - updaters.add(SiriAzureUpdater.createETUpdater(configItem, provideSiriAdapter())); + updaters.add( + SiriAzureUpdater.createETUpdater( + configItem, + provideSiriAdapter(configItem.isFuzzyTripMatching()) + ) + ); } for (var configItem : updatersParameters.getSiriAzureSXUpdaterParameters()) { - updaters.add(SiriAzureUpdater.createSXUpdater(configItem, timetableRepository)); + updaters.add( + SiriAzureUpdater.createSXUpdater( + configItem, + timetableRepository, + siriFuzzyTripMatcherCache() + ) + ); } for (var configItem : updatersParameters.getMqttSiriETUpdaterParameters()) { - updaters.add(new SiriETMqttUpdater(configItem, provideSiriAdapter())); + updaters.add( + new SiriETMqttUpdater(configItem, provideSiriAdapter(configItem.fuzzyTripMatching())) + ); } return updaters; } - private SiriRealTimeTripUpdateAdapter provideSiriAdapter() { - return new SiriRealTimeTripUpdateAdapter(timetableRepository, deduplicator, snapshotManager); + private SiriRealTimeTripUpdateAdapter provideSiriAdapter(boolean fuzzyTripMatching) { + var cache = fuzzyTripMatching ? siriFuzzyTripMatcherCache() : null; + return new SiriRealTimeTripUpdateAdapter(timetableRepository, deduplicator, cache); } - private GtfsRealTimeTripUpdateAdapter provideGtfsAdapter() { - return new GtfsRealTimeTripUpdateAdapter( - timetableRepository, - deduplicator, - snapshotManager, - () -> LocalDate.now(timetableRepository.getTimeZone()) - ); + private SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache() { + if (siriFuzzyTripMatcherCache == null) { + siriFuzzyTripMatcherCache = new SiriFuzzyTripMatcherCache(timetableRepository); + } + return siriFuzzyTripMatcherCache; } - /** - * If SIRI or GTFS real-time updaters are in use, configure a periodic flush of the timetable - * snapshot. - */ - private void configureTimetableSnapshotFlush( - GraphUpdaterManager updaterManager, - TimetableSnapshotManager snapshotManager - ) { - updaterManager - .getScheduler() - .scheduleWithFixedDelay( - new TimetableSnapshotFlush(snapshotManager), - 0, - updatersParameters.timetableSnapshotParameters().maxSnapshotFrequency().toSeconds(), - TimeUnit.SECONDS - ); + private GtfsRealTimeTripUpdateAdapter provideGtfsAdapter() { + return new GtfsRealTimeTripUpdateAdapter(timetableRepository, deduplicator, () -> + LocalDate.now(timetableRepository.getTimeZone()) + ); } } diff --git a/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java b/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java deleted file mode 100644 index 1d802ff0e9e..00000000000 --- a/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.opentripplanner.updater.spi; - -import org.opentripplanner.updater.trip.TimetableSnapshotManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Flush the timetable snapshot buffer by committing pending changes. - * Exceptions occurring during the flush are caught and ignored: the scheduler can then retry - * the task later. - */ -public class TimetableSnapshotFlush implements Runnable { - - private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotFlush.class); - - private final TimetableSnapshotManager snapshotManager; - - public TimetableSnapshotFlush(TimetableSnapshotManager snapshotManager) { - this.snapshotManager = snapshotManager; - } - - @Override - public void run() { - try { - LOG.debug("Flushing timetable snapshot buffer"); - snapshotManager.purgeAndCommit(); - LOG.debug("Flushed timetable snapshot buffer"); - } catch (Throwable t) { - LOG.error("Error flushing timetable snapshot buffer", t); - } - } -} diff --git a/application/src/main/java/org/opentripplanner/updater/spi/WriteToGraphCallback.java b/application/src/main/java/org/opentripplanner/updater/spi/WriteToGraphCallback.java index d7c07c7dab3..80a35a790d9 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/WriteToGraphCallback.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/WriteToGraphCallback.java @@ -1,16 +1,20 @@ package org.opentripplanner.updater.spi; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import org.opentripplanner.updater.GraphWriterRunnable; public interface WriteToGraphCallback { + /** + * A no-op callback that does nothing - useful for mocking in tests. + */ + WriteToGraphCallback NOOP = _ -> CompletableFuture.completedFuture(null); + /** * This is the method to use to modify the graph from the updaters. The runnables will be * scheduled after each other, guaranteeing that only one of these runnables will be active at any * time. If a particular GraphUpdater calls this method on more than one GraphWriterRunnable, they * should be executed in the same order that GraphUpdater made the calls. - * - * @param runnable is a graph writer runnable */ Future execute(GraphWriterRunnable runnable); } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java deleted file mode 100644 index 64084891cec..00000000000 --- a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java +++ /dev/null @@ -1,217 +0,0 @@ -package org.opentripplanner.updater.trip; - -import java.time.LocalDate; -import java.util.Objects; -import java.util.function.Supplier; -import javax.annotation.Nullable; -import org.opentripplanner.core.model.id.FeedScopedId; -import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; -import org.opentripplanner.routing.util.ConcurrentPublished; -import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; -import org.opentripplanner.transit.model.timetable.Timetable; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; -import org.opentripplanner.updater.spi.UpdateSuccess; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A class that abstracts away locking, updating, committing, and purging of the timetable snapshot. - */ -public final class TimetableSnapshotManager { - - private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotManager.class); - - /** - * The working copy of the timetable snapshot. Should not be visible to routing threads. - * By design, only one thread should ever be written to this buffer. - */ - private final TimetableSnapshot buffer; - - /** - * The last committed snapshot that was handed off to a routing thread. This snapshot may be given - * to more than one routing thread. - */ - private final ConcurrentPublished snapshot = new ConcurrentPublished<>(); - - /** - * Should expired real-time data be purged from the graph. - * TODO RT_AB: Clarify exactly what "purge" means and in what circumstances would one turn it off. - */ - private final boolean purgeExpiredData; - /** - * We inject a provider to retrieve the current service-date(now). This enables us to unit-test - * the purgeExpiredData feature. - */ - private final Supplier localDateNow; - - private LocalDate lastPurgeDate = null; - - /** - * Creates and immediately initializes the snapshot with the provided scheduled Raptor data. - *

- * This constructor is intended for production use where the Raptor data is computed before - * Dagger constructs this instance, avoiding a separate init call. - * - * @param localDateNow This supplier allows you to inject a custom lambda to - * override what is considered 'today'. This is useful for - * unit testing. - * @param scheduledRaptorTransitData The pre-computed scheduled Raptor transit data. - * @param tripCalendars The trip calendars copied for real-time updates. - */ - public TimetableSnapshotManager( - TimetableSnapshotParameters parameters, - Supplier localDateNow, - RaptorTransitData scheduledRaptorTransitData, - DefaultTripCalendars tripCalendars - ) { - this.purgeExpiredData = parameters.purgeExpiredData(); - this.localDateNow = Objects.requireNonNull(localDateNow); - this.buffer = new TimetableSnapshot(scheduledRaptorTransitData, tripCalendars); - commitTimetableSnapshot(true); - } - - /** - * @return an up-to-date snapshot of real-time data. This snapshot and the timetable objects it - * references are guaranteed to never change, so the requesting thread is - * provided a consistent view of all TripTimes. The routing thread need only release its reference - * to the snapshot to release resources. - */ - public TimetableSnapshot getTimetableSnapshot() { - return snapshot.get(); - } - - /** - * @return the current timetable snapshot buffer that contains pending changes (not yet published - * in a snapshot). - * This should be used in the context of an updater to build a TransitEditorService that sees all - * the changes applied so far by real-time updates. - */ - public TimetableSnapshot getTimetableSnapshotBuffer() { - return buffer; - } - - /** - * Request a commit of the timetable snapshot. - *

- * If there are no updates buffered up or not enough time has elapsed, the existing snapshot - * is returned. - * - * @param force Force the committing of a new snapshot even if the above conditions are not met. - */ - void commitTimetableSnapshot(final boolean force) { - if (force || buffer.isDirty()) { - snapshot.publish(buffer.commit(force)); - } - } - - /** - * Get the current trip pattern given a trip id and a service date, if it has been changed from - * the scheduled pattern with an update, for which the stopPattern is different. - * - * @param tripId trip id - * @param serviceDate service date - * @return trip pattern created by the updater; null if pattern has not been changed for this trip. - */ - @Nullable - public TripPattern getNewTripPatternForModifiedTrip(FeedScopedId tripId, LocalDate serviceDate) { - return buffer.getNewTripPatternForModifiedTrip(tripId, serviceDate); - } - - /** - * Make a snapshot after each message in anticipation of incoming requests. - * Purge data if necessary (and force new snapshot if anything was purged). - * Make sure that the public (locking) getTimetableSnapshot function is not called. - */ - public void purgeAndCommit() { - if (purgeExpiredData) { - final boolean modified = purgeExpiredData(); - commitTimetableSnapshot(modified); - } else { - commitTimetableSnapshot(false); - } - } - - /** - * Remove realtime data from previous service dates from the snapshot. This is useful so that - * instances that run for multiple days don't accumulate a lot of realtime data for past - * dates which would increase memory consumption. - * If your OTP instances are restarted throughout the day, this is less useful and can be - * turned off. - * - * @return true if any data has been modified and false if no purging has happened. - */ - private boolean purgeExpiredData() { - final LocalDate today = localDateNow.get(); - // TODO: Base this on numberOfDaysOfLongestTrip for tripPatterns - // Just to be safe... - final LocalDate previously = today.minusDays(2); - - // Purge data only if we have changed date - if (lastPurgeDate != null && lastPurgeDate.compareTo(previously) >= 0) { - return false; - } - - lastPurgeDate = previously; - - return buffer.purgeExpiredData(previously); - } - - /** - * Clear all data of snapshot for the provided feed id - */ - public void clearBuffer(String feedId) { - buffer.clear(feedId); - } - - /** - * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes - * does not exist yet in the Timetable, add it. This method will make a protective copy of the - * Timetable if such a copy has not already been made while building up this snapshot, handling - * both cases where patterns were pre-existing in static data or created by realtime data. - * - * @return whether the update was actually applied - */ - public UpdateSuccess updateBuffer(RealTimeTripUpdate realTimeTripUpdate) { - var trip = realTimeTripUpdate.updatedTripTimes().getTrip(); - var serviceDate = realTimeTripUpdate.serviceDate(); - - // Phase 1: Revert previous real-time modifications if requested - if (realTimeTripUpdate.revertPreviousRealTimeUpdates()) { - buffer.revertTripToScheduledTripPattern(trip.getId(), serviceDate); - } - - // Phase 2: Mark trip as deleted in scheduled pattern if moving to a modified pattern - var scheduledPattern = realTimeTripUpdate.hideTripInScheduledPattern(); - if (scheduledPattern != null) { - var scheduledTripTimes = scheduledPattern.getScheduledTimetable().getTripTimes(trip); - if (scheduledTripTimes != null) { - var builder = scheduledTripTimes.createRealTimeFromScheduledTimes(); - builder.withDeleted(); - buffer.update( - RealTimeTripUpdate.of(scheduledPattern, builder.build(), serviceDate).build() - ); - } else if (LOG.isDebugEnabled()) { - LOG.debug( - "Trip {} not found in scheduled pattern {}, skipping deletion.", - trip.getId(), - scheduledPattern.logName() - ); - } - } - - // Phase 3: Apply the main update - buffer.update(realTimeTripUpdate); - return UpdateSuccess.noWarnings(realTimeTripUpdate.producer()); - } - - /** - * Returns an updated timetable for the specified pattern if one is available in this snapshot, or - * the originally scheduled timetable if there are no updates in this snapshot. - */ - public Timetable resolve(TripPattern pattern, LocalDate serviceDate) { - return buffer.resolve(pattern, serviceDate); - } -} diff --git a/application/src/main/java/org/opentripplanner/updater/trip/TripUpdateApplier.java b/application/src/main/java/org/opentripplanner/updater/trip/TripUpdateApplier.java new file mode 100644 index 00000000000..4616c382fb1 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/updater/trip/TripUpdateApplier.java @@ -0,0 +1,56 @@ +package org.opentripplanner.updater.trip; + +import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.updater.spi.UpdateSuccess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Applies a {@link RealTimeTripUpdate} to a mutable timetable snapshot in three ordered phases: + *

    + *
  1. Revert any previous real-time modifications to the trip's pattern if requested.
  2. + *
  3. Mark the trip as deleted in its scheduled pattern when it is being moved to a modified + * pattern with a different stop sequence.
  4. + *
  5. Apply the main update.
  6. + *
+ */ +public class TripUpdateApplier { + + private static final Logger LOG = LoggerFactory.getLogger(TripUpdateApplier.class); + + private TripUpdateApplier() {} + + public static UpdateSuccess apply(MutableTimetableSnapshot buffer, RealTimeTripUpdate update) { + var trip = update.updatedTripTimes().getTrip(); + var serviceDate = update.serviceDate(); + + // Phase 1: Revert previous real-time modifications if requested + if (update.revertPreviousRealTimeUpdates()) { + buffer.revertTripToScheduledTripPattern(trip.getId(), serviceDate); + } + + // Phase 2: Mark trip as deleted in scheduled pattern if moving to a modified pattern + var scheduledPattern = update.hideTripInScheduledPattern(); + if (scheduledPattern != null) { + var scheduledTripTimes = scheduledPattern.getScheduledTimetable().getTripTimes(trip); + if (scheduledTripTimes != null) { + var builder = scheduledTripTimes.createRealTimeFromScheduledTimes(); + builder.withDeleted(); + buffer.update( + RealTimeTripUpdate.of(scheduledPattern, builder.build(), serviceDate).build() + ); + } else if (LOG.isDebugEnabled()) { + LOG.debug( + "Trip {} not found in scheduled pattern {}, skipping deletion.", + trip.getId(), + scheduledPattern.logName() + ); + } + } + + // Phase 3: Apply the main update + buffer.update(update); + return UpdateSuccess.noWarnings(update.producer()); + } +} diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/CanceledTripHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/CanceledTripHandler.java index 959ab7e034a..89b5cace8cf 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/CanceledTripHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/CanceledTripHandler.java @@ -7,10 +7,11 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.UpdateException; import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; +import org.opentripplanner.updater.trip.TripUpdateApplier; import org.opentripplanner.updater.trip.UpdateIncrementality; import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; @@ -22,14 +23,11 @@ class CanceledTripHandler { private final TransitEditorService transitEditorService; - private final TimetableSnapshotManager snapshotManager; + private final MutableTimetableSnapshot buffer; - CanceledTripHandler( - TransitEditorService transitEditorService, - TimetableSnapshotManager snapshotManager - ) { + CanceledTripHandler(TransitEditorService transitEditorService, MutableTimetableSnapshot buffer) { this.transitEditorService = transitEditorService; - this.snapshotManager = snapshotManager; + this.buffer = buffer; } UpdateSuccess cancel(TripUpdate tripUpdate, UpdateIncrementality incrementality) @@ -49,12 +47,12 @@ private UpdateSuccess handle( ) throws UpdateException { // For DIFFERENTIAL updates, try to cancel a previously added trip if (incrementality != FULL_DATASET) { - var addedPattern = snapshotManager.getNewTripPatternForModifiedTrip( + var addedPattern = buffer.getNewTripPatternForModifiedTrip( tripUpdate.tripId(), tripUpdate.startDate() ); if (addedPattern != null) { - var timetable = snapshotManager.resolve(addedPattern, tripUpdate.startDate()); + var timetable = buffer.resolve(addedPattern, tripUpdate.startDate()); if (timetable != null) { var tripTimes = timetable.getTripTimes(tripUpdate.tripId()); if (tripTimes != null && tripTimes.isAdded()) { @@ -63,7 +61,8 @@ private UpdateSuccess handle( case CANCEL -> builder.withCanceled(); case DELETE -> builder.withDeleted(); } - return snapshotManager.updateBuffer( + return TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(addedPattern, builder.build(), tripUpdate.startDate()).build() ); } @@ -87,7 +86,8 @@ private UpdateSuccess handle( case CANCEL -> builder.withCanceled(); case DELETE -> builder.withDeleted(); } - return snapshotManager.updateBuffer( + return TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(pattern, builder.build(), tripUpdate.startDate()) .withRevertPreviousRealTimeUpdates(true) .build() diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/DuplicatedTripHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/DuplicatedTripHandler.java index c15cad0a0f4..f5347ef9b76 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/DuplicatedTripHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/DuplicatedTripHandler.java @@ -10,10 +10,11 @@ import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.UpdateException; import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; +import org.opentripplanner.updater.trip.TripUpdateApplier; import org.opentripplanner.updater.trip.UpdateIncrementality; import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; @@ -22,16 +23,16 @@ class DuplicatedTripHandler { private final TransitEditorService transitEditorService; - private final TimetableSnapshotManager snapshotManager; + private final MutableTimetableSnapshot buffer; private final DeduplicatorService deduplicator; DuplicatedTripHandler( TransitEditorService transitEditorService, - TimetableSnapshotManager snapshotManager, + MutableTimetableSnapshot buffer, DeduplicatorService deduplicator ) { this.transitEditorService = transitEditorService; - this.snapshotManager = snapshotManager; + this.buffer = buffer; this.deduplicator = deduplicator; } @@ -101,7 +102,7 @@ UpdateSuccess handleDuplicated(TripUpdate tripUpdate, UpdateIncrementality updat .withTripCreation(true) .withAddedTripOnServiceDate(tripOnServiceDate) .build(); - return snapshotManager.updateBuffer(update); + return TripUpdateApplier.apply(buffer, update); } /// The spec is silent about how these ids should be constructed, so we create a new ID diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeTripUpdateAdapter.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeTripUpdateAdapter.java index b818ce32eed..cefd08e8ef2 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeTripUpdateAdapter.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeTripUpdateAdapter.java @@ -1,43 +1,26 @@ package org.opentripplanner.updater.trip.gtfs; -import static org.opentripplanner.updater.spi.UpdateErrorType.NOT_IMPLEMENTED_UNSCHEDULED; -import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; - -import com.google.transit.realtime.GtfsRealtime; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; import java.util.function.Supplier; -import javax.annotation.Nullable; import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; -import org.opentripplanner.transit.model.framework.DataValidationException; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; -import org.opentripplanner.updater.spi.DataValidationExceptionMapper; -import org.opentripplanner.updater.spi.ResultLogger; -import org.opentripplanner.updater.spi.UpdateError; -import org.opentripplanner.updater.spi.UpdateException; -import org.opentripplanner.updater.spi.UpdateResult; -import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; -import org.opentripplanner.updater.trip.UpdateIncrementality; -import org.opentripplanner.updater.trip.gtfs.interpolation.BackwardsDelayPropagationType; -import org.opentripplanner.updater.trip.gtfs.interpolation.ForwardsDelayPropagationType; -import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.trip.patterncache.TripPatternCache; import org.opentripplanner.updater.trip.patterncache.TripPatternIdGenerator; /** - * Adapts from GTFS-RT TripUpdates to OTP's internal real-time data model. + * Application-scoped factory for GTFS-RT trip update processing. Holds stable, application-lifetime + * state and produces a per-task {@link GtfsRealTimeUpdateHandler} via {@link #forUpdate(MutableTimetableSnapshot)}. */ public class GtfsRealTimeTripUpdateAdapter { - private final TimetableSnapshotManager snapshotManager; + private final TimetableRepository timetableRepository; private final Supplier localDateNow; - private final ScheduledTripHandler scheduledTripHandler; - private final NewTripHandler addedTripHandler; - private final DuplicatedTripHandler duplicatedTripHandler; - private final CanceledTripHandler canceledTripHandler; + private final TripPatternCache tripPatternCache; + private final TripTimesUpdater tripTimesUpdater; + private final DeduplicatorService deduplicator; /** * Constructor to allow tests to provide their own clock, not using system time. @@ -45,128 +28,30 @@ public class GtfsRealTimeTripUpdateAdapter { public GtfsRealTimeTripUpdateAdapter( TimetableRepository timetableRepository, DeduplicatorService deduplicator, - TimetableSnapshotManager snapshotManager, Supplier localDateNow ) { - this.snapshotManager = snapshotManager; + this.timetableRepository = timetableRepository; this.localDateNow = localDateNow; - - var transitEditorService = new DefaultTransitService( - timetableRepository, - snapshotManager.getTimetableSnapshotBuffer() - ); - var tripTimesUpdater = new TripTimesUpdater(timetableRepository.getTimeZone(), deduplicator); - var tripPatternCache = new TripPatternCache(new TripPatternIdGenerator()); - - this.scheduledTripHandler = new ScheduledTripHandler( - transitEditorService, - snapshotManager, - tripTimesUpdater, - tripPatternCache - ); - this.addedTripHandler = new NewTripHandler( - transitEditorService, - snapshotManager, - tripTimesUpdater, - tripPatternCache - ); - this.duplicatedTripHandler = new DuplicatedTripHandler( - transitEditorService, - snapshotManager, - deduplicator - ); - this.canceledTripHandler = new CanceledTripHandler(transitEditorService, snapshotManager); + this.tripPatternCache = new TripPatternCache(new TripPatternIdGenerator()); + this.tripTimesUpdater = new TripTimesUpdater(timetableRepository.getTimeZone(), deduplicator); + this.deduplicator = deduplicator; } /** - * Method to apply a trip update list to the most recent version of the timetable snapshot. A - * GTFS-RT feed is always applied against a single static feed (indicated by feedId). - *

- * However, multi-feed support is not completed and we currently assume there is only one static - * feed when matching IDs. - * - * @param backwardsDelayPropagationType Defines when delays are propagated to previous stops and - * if these stops are given the NO_DATA flag. - * @param updateIncrementality Determines the incrementality of the updates. FULL updates clear the buffer - * of all previous updates for the given feed id. - * @param updates GTFS-RT TripUpdate's that should be applied atomically + * Create an update-scoped task for applying GTFS-RT trip updates. The task holds sub-handlers + * backed by a {@link TransitEditorService} constructed from + * the given buffer, so all pattern and trip lookups within the task see in-progress real-time + * additions. */ - public UpdateResult applyTripUpdates( - @Nullable GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher, - ForwardsDelayPropagationType forwardsDelayPropagationType, - BackwardsDelayPropagationType backwardsDelayPropagationType, - UpdateIncrementality updateIncrementality, - List updates, - String feedId - ) { - List successes = new ArrayList<>(); - List errors = new ArrayList<>(); - - if (updateIncrementality == FULL_DATASET) { - // Remove all updates from the buffer - snapshotManager.clearBuffer(feedId); - } - - for (var rawTripUpdate : updates) { - UpdateSuccess result; - try { - if (fuzzyTripMatcher != null) { - var trip = fuzzyTripMatcher.match(feedId, rawTripUpdate.getTrip()); - rawTripUpdate = rawTripUpdate.toBuilder().setTrip(trip).build(); - } - - var tripUpdate = new TripUpdate(feedId, rawTripUpdate, localDateNow); - tripUpdate.validate(); - - result = applyUpdate( - tripUpdate, - updateIncrementality, - backwardsDelayPropagationType, - forwardsDelayPropagationType - ); - successes.add(result); - } catch (DataValidationException e) { - errors.add(DataValidationExceptionMapper.map(e).toError()); - } catch (UpdateException e) { - errors.add(e.toError()); - } - } - - var updateResult = UpdateResult.of(successes, errors); - - if (updateIncrementality == FULL_DATASET) { - ResultLogger.logUpdateResult(feedId, "gtfs-rt-trip-updates", updateResult); - } - return updateResult; - } - - private UpdateSuccess applyUpdate( - TripUpdate tripUpdate, - UpdateIncrementality updateIncrementality, - BackwardsDelayPropagationType backwardsDelayPropagationType, - ForwardsDelayPropagationType forwardsDelayPropagationType - ) throws UpdateException { - // The GTFS-RT TripDescriptor.schedule_relationship field is a protobuf optional enum, - // so a single TripUpdate message carries exactly one value — it is structurally impossible - // for a message to express two states (e.g. ADDED and CANCELED) at the same time. - // Cancelling a previously-added trip therefore always arrives as a second, separate feed - // entity carrying only CANCELED or DELETED. This is why the RealTimeTripTimesBuilder never - // needs to hold both added=true and canceled=true simultaneously for a GTFS-RT source. - return switch (tripUpdate.scheduleRelationship()) { - case SCHEDULED -> scheduledTripHandler.handle( - tripUpdate, - forwardsDelayPropagationType, - backwardsDelayPropagationType - ); - case NEW, ADDED -> addedTripHandler.handleNew(tripUpdate); - case CANCELED -> canceledTripHandler.cancel(tripUpdate, updateIncrementality); - case DELETED -> canceledTripHandler.delete(tripUpdate, updateIncrementality); - case DUPLICATED -> duplicatedTripHandler.handleDuplicated(tripUpdate, updateIncrementality); - case REPLACEMENT -> addedTripHandler.handleReplacement(tripUpdate); - case UNSCHEDULED -> throw UpdateException.of( - tripUpdate.tripId(), - NOT_IMPLEMENTED_UNSCHEDULED - ); - }; + public GtfsRealTimeUpdateHandler forUpdate(MutableTimetableSnapshot buffer) { + var editorService = new DefaultTransitService(timetableRepository, buffer); + return new GtfsRealTimeUpdateHandler( + buffer, + localDateNow, + new ScheduledTripHandler(editorService, buffer, tripTimesUpdater, tripPatternCache), + new NewTripHandler(editorService, buffer, tripTimesUpdater, tripPatternCache), + new CanceledTripHandler(editorService, buffer), + new DuplicatedTripHandler(editorService, buffer, deduplicator) + ); } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeUpdateHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeUpdateHandler.java new file mode 100644 index 00000000000..2f97743d46f --- /dev/null +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/GtfsRealTimeUpdateHandler.java @@ -0,0 +1,146 @@ +package org.opentripplanner.updater.trip.gtfs; + +import static org.opentripplanner.updater.spi.UpdateErrorType.NOT_IMPLEMENTED_UNSCHEDULED; +import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; + +import com.google.transit.realtime.GtfsRealtime; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.framework.DataValidationException; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.updater.spi.DataValidationExceptionMapper; +import org.opentripplanner.updater.spi.ResultLogger; +import org.opentripplanner.updater.spi.UpdateError; +import org.opentripplanner.updater.spi.UpdateException; +import org.opentripplanner.updater.spi.UpdateResult; +import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.UpdateIncrementality; +import org.opentripplanner.updater.trip.gtfs.interpolation.BackwardsDelayPropagationType; +import org.opentripplanner.updater.trip.gtfs.interpolation.ForwardsDelayPropagationType; +import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; + +/** + * Update-scoped object produced by {@link GtfsRealTimeTripUpdateAdapter#forUpdate}. Holds the + * per-task collaborators (sub-handlers constructed with an update-scoped {@code TransitEditorService}) + * and applies GTFS-RT trip updates against the mutable timetable snapshot. + */ +public class GtfsRealTimeUpdateHandler { + + private final MutableTimetableSnapshot buffer; + private final Supplier localDateNow; + private final ScheduledTripHandler scheduledTripHandler; + private final NewTripHandler addedTripHandler; + private final CanceledTripHandler canceledTripHandler; + private final DuplicatedTripHandler duplicatedTripHandler; + + GtfsRealTimeUpdateHandler( + MutableTimetableSnapshot buffer, + Supplier localDateNow, + ScheduledTripHandler scheduledTripHandler, + NewTripHandler addedTripHandler, + CanceledTripHandler canceledTripHandler, + DuplicatedTripHandler duplicatedTripHandler + ) { + this.buffer = buffer; + this.localDateNow = localDateNow; + this.scheduledTripHandler = scheduledTripHandler; + this.addedTripHandler = addedTripHandler; + this.canceledTripHandler = canceledTripHandler; + this.duplicatedTripHandler = duplicatedTripHandler; + } + + /** + * Method to apply a trip update list to the most recent version of the timetable snapshot. A + * GTFS-RT feed is always applied against a single static feed (indicated by feedId). + *

+ * However, multi-feed support is not completed, and we currently assume there is only one static + * feed when matching IDs. + * + * @param backwardsDelayPropagationType Defines when delays are propagated to previous stops and + * if these stops are given the NO_DATA flag. + * @param updateIncrementality Determines the incrementality of the updates. FULL updates clear the buffer + * of all previous updates for the given feed id. + * @param updates GTFS-RT TripUpdate's that should be applied atomically + */ + public UpdateResult applyTripUpdates( + @Nullable GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher, + ForwardsDelayPropagationType forwardsDelayPropagationType, + BackwardsDelayPropagationType backwardsDelayPropagationType, + UpdateIncrementality updateIncrementality, + List updates, + String feedId + ) { + List successes = new ArrayList<>(); + List errors = new ArrayList<>(); + + if (updateIncrementality == FULL_DATASET) { + // Remove all updates from the buffer + buffer.clear(feedId); + } + + for (var rawTripUpdate : updates) { + UpdateSuccess result; + try { + if (fuzzyTripMatcher != null) { + var trip = fuzzyTripMatcher.match(feedId, rawTripUpdate.getTrip()); + rawTripUpdate = rawTripUpdate.toBuilder().setTrip(trip).build(); + } + + var tripUpdate = new TripUpdate(feedId, rawTripUpdate, localDateNow); + tripUpdate.validate(); + + result = applyUpdate( + tripUpdate, + updateIncrementality, + backwardsDelayPropagationType, + forwardsDelayPropagationType + ); + successes.add(result); + } catch (DataValidationException e) { + errors.add(DataValidationExceptionMapper.map(e).toError()); + } catch (UpdateException e) { + errors.add(e.toError()); + } + } + + var updateResult = UpdateResult.of(successes, errors); + + if (updateIncrementality == FULL_DATASET) { + ResultLogger.logUpdateResult(feedId, "gtfs-rt-trip-updates", updateResult); + } + return updateResult; + } + + private UpdateSuccess applyUpdate( + TripUpdate tripUpdate, + UpdateIncrementality updateIncrementality, + BackwardsDelayPropagationType backwardsDelayPropagationType, + ForwardsDelayPropagationType forwardsDelayPropagationType + ) throws UpdateException { + // The GTFS-RT TripDescriptor.schedule_relationship field is a protobuf optional enum, + // so a single TripUpdate message carries exactly one value — it is structurally impossible + // for a message to express two states (e.g. ADDED and CANCELED) at the same time. + // Cancelling a previously-added trip therefore always arrives as a second, separate feed + // entity carrying only CANCELED or DELETED. This is why the RealTimeTripTimesBuilder never + // needs to hold both added=true and canceled=true simultaneously for a GTFS-RT source. + return switch (tripUpdate.scheduleRelationship()) { + case SCHEDULED -> scheduledTripHandler.handle( + tripUpdate, + forwardsDelayPropagationType, + backwardsDelayPropagationType + ); + case NEW, ADDED -> addedTripHandler.handleNew(tripUpdate); + case CANCELED -> canceledTripHandler.cancel(tripUpdate, updateIncrementality); + case DELETED -> canceledTripHandler.delete(tripUpdate, updateIncrementality); + case DUPLICATED -> duplicatedTripHandler.handleDuplicated(tripUpdate, updateIncrementality); + case REPLACEMENT -> addedTripHandler.handleReplacement(tripUpdate); + case UNSCHEDULED -> throw UpdateException.of( + tripUpdate.tripId(), + NOT_IMPLEMENTED_UNSCHEDULED + ); + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/NewTripHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/NewTripHandler.java index 9519961c8bd..48d4d70d154 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/NewTripHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/NewTripHandler.java @@ -18,10 +18,11 @@ import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.UpdateException; import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; +import org.opentripplanner.updater.trip.TripUpdateApplier; import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; import org.opentripplanner.updater.trip.patterncache.TripPatternCache; @@ -33,18 +34,18 @@ class NewTripHandler { private final TransitEditorService transitEditorService; - private final TimetableSnapshotManager snapshotManager; + private final MutableTimetableSnapshot buffer; private final TripTimesUpdater tripTimesUpdater; private final TripPatternCache tripPatternCache; NewTripHandler( TransitEditorService transitEditorService, - TimetableSnapshotManager snapshotManager, + MutableTimetableSnapshot buffer, TripTimesUpdater tripTimesUpdater, TripPatternCache tripPatternCache ) { this.transitEditorService = transitEditorService; - this.snapshotManager = snapshotManager; + this.buffer = buffer; this.tripTimesUpdater = tripTimesUpdater; this.tripPatternCache = tripPatternCache; } @@ -174,7 +175,7 @@ private UpdateSuccess addNewOrReplacementTripToSnapshot( ) .withTripCreation(true); } - return snapshotManager.updateBuffer(builder.build()); + return TripUpdateApplier.apply(buffer, builder.build()); } /** diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/ScheduledTripHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/ScheduledTripHandler.java index 2e4e49a66ad..6327fcd7980 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/ScheduledTripHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/ScheduledTripHandler.java @@ -12,10 +12,11 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.UpdateException; import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; +import org.opentripplanner.updater.trip.TripUpdateApplier; import org.opentripplanner.updater.trip.gtfs.interpolation.BackwardsDelayPropagationType; import org.opentripplanner.updater.trip.gtfs.interpolation.ForwardsDelayPropagationType; import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; @@ -29,18 +30,18 @@ class ScheduledTripHandler { private final TransitEditorService transitEditorService; - private final TimetableSnapshotManager snapshotManager; + private final MutableTimetableSnapshot buffer; private final TripTimesUpdater tripTimesUpdater; private final TripPatternCache tripPatternCache; ScheduledTripHandler( TransitEditorService transitEditorService, - TimetableSnapshotManager snapshotManager, + MutableTimetableSnapshot buffer, TripTimesUpdater tripTimesUpdater, TripPatternCache tripPatternCache ) { this.transitEditorService = transitEditorService; - this.snapshotManager = snapshotManager; + this.buffer = buffer; this.tripTimesUpdater = tripTimesUpdater; this.tripPatternCache = tripPatternCache; } @@ -104,14 +105,16 @@ UpdateSuccess handle( pattern ); - return snapshotManager.updateBuffer( + return TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(newPattern, updatedTripTimes, tripUpdate.startDate()) .withRevertPreviousRealTimeUpdates(true) .withHideTripInScheduledPattern(pattern) .build() ); } else { - return snapshotManager.updateBuffer( + return TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(pattern, updatedTripTimes, tripUpdate.startDate()) .withRevertPreviousRealTimeUpdates(true) .build() diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/updater/TripUpdateGraphWriterRunnable.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/updater/TripUpdateGraphWriterRunnable.java index a37100db46b..b08b90d03cf 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/updater/TripUpdateGraphWriterRunnable.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/updater/TripUpdateGraphWriterRunnable.java @@ -52,14 +52,16 @@ public TripUpdateGraphWriterRunnable( @Override public void run(RealTimeUpdateContext context) { - var result = adapter.applyTripUpdates( - fuzzyTripMatching ? context.gtfsRealtimeFuzzyTripMatcher() : null, - forwardsDelayPropagationType, - backwardsDelayPropagationType, - updateIncrementality, - updates, - feedId - ); + var result = adapter + .forUpdate(context.mutableSnapshot()) + .applyTripUpdates( + fuzzyTripMatching ? context.gtfsRealtimeFuzzyTripMatcher() : null, + forwardsDelayPropagationType, + backwardsDelayPropagationType, + updateIncrementality, + updates, + feedId + ); sendMetrics.accept(result); } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcher.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcher.java index 445ab8ef493..9e1d5256479 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcher.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcher.java @@ -8,15 +8,12 @@ import java.time.LocalDate; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; import org.opentripplanner.core.model.id.FeedScopedId; -import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.calendar.TripCalendars; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; @@ -32,29 +29,27 @@ import org.slf4j.LoggerFactory; /** - * This class is used for matching TripDescriptors without trip_ids to scheduled GTFS data and to - * feed back that information into a new TripDescriptor with proper trip_id. + * Matches SIRI TripDescriptors without trip IDs to scheduled GTFS data. *

- * It is mainly written for Entur (Norway) data, which doesn't yet have complete ID-based matching - * of SIRI messages to transit model objects. But it may be easily adaptable to other locations, as - * it's mainly looking at the last stop and arrival times of the scheduled trip. The matching - * process will always be applied even in places where you have good quality IDs in SIRI data and - * don't need it - we'd have to add a way to disable it. + * Written for Entur (Norway) data, which doesn't yet have complete ID-based matching of SIRI + * messages to transit model objects. Looks at the last stop and arrival times of the scheduled + * trip. The matching process will always be applied even where good quality IDs exist — a way to + * disable it would need to be added separately. *

- * The same instance of this SiriFuzzyTripMatcher may appear in different SIRI updaters. Be sure - * to fetch the instance at during the setup of the updaters, the initialization is not thread-safe. + * Constructed per update task (cheap — only assigns fields). The expensive scheduled-data index + * lives in {@link SiriFuzzyTripMatcherCache}, which is built once and shared across all SIRI + * updaters. */ public class SiriFuzzyTripMatcher { private static final Logger LOG = LoggerFactory.getLogger(SiriFuzzyTripMatcher.class); - private final Map> internalPlanningCodeCache = new HashMap<>(); - private final Map> startStopTripCache = new HashMap<>(); + private final SiriFuzzyTripMatcherCache cache; private final TransitService transitService; - public SiriFuzzyTripMatcher(TransitService transitService) { + public SiriFuzzyTripMatcher(SiriFuzzyTripMatcherCache cache, TransitService transitService) { + this.cache = cache; this.transitService = transitService; - initCache(this.transitService); } /** @@ -77,7 +72,7 @@ public TripAndPattern match( Set trips = null; if (journeyWrapper.internalPlanningCode() != null && journeyWrapper.isRail()) { - trips = getCachedTripsByInternalPlanningCode(journeyWrapper.internalPlanningCode()); + trips = cachedTripsByInternalPlanningCode(journeyWrapper.internalPlanningCode()); } if (trips == null || trips.isEmpty()) { @@ -128,7 +123,7 @@ public List getTripIdForInternalPlanningCodeServiceDate( LocalDate serviceDate ) { List matches = new ArrayList<>(); - for (Trip trip : getCachedTripsByInternalPlanningCode(internalPlanningCode)) { + for (Trip trip : cachedTripsByInternalPlanningCode(internalPlanningCode)) { Set serviceDates = transitService .getTripCalendars() .listServiceDates(trip.getServiceId()); @@ -140,43 +135,8 @@ public List getTripIdForInternalPlanningCodeServiceDate( return matches; } - private void initCache(TransitService index) { - for (Trip trip : index.listTrips()) { - TripPattern tripPattern = index.findPattern(trip); - - if (tripPattern == null) { - continue; - } - - if (tripPattern.getRoute().getMode().equals(TransitMode.RAIL)) { - String internalPlanningCode = trip.getNetexInternalPlanningCode(); - if (internalPlanningCode != null) { - internalPlanningCodeCache - .computeIfAbsent(internalPlanningCode, key -> new HashSet<>()) - .add(trip); - } - } - String lastStopId = tripPattern.lastStop().getId().getId(); - - TripTimes tripTimes = tripPattern.getScheduledTimetable().getTripTimes(trip); - if (tripTimes != null) { - int arrivalTime = tripTimes.getArrivalTime(tripTimes.getNumStops() - 1); - - String key = createStartStopKey(lastStopId, arrivalTime); - startStopTripCache.computeIfAbsent(key, k -> new HashSet<>()).add(trip); - } - } - - LOG.info("Built internalPlanningCode-cache [{}].", internalPlanningCodeCache.size()); - LOG.info("Built start-stop-cache [{}].", startStopTripCache.size()); - } - private static String createStartStopKey(RegularStop stop, int lastStopArrivalTime) { - return createStartStopKey(stop.getId().getId(), lastStopArrivalTime); - } - - private static String createStartStopKey(String lastStopId, int lastStopArrivalTime) { - return lastStopId + ":" + lastStopArrivalTime; + return stop.getId().getId() + ":" + lastStopArrivalTime; } private Set getMatchingTripsOnStopOrSiblings( @@ -194,10 +154,14 @@ private Set getMatchingTripsOnStopOrSiblings( transitService.getTimeZone() ); - Set trips = startStopTripCache.get(createStartStopKey(lastStop, secondsSinceMidnight)); + Set trips = cache.startStopTripCache.get( + createStartStopKey(lastStop, secondsSinceMidnight) + ); if (trips == null) { //Attempt to fetch trips that started yesterday - i.e. add 24 hours to arrival-time - trips = startStopTripCache.get(createStartStopKey(lastStop, secondsSinceMidnightYesterday)); + trips = cache.startStopTripCache.get( + createStartStopKey(lastStop, secondsSinceMidnightYesterday) + ); } if (trips != null) { @@ -212,8 +176,8 @@ private Set getMatchingTripsOnStopOrSiblings( trips = new HashSet<>(); var allQuays = lastStop.getParentStation().getChildStops(); for (var quay : allQuays) { - Set tripSet = startStopTripCache.get( - createStartStopKey(quay.getId().getId(), secondsSinceMidnight) + Set tripSet = cache.startStopTripCache.get( + quay.getId().getId() + ":" + secondsSinceMidnight ); if (tripSet != null) { trips.addAll(tripSet); @@ -222,11 +186,11 @@ private Set getMatchingTripsOnStopOrSiblings( return trips; } - private Set getCachedTripsByInternalPlanningCode(String internalPlanningCode) { + private Set cachedTripsByInternalPlanningCode(String internalPlanningCode) { if (internalPlanningCode == null) { return null; } - return internalPlanningCodeCache.getOrDefault(internalPlanningCode, new HashSet<>()); + return cache.internalPlanningCodeCache.getOrDefault(internalPlanningCode, new HashSet<>()); } /** diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherCache.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherCache.java new file mode 100644 index 00000000000..26f084136d5 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherCache.java @@ -0,0 +1,61 @@ +package org.opentripplanner.updater.trip.siri; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TimetableRepository; +import org.opentripplanner.transit.service.TransitService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Scheduled-data cache for {@link SiriFuzzyTripMatcher}. Built once from the static transit model + * and shared across all SIRI updaters. The maps are immutable after construction. + */ +public class SiriFuzzyTripMatcherCache { + + private static final Logger LOG = LoggerFactory.getLogger(SiriFuzzyTripMatcherCache.class); + + final Map> internalPlanningCodeCache = new HashMap<>(); + final Map> startStopTripCache = new HashMap<>(); + + public SiriFuzzyTripMatcherCache(TimetableRepository timetableRepository) { + initCache(new DefaultTransitService(timetableRepository, null)); + } + + private void initCache(TransitService index) { + for (Trip trip : index.listTrips()) { + TripPattern tripPattern = index.findPattern(trip); + + if (tripPattern == null) { + continue; + } + + if (tripPattern.getRoute().getMode().equals(TransitMode.RAIL)) { + String internalPlanningCode = trip.getNetexInternalPlanningCode(); + if (internalPlanningCode != null) { + internalPlanningCodeCache + .computeIfAbsent(internalPlanningCode, key -> new HashSet<>()) + .add(trip); + } + } + String lastStopId = tripPattern.lastStop().getId().getId(); + + TripTimes tripTimes = tripPattern.getScheduledTimetable().getTripTimes(trip); + if (tripTimes != null) { + int arrivalTime = tripTimes.getArrivalTime(tripTimes.getNumStops() - 1); + String key = lastStopId + ":" + arrivalTime; + startStopTripCache.computeIfAbsent(key, k -> new HashSet<>()).add(trip); + } + } + + LOG.info("Built internalPlanningCode-cache [{}].", internalPlanningCodeCache.size()); + LOG.info("Built start-stop-cache [{}].", startStopTripCache.size()); + } +} diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeTripUpdateAdapter.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeTripUpdateAdapter.java index 60a59a6344d..e692713d8a2 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeTripUpdateAdapter.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeTripUpdateAdapter.java @@ -1,47 +1,20 @@ package org.opentripplanner.updater.trip.siri; -import static org.opentripplanner.updater.spi.UpdateErrorType.NO_START_DATE; -import static org.opentripplanner.updater.spi.UpdateErrorType.TRIP_NOT_FOUND; -import static org.opentripplanner.updater.spi.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN; -import static org.opentripplanner.updater.spi.UpdateErrorType.UNKNOWN; -import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; import javax.annotation.Nullable; import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; -import org.opentripplanner.transit.model.framework.DataValidationException; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.timetable.RealTimeTripTimesBuilder; -import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; -import org.opentripplanner.transit.model.timetable.Timetable; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; -import org.opentripplanner.transit.service.TransitEditorService; -import org.opentripplanner.updater.spi.DataValidationExceptionMapper; -import org.opentripplanner.updater.spi.UpdateError; -import org.opentripplanner.updater.spi.UpdateException; -import org.opentripplanner.updater.spi.UpdateResult; -import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; -import org.opentripplanner.updater.trip.UpdateIncrementality; import org.opentripplanner.updater.trip.patterncache.TripPatternCache; import org.opentripplanner.updater.trip.patterncache.TripPatternIdGenerator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import uk.org.siri.siri21.EstimatedTimetableDeliveryStructure; -import uk.org.siri.siri21.EstimatedVehicleJourney; /** - * Adapts from SIRI-ET EstimatedTimetables to OTP's internal real-time data model. + * Application-scoped factory for SIRI-ET estimated timetable processing. Holds stable, + * application-lifetime state and produces a per-task {@link SiriRealTimeUpdateHandler} via + * {@link #forUpdate(MutableTimetableSnapshot)}. */ public class SiriRealTimeTripUpdateAdapter { - private static final Logger LOG = LoggerFactory.getLogger(SiriRealTimeTripUpdateAdapter.class); - /** * Use an id generator to generate TripPattern ids for new TripPatterns created by RealTime * updates. @@ -53,342 +26,40 @@ public class SiriRealTimeTripUpdateAdapter { */ private final TripPatternCache tripPatternCache; - /** - * Long-lived transit editor service that has access to the timetable snapshot buffer. - * This differs from the usual use case where the transit service refers to the latest published - * timetable snapshot. - */ - private final TransitEditorService transitEditorService; - private final DeduplicatorService deduplicator; - private final TimetableSnapshotManager snapshotManager; + private final TimetableRepository timetableRepository; + + @Nullable + private final SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache; public SiriRealTimeTripUpdateAdapter( TimetableRepository timetableRepository, DeduplicatorService deduplicator, - TimetableSnapshotManager snapshotManager + @Nullable SiriFuzzyTripMatcherCache siriFuzzyTripMatcherCache ) { this.deduplicator = deduplicator; - this.snapshotManager = snapshotManager; - this.transitEditorService = new DefaultTransitService( - timetableRepository, - snapshotManager.getTimetableSnapshotBuffer() - ); + this.timetableRepository = timetableRepository; + this.siriFuzzyTripMatcherCache = siriFuzzyTripMatcherCache; this.tripPatternCache = new TripPatternCache(tripPatternIdGenerator); } /** - * Method to apply estimated timetables to the most recent version of the timetable snapshot. - * - * @param incrementality the incrementality of the update, for example if updates represent all - * updates that are active right now, i.e. all previous updates should be - * disregarded - * @param updates SIRI EstimatedTimetable deliveries that should be applied atomically. + * Create an update-scoped task for applying SIRI-ET estimated timetables. The task holds a + * {@link org.opentripplanner.transit.service.TransitEditorService} constructed from the given + * buffer, so all pattern and trip lookups within the task see in-progress real-time additions. */ - public UpdateResult applyEstimatedTimetable( - @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, - EntityResolver entityResolver, - String feedId, - UpdateIncrementality incrementality, - List updates - ) { - if (updates == null) { - LOG.warn("updates is null"); - return UpdateResult.empty(); - } - - List successes = new ArrayList<>(); - List errors = new ArrayList<>(); - - if (incrementality == FULL_DATASET) { - // Remove all updates from the buffer - snapshotManager.clearBuffer(feedId); - } - - for (var etDelivery : updates) { - for (var estimatedJourneyVersion : etDelivery.getEstimatedJourneyVersionFrames()) { - var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies(); - LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size()); - for (EstimatedVehicleJourney journey : journeys) { - try { - successes.add(apply(journey, transitEditorService, fuzzyTripMatcher, entityResolver)); - } catch (UpdateException e) { - errors.add( - e - .withTripReference(DebugString.tripReference(journey)) - .toError(journey.getDataSource()) - ); - } - } - } - } - - LOG.debug("message contains {} trip updates", updates.size()); - - return UpdateResult.of(successes, errors); - } - - private UpdateSuccess apply( - EstimatedVehicleJourney journey, - TransitEditorService transitService, - @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, - EntityResolver entityResolver - ) throws UpdateException { - var journeyWrapper = EstimatedVehicleJourneyWrapper.of(journey); - SiriUpdateType siriUpdateType = null; - try { - siriUpdateType = updateType(journeyWrapper, entityResolver); - TripUpdate result = switch (siriUpdateType) { - case REPLACEMENT_DEPARTURE -> new AddedTripBuilder( - journeyWrapper, - transitService, - deduplicator, - entityResolver, - tripPatternIdGenerator::generateUniqueTripPatternId - ).build(); - case EXTRA_CALL -> handleExtraCall(fuzzyTripMatcher, entityResolver, journeyWrapper); - case TRIP_UPDATE -> handleModifiedTrip(fuzzyTripMatcher, entityResolver, journeyWrapper); - }; - - /* commit */ - return addTripToGraphAndBuffer(result); - } catch (UpdateException e) { - throw e; - } catch (DataValidationException e) { - throw DataValidationExceptionMapper.map(e); - } catch (Exception e) { - LOG.warn("{} EstimatedJourney {} failed.", siriUpdateType, journeyWrapper.debugString(), e); - throw UpdateException.noTripId(UNKNOWN); - } - } - - /** - * Determines the type of SIRI-ET update carried by {@code vehicleJourney}. - * - *

Why ExtraJourney and Cancellation are never both true in the same message

- * - * In the SIRI 2.0/2.1 XSD (and in the Nordic SIRI profile), the {@code ExtraJourney} and - * {@code Cancellation} elements of {@code EstimatedVehicleJourney} are enclosed in an - * {@code } group: - * - *
{@code
-   * 
-   *   
-   *   
-   * 
-   * }
- * - * This means a single {@code EstimatedVehicleJourney} is schema-invalid if it contains both - * elements. Cancelling a previously-added extra journey therefore always arrives as a second, - * separate {@code ServiceDelivery} that carries only {@code Cancellation=true} (and no - * {@code ExtraJourney} element). That second message is routed here as {@code TRIP_UPDATE} - * (because {@code isExtraJourney()} is {@code null}/false), and {@code ModifiedTripBuilder} - * handles the cancellation. - * - *

This is why the {@link RealTimeTripTimesBuilder} never needs to hold both - * {@code added=true} and {@code canceled=true} at the same time for a SIRI source. - */ - private SiriUpdateType updateType( - EstimatedVehicleJourneyWrapper journey, - EntityResolver entityResolver - ) { - // Extra call if at least one of the call is an extra call - if (journey.hasExtraCall()) { - return SiriUpdateType.EXTRA_CALL; - } - - // Replacement departure if the trip is marked as extra journey, and it has not been added before - if (journey.isExtraJourney() && entityResolver.resolveTrip(journey) == null) { - return SiriUpdateType.REPLACEMENT_DEPARTURE; - } - - // otherwise this is a trip update - return SiriUpdateType.TRIP_UPDATE; - } - - /** - * Get the latest timetable for TripPattern for a given service date. - *

- * Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not. - */ - private Timetable getCurrentTimetable(TripPattern tripPattern, LocalDate serviceDate) { - return snapshotManager.resolve(tripPattern, serviceDate); - } - - private TripUpdate handleModifiedTrip( - @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, - EntityResolver entityResolver, - EstimatedVehicleJourneyWrapper journey - ) throws UpdateException { - Trip trip = entityResolver.resolveTrip(journey); - - LocalDate serviceDate = entityResolver.resolveServiceDate(journey); - - if (serviceDate == null) { - throw UpdateException.of(trip != null ? trip.getId() : null, NO_START_DATE); - } - - TripPattern pattern; - - if (trip != null) { - // Found exact match - pattern = transitEditorService.findPattern(trip); - } else if (fuzzyTripMatcher != null) { - // No exact match found - search for trips based on arrival-times/stop-patterns - var tripAndPattern = fuzzyTripMatcher.match( - journey, - entityResolver, - this::getCurrentTimetable, - snapshotManager::getNewTripPatternForModifiedTrip - ); - trip = tripAndPattern.trip(); - pattern = tripAndPattern.tripPattern(); - } else { - throw UpdateException.of(null, TRIP_NOT_FOUND); - } - - Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate); - TripTimes existingTripTimes = currentTimetable.getTripTimes(trip); - if (existingTripTimes == null) { - LOG.debug("tripId {} not found in pattern.", trip.getId()); - throw UpdateException.of(trip.getId(), TRIP_NOT_FOUND_IN_PATTERN); - } - var tripUpdate = new ModifiedTripBuilder( - existingTripTimes, - pattern, - journey, - serviceDate, - transitEditorService.getTimeZone(), - entityResolver - ).build(); - - TripPattern deleteFrom = !tripUpdate.stopPattern().equals(pattern.getStopPattern()) - ? pattern + public SiriRealTimeUpdateHandler forUpdate(MutableTimetableSnapshot buffer) { + var transitService = new DefaultTransitService(timetableRepository, buffer); + var fuzzyTripMatcher = siriFuzzyTripMatcherCache != null + ? new SiriFuzzyTripMatcher(siriFuzzyTripMatcherCache, transitService) : null; - - return tripUpdate.withHideTripInScheduledPattern(deleteFrom); - } - - private TripUpdate handleExtraCall( - @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, - EntityResolver entityResolver, - EstimatedVehicleJourneyWrapper journey - ) throws UpdateException { - Trip trip = entityResolver.resolveTrip(journey); - - LocalDate serviceDate = entityResolver.resolveServiceDate(journey); - - if (serviceDate == null) { - throw UpdateException.of(trip != null ? trip.getId() : null, NO_START_DATE); - } - - TripPattern pattern; - - if (trip != null) { - // Found exact match - pattern = transitEditorService.findPattern(trip); - } else if (fuzzyTripMatcher != null) { - // No exact match found - search for trips based on arrival-times/stop-patterns - var tripAndPattern = fuzzyTripMatcher.match( - journey, - entityResolver, - this::getCurrentTimetable, - snapshotManager::getNewTripPatternForModifiedTrip - ); - - trip = tripAndPattern.trip(); - pattern = tripAndPattern.tripPattern(); - } else { - throw UpdateException.of(null, TRIP_NOT_FOUND); - } - - Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate); - TripTimes existingTripTimes = currentTimetable.getTripTimes(trip); - if (existingTripTimes == null) { - LOG.debug("tripId {} not found in pattern.", trip.getId()); - throw UpdateException.of(trip.getId(), TRIP_NOT_FOUND_IN_PATTERN); - } - var tripUpdate = new ExtraCallTripBuilder( - journey, - transitEditorService, + return new SiriRealTimeUpdateHandler( + transitService, + buffer, + fuzzyTripMatcher, + tripPatternCache, deduplicator, - entityResolver, - tripPatternIdGenerator::generateUniqueTripPatternId, - trip - ).build(); - - TripPattern deleteFrom = !tripUpdate.stopPattern().equals(pattern.getStopPattern()) - ? pattern - : null; - - return tripUpdate.withHideTripInScheduledPattern(deleteFrom); - } - - /** - * Add a (new) trip to the timetableRepository and the buffer - */ - private UpdateSuccess addTripToGraphAndBuffer(TripUpdate tripUpdate) { - Trip trip = tripUpdate.tripTimes().getTrip(); - LocalDate serviceDate = tripUpdate.serviceDate(); - - final TripPattern pattern; - if (tripUpdate.tripPatternCreation()) { - pattern = tripUpdate.addedTripPattern(); - } else { - // Get cached trip pattern or create one if it doesn't exist yet - pattern = tripPatternCache.getOrCreateTripPattern( - tripUpdate.stopPattern(), - trip, - transitEditorService.findPattern(trip) - ); - } - - // Revert for TRIP_UPDATE and EXTRA_CALL, but NOT for REPLACEMENT_DEPARTURE (new trips) - boolean revertPreviousRealTimeUpdates = !tripUpdate.tripCreation(); - - // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. - RealTimeTripUpdate realTimeTripUpdate = RealTimeTripUpdate.of( - pattern, - tripUpdate.tripTimes(), - serviceDate - ) - .withAddedTripOnServiceDate(tripUpdate.addedTripOnServiceDate()) - .withTripCreation(tripUpdate.tripCreation()) - .withRouteCreation(tripUpdate.routeCreation()) - .withProducer(tripUpdate.dataSource()) - .withRevertPreviousRealTimeUpdates(revertPreviousRealTimeUpdates) - .withHideTripInScheduledPattern(tripUpdate.hideTripInScheduledPattern()) - .build(); - var result = snapshotManager.updateBuffer(realTimeTripUpdate); - LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); - return result; - } - - /** - * Types of SIRI update messages. - */ - private enum SiriUpdateType { - /** - * Update of an existing trip. - * This can be either a trip defined in planned data or a replacement departure - * that was previously added by a real-time message. - * The update can consist in updated passing times and/or cancellation of some stops. - * A stop can be substituted by another if they belong to the same station. - * The whole trip can also be marked as cancelled. - */ - TRIP_UPDATE, - - /** - * Addition of a new trip, not currently present in the system. - * The new trip has a new unique id. - * The trip can replace one or more existing trips, another SIRI message should handle the - * cancellation of the replaced trips. - */ - REPLACEMENT_DEPARTURE, - - /** - * Addition of one or more stops in an existing trip. - */ - EXTRA_CALL, + tripPatternIdGenerator + ); } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeUpdateHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeUpdateHandler.java new file mode 100644 index 00000000000..659dcafa3ea --- /dev/null +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/SiriRealTimeUpdateHandler.java @@ -0,0 +1,384 @@ +package org.opentripplanner.updater.trip.siri; + +import static org.opentripplanner.updater.spi.UpdateErrorType.NO_START_DATE; +import static org.opentripplanner.updater.spi.UpdateErrorType.TRIP_NOT_FOUND; +import static org.opentripplanner.updater.spi.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN; +import static org.opentripplanner.updater.spi.UpdateErrorType.UNKNOWN; +import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; +import org.opentripplanner.transit.model.framework.DataValidationException; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimesBuilder; +import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; +import org.opentripplanner.transit.model.timetable.Timetable; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.service.TransitEditorService; +import org.opentripplanner.updater.spi.DataValidationExceptionMapper; +import org.opentripplanner.updater.spi.UpdateError; +import org.opentripplanner.updater.spi.UpdateException; +import org.opentripplanner.updater.spi.UpdateResult; +import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.TripUpdateApplier; +import org.opentripplanner.updater.trip.UpdateIncrementality; +import org.opentripplanner.updater.trip.patterncache.TripPatternCache; +import org.opentripplanner.updater.trip.patterncache.TripPatternIdGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.org.siri.siri21.EstimatedTimetableDeliveryStructure; +import uk.org.siri.siri21.EstimatedVehicleJourney; + +/** + * Update-scoped object produced by {@link SiriRealTimeTripUpdateAdapter#forUpdate}. Holds a + * per-task {@link TransitEditorService} backed by the update's mutable timetable snapshot, so all + * pattern and trip lookups within the task see in-progress real-time additions. + */ +public class SiriRealTimeUpdateHandler { + + private static final Logger LOG = LoggerFactory.getLogger(SiriRealTimeUpdateHandler.class); + + private final TransitEditorService transitEditorService; + private final MutableTimetableSnapshot buffer; + + @Nullable + private final SiriFuzzyTripMatcher fuzzyTripMatcher; + + private final TripPatternCache tripPatternCache; + private final DeduplicatorService deduplicator; + private final TripPatternIdGenerator tripPatternIdGenerator; + + SiriRealTimeUpdateHandler( + TransitEditorService transitEditorService, + MutableTimetableSnapshot buffer, + @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, + TripPatternCache tripPatternCache, + DeduplicatorService deduplicator, + TripPatternIdGenerator tripPatternIdGenerator + ) { + this.transitEditorService = transitEditorService; + this.buffer = buffer; + this.fuzzyTripMatcher = fuzzyTripMatcher; + this.tripPatternCache = tripPatternCache; + this.deduplicator = deduplicator; + this.tripPatternIdGenerator = tripPatternIdGenerator; + } + + /** + * Method to apply estimated timetables to the most recent version of the timetable snapshot. + * + * @param incrementality the incrementality of the update, for example if updates represent all + * updates that are active right now, i.e. all previous updates should be + * disregarded + * @param updates SIRI EstimatedTimetable deliveries that should be applied atomically. + */ + public UpdateResult applyEstimatedTimetable( + EntityResolver entityResolver, + String feedId, + UpdateIncrementality incrementality, + List updates + ) { + if (updates == null) { + LOG.warn("updates is null"); + return UpdateResult.empty(); + } + + List successes = new ArrayList<>(); + List errors = new ArrayList<>(); + + if (incrementality == FULL_DATASET) { + // Remove all updates from the buffer + buffer.clear(feedId); + } + + for (var etDelivery : updates) { + for (var estimatedJourneyVersion : etDelivery.getEstimatedJourneyVersionFrames()) { + var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies(); + LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size()); + for (EstimatedVehicleJourney journey : journeys) { + try { + successes.add(apply(journey, transitEditorService, entityResolver)); + } catch (UpdateException e) { + errors.add( + e + .withTripReference(DebugString.tripReference(journey)) + .toError(journey.getDataSource()) + ); + } + } + } + } + + LOG.debug("message contains {} trip updates", updates.size()); + + return UpdateResult.of(successes, errors); + } + + private UpdateSuccess apply( + EstimatedVehicleJourney journey, + TransitEditorService transitService, + EntityResolver entityResolver + ) throws UpdateException { + var journeyWrapper = EstimatedVehicleJourneyWrapper.of(journey); + SiriUpdateType siriUpdateType = null; + try { + siriUpdateType = updateType(journeyWrapper, entityResolver); + TripUpdate result = switch (siriUpdateType) { + case REPLACEMENT_DEPARTURE -> new AddedTripBuilder( + journeyWrapper, + transitService, + deduplicator, + entityResolver, + tripPatternIdGenerator::generateUniqueTripPatternId + ).build(); + case EXTRA_CALL -> handleExtraCall(fuzzyTripMatcher, entityResolver, journeyWrapper); + case TRIP_UPDATE -> handleModifiedTrip(fuzzyTripMatcher, entityResolver, journeyWrapper); + }; + + /* commit */ + return addTripToGraphAndBuffer(result); + } catch (UpdateException e) { + throw e; + } catch (DataValidationException e) { + throw DataValidationExceptionMapper.map(e); + } catch (Exception e) { + LOG.warn("{} EstimatedJourney {} failed.", siriUpdateType, journeyWrapper.debugString(), e); + throw UpdateException.noTripId(UNKNOWN); + } + } + + /** + * Determines the type of SIRI-ET update carried by {@code vehicleJourney}. + * + *

Why ExtraJourney and Cancellation are never both true in the same message

+ * + * In the SIRI 2.0/2.1 XSD (and in the Nordic SIRI profile), the {@code ExtraJourney} and + * {@code Cancellation} elements of {@code EstimatedVehicleJourney} are enclosed in an + * {@code } group: + * + *
{@code
+   * 
+   *   
+   *   
+   * 
+   * }
+ * + * This means a single {@code EstimatedVehicleJourney} is schema-invalid if it contains both + * elements. Cancelling a previously-added extra journey therefore always arrives as a second, + * separate {@code ServiceDelivery} that carries only {@code Cancellation=true} (and no + * {@code ExtraJourney} element). That second message is routed here as {@code TRIP_UPDATE} + * (because {@code isExtraJourney()} is {@code null}/false), and {@code ModifiedTripBuilder} + * handles the cancellation. + * + *

This is why the {@link RealTimeTripTimesBuilder} never needs to hold both + * {@code added=true} and {@code canceled=true} at the same time for a SIRI source. + */ + private SiriUpdateType updateType( + EstimatedVehicleJourneyWrapper journey, + EntityResolver entityResolver + ) { + // Extra call if at least one of the call is an extra call + if (journey.hasExtraCall()) { + return SiriUpdateType.EXTRA_CALL; + } + + // Replacement departure if the trip is marked as extra journey, and it has not been added before + if (journey.isExtraJourney() && entityResolver.resolveTrip(journey) == null) { + return SiriUpdateType.REPLACEMENT_DEPARTURE; + } + + // otherwise this is a trip update + return SiriUpdateType.TRIP_UPDATE; + } + + /** + * Get the latest timetable for TripPattern for a given service date. + *

+ * Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not. + */ + private Timetable getCurrentTimetable(TripPattern tripPattern, LocalDate serviceDate) { + return buffer.resolve(tripPattern, serviceDate); + } + + private TripUpdate handleModifiedTrip( + @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, + EntityResolver entityResolver, + EstimatedVehicleJourneyWrapper journey + ) throws UpdateException { + Trip trip = entityResolver.resolveTrip(journey); + + LocalDate serviceDate = entityResolver.resolveServiceDate(journey); + + if (serviceDate == null) { + throw UpdateException.of(trip != null ? trip.getId() : null, NO_START_DATE); + } + + TripPattern pattern; + + if (trip != null) { + // Found exact match + pattern = transitEditorService.findPattern(trip); + } else if (fuzzyTripMatcher != null) { + // No exact match found - search for trips based on arrival-times/stop-patterns + var tripAndPattern = fuzzyTripMatcher.match( + journey, + entityResolver, + this::getCurrentTimetable, + buffer::getNewTripPatternForModifiedTrip + ); + trip = tripAndPattern.trip(); + pattern = tripAndPattern.tripPattern(); + } else { + throw UpdateException.of(null, TRIP_NOT_FOUND); + } + + Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate); + TripTimes existingTripTimes = currentTimetable.getTripTimes(trip); + if (existingTripTimes == null) { + LOG.debug("tripId {} not found in pattern.", trip.getId()); + throw UpdateException.of(trip.getId(), TRIP_NOT_FOUND_IN_PATTERN); + } + var tripUpdate = new ModifiedTripBuilder( + existingTripTimes, + pattern, + journey, + serviceDate, + transitEditorService.getTimeZone(), + entityResolver + ).build(); + + TripPattern deleteFrom = !tripUpdate.stopPattern().equals(pattern.getStopPattern()) + ? pattern + : null; + + return tripUpdate.withHideTripInScheduledPattern(deleteFrom); + } + + private TripUpdate handleExtraCall( + @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, + EntityResolver entityResolver, + EstimatedVehicleJourneyWrapper journey + ) throws UpdateException { + Trip trip = entityResolver.resolveTrip(journey); + + LocalDate serviceDate = entityResolver.resolveServiceDate(journey); + + if (serviceDate == null) { + throw UpdateException.of(trip != null ? trip.getId() : null, NO_START_DATE); + } + + TripPattern pattern; + + if (trip != null) { + // Found exact match + pattern = transitEditorService.findPattern(trip); + } else if (fuzzyTripMatcher != null) { + // No exact match found - search for trips based on arrival-times/stop-patterns + var tripAndPattern = fuzzyTripMatcher.match( + journey, + entityResolver, + this::getCurrentTimetable, + buffer::getNewTripPatternForModifiedTrip + ); + + trip = tripAndPattern.trip(); + pattern = tripAndPattern.tripPattern(); + } else { + throw UpdateException.of(null, TRIP_NOT_FOUND); + } + + Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate); + TripTimes existingTripTimes = currentTimetable.getTripTimes(trip); + if (existingTripTimes == null) { + LOG.debug("tripId {} not found in pattern.", trip.getId()); + throw UpdateException.of(trip.getId(), TRIP_NOT_FOUND_IN_PATTERN); + } + var tripUpdate = new ExtraCallTripBuilder( + journey, + transitEditorService, + deduplicator, + entityResolver, + tripPatternIdGenerator::generateUniqueTripPatternId, + trip + ).build(); + + TripPattern deleteFrom = !tripUpdate.stopPattern().equals(pattern.getStopPattern()) + ? pattern + : null; + + return tripUpdate.withHideTripInScheduledPattern(deleteFrom); + } + + /** + * Add a (new) trip to the timetableRepository and the buffer + */ + private UpdateSuccess addTripToGraphAndBuffer(TripUpdate tripUpdate) { + Trip trip = tripUpdate.tripTimes().getTrip(); + LocalDate serviceDate = tripUpdate.serviceDate(); + + final TripPattern pattern; + if (tripUpdate.tripPatternCreation()) { + pattern = tripUpdate.addedTripPattern(); + } else { + // Get cached trip pattern or create one if it doesn't exist yet + pattern = tripPatternCache.getOrCreateTripPattern( + tripUpdate.stopPattern(), + trip, + transitEditorService.findPattern(trip) + ); + } + + // Revert for TRIP_UPDATE and EXTRA_CALL, but NOT for REPLACEMENT_DEPARTURE (new trips) + boolean revertPreviousRealTimeUpdates = !tripUpdate.tripCreation(); + + // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. + RealTimeTripUpdate realTimeTripUpdate = RealTimeTripUpdate.of( + pattern, + tripUpdate.tripTimes(), + serviceDate + ) + .withAddedTripOnServiceDate(tripUpdate.addedTripOnServiceDate()) + .withTripCreation(tripUpdate.tripCreation()) + .withRouteCreation(tripUpdate.routeCreation()) + .withProducer(tripUpdate.dataSource()) + .withRevertPreviousRealTimeUpdates(revertPreviousRealTimeUpdates) + .withHideTripInScheduledPattern(tripUpdate.hideTripInScheduledPattern()) + .build(); + var result = TripUpdateApplier.apply(buffer, realTimeTripUpdate); + LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); + return result; + } + + /** + * Types of SIRI update messages. + */ + private enum SiriUpdateType { + /** + * Update of an existing trip. + * This can be either a trip defined in planned data or a replacement departure + * that was previously added by a real-time message. + * The update can consist in updated passing times and/or cancellation of some stops. + * A stop can be substituted by another if they belong to the same station. + * The whole trip can also be marked as cancelled. + */ + TRIP_UPDATE, + + /** + * Addition of a new trip, not currently present in the system. + * The new trip has a new unique id. + * The trip can replace one or more existing trips, another SIRI message should handle the + * cancellation of the replaced trips. + */ + REPLACEMENT_DEPARTURE, + + /** + * Addition of one or more stops in an existing trip. + */ + EXTRA_CALL, + } +} diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/StopTimesMapper.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/StopTimesMapper.java index 2550a375eed..ab588ea644b 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/StopTimesMapper.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/StopTimesMapper.java @@ -82,9 +82,6 @@ StopTime createAimedStopTime( stopTime.setStopHeadsign(new NonLocalizedString(destinationDisplay)); } else if (trip.getHeadsign() != null) { stopTime.setStopHeadsign(trip.getHeadsign()); - } else { - // Fallback to empty string - stopTime.setStopHeadsign(new NonLocalizedString("")); } // Update pickup / dropoff diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/EstimatedTimetableHandler.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/EstimatedTimetableHandler.java index aa5493255d1..e0acd555d72 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/EstimatedTimetableHandler.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/EstimatedTimetableHandler.java @@ -13,19 +13,13 @@ public class EstimatedTimetableHandler { private final SiriRealTimeTripUpdateAdapter adapter; - private final boolean fuzzyTripMatching; /** * The ID for the static feed to which these real time updates are applied */ private final String feedId; - public EstimatedTimetableHandler( - SiriRealTimeTripUpdateAdapter adapter, - boolean fuzzyTripMatching, - String feedId - ) { + public EstimatedTimetableHandler(SiriRealTimeTripUpdateAdapter adapter, String feedId) { this.adapter = adapter; - this.fuzzyTripMatching = fuzzyTripMatching; this.feedId = feedId; } @@ -37,12 +31,13 @@ public UpdateResult applyUpdate( UpdateIncrementality updateMode, RealTimeUpdateContext context ) { - return adapter.applyEstimatedTimetable( - fuzzyTripMatching ? context.siriFuzzyTripMatcher() : null, - context.entityResolver(feedId), - feedId, - updateMode, - estimatedTimetableDeliveries - ); + return adapter + .forUpdate(context.mutableSnapshot()) + .applyEstimatedTimetable( + context.entityResolver(feedId), + feedId, + updateMode, + estimatedTimetableDeliveries + ); } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/SiriETUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/SiriETUpdater.java index c096b6c4f69..00f6d1cfd85 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/SiriETUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/SiriETUpdater.java @@ -47,11 +47,7 @@ public SiriETUpdater( LOG.info("Creating SIRI-ET updater running every {}: {}", pollingPeriod(), updateSource); - estimatedTimetableHandler = new EstimatedTimetableHandler( - adapter, - config.fuzzyTripMatching(), - feedId - ); + estimatedTimetableHandler = new EstimatedTimetableHandler(adapter, feedId); this.metricsConsumer = metricsConsumer; } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/google/SiriETGooglePubsubUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/google/SiriETGooglePubsubUpdater.java index 156d4508443..7045eb48ad8 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/google/SiriETGooglePubsubUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/siri/updater/google/SiriETGooglePubsubUpdater.java @@ -38,11 +38,7 @@ public SiriETGooglePubsubUpdater( config.topicName() ); - estimatedTimetableHandler = new EstimatedTimetableHandler( - adapter, - config.fuzzyTripMatching(), - config.feedId() - ); + estimatedTimetableHandler = new EstimatedTimetableHandler(adapter, config.feedId()); updateResultConsumer = TripUpdateMetrics.streaming(config); } diff --git a/application/src/test-fixtures/java/org/opentripplanner/standalone/api/TestServerContext.java b/application/src/test-fixtures/java/org/opentripplanner/standalone/api/TestServerContext.java index 248e9b2b2bf..76e6bde132b 100644 --- a/application/src/test-fixtures/java/org/opentripplanner/standalone/api/TestServerContext.java +++ b/application/src/test-fixtures/java/org/opentripplanner/standalone/api/TestServerContext.java @@ -11,7 +11,8 @@ import org.opentripplanner.ext.emission.internal.itinerary.EmissionItineraryDecorator; import org.opentripplanner.ext.fares.service.gtfs.v1.DefaultFareService; import org.opentripplanner.ext.flex.FlexParameters; -import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; +import org.opentripplanner.framework.transaction.internal.TransactionFactory; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.filterchain.framework.spi.ItineraryDecorator; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; @@ -52,10 +53,13 @@ import org.opentripplanner.transfer.regular.TransferServiceTestFactory; import org.opentripplanner.transfer.regular.internal.DefaultTransferRepository; import org.opentripplanner.transfer.regular.internal.TransferIndex; +import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.repository.TimetableSnapshotLifecycle; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; public class TestServerContext { @@ -74,7 +78,6 @@ public static OtpServerRequestContext createServerContext( transferRepository, fareService, null, - null, null ); } @@ -85,7 +88,6 @@ public static OtpServerRequestContext createServerContext( TimetableRepository timetableRepository, TransferRepository transferRepository, FareService fareService, - @Nullable TimetableSnapshotManager snapshotManager, @Nullable RouteRequest request, @Nullable FlexParameters flexParameters ) { @@ -107,20 +109,79 @@ public static OtpServerRequestContext createServerContext( ); timetableRepository.initRaptorTransitData(scheduledRaptorData); - if (snapshotManager == null) { - snapshotManager = new TimetableSnapshotManager( - TimetableSnapshotParameters.DEFAULT, - LocalDate::now, - new RaptorTransitData(timetableRepository.getRaptorTransitData()), - timetableRepository.copyTripCalendarForRealTimeUpdates() + var registry = TransactionFactory.createRepositoryRegistry(); + var timetableSnapshot = new TimetableSnapshot( + new RaptorTransitData(timetableRepository.getRaptorTransitData()), + timetableRepository.copyTripCalendarForRealTimeUpdates() + ); + RepositoryHandle timetableHandle = + registry.registerRepositorySnapshot( + timetableSnapshot, + new TimetableSnapshotLifecycle(timetableSnapshot, false, LocalDate::now) ); - } - snapshotManager.purgeAndCommit(); + return buildContext( + graph, + timetableRepository, + transferRepository, + fareService, + request, + flexParameters, + routerConfig, + registry, + timetableHandle + ); + } + + /** + * Create a context for unit testing using an existing repository handle (e.g. when real-time + * updates have already been applied to that handle before context creation). + */ + public static OtpServerRequestContext createServerContext( + Graph graph, + TimetableRepository timetableRepository, + TransferRepository transferRepository, + FareService fareService, + RepositoryHandle timetableHandle, + org.opentripplanner.framework.transaction.RepositoryRegistry registry, + @Nullable RouteRequest request, + @Nullable FlexParameters flexParameters + ) { + var routerConfig = RouterConfig.DEFAULT; + if (request == null) { + request = routerConfig.routingRequestDefaults(); + } + if (flexParameters == null) { + flexParameters = routerConfig.flexParameters(); + } + return buildContext( + graph, + timetableRepository, + transferRepository, + fareService, + request, + flexParameters, + routerConfig, + registry, + timetableHandle + ); + } + private static OtpServerRequestContext buildContext( + Graph graph, + TimetableRepository timetableRepository, + TransferRepository transferRepository, + FareService fareService, + RouteRequest request, + FlexParameters flexParameters, + RouterConfig routerConfig, + org.opentripplanner.framework.transaction.RepositoryRegistry registry, + RepositoryHandle timetableHandle + ) { + var transactionScope = registry.scope(); var transitService = new DefaultTransitService( timetableRepository, - snapshotManager.getTimetableSnapshot() + timetableHandle.repositorySnapshot(transactionScope) ); var raptorConfig = new RaptorConfig( @@ -144,6 +205,7 @@ public static OtpServerRequestContext createServerContext( request, createStreetLimitationParametersService(), TransferServiceTestFactory.transferService(transferRepository), + transactionScope, routerConfig.transitTuningConfig(), transitService, routerConfig.triasApiParameters(), diff --git a/application/src/test-fixtures/java/org/opentripplanner/transit/model/TransitTestEnvironment.java b/application/src/test-fixtures/java/org/opentripplanner/transit/model/TransitTestEnvironment.java index feea3824662..4896ed66f75 100644 --- a/application/src/test-fixtures/java/org/opentripplanner/transit/model/TransitTestEnvironment.java +++ b/application/src/test-fixtures/java/org/opentripplanner/transit/model/TransitTestEnvironment.java @@ -2,11 +2,15 @@ import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.time.LocalDate; import java.time.ZoneId; import org.opentripplanner.LocalTimeParser; import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory; -import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; +import org.opentripplanner.framework.transaction.RepositoryRegistry; +import org.opentripplanner.framework.transaction.UpdateManager; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; +import org.opentripplanner.framework.transaction.internal.TransactionFactory; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RaptorTransitDataMapper; import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.DefaultTransitDataProviderFilterBuilder; @@ -15,10 +19,12 @@ import org.opentripplanner.transfer.regular.TransferRepository; import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.repository.TimetableSnapshotLifecycle; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.utils.time.ServiceDateUtils; /** @@ -30,7 +36,12 @@ public final class TransitTestEnvironment { private final TimetableRepository timetableRepository; - private final TimetableSnapshotManager snapshotManager; + private final RepositoryRegistry repositoryRegistry; + private final RepositoryHandle< + ReadOnlyTimetableSnapshot, + MutableTimetableSnapshot + > timetableHandle; + private final UpdateManager updateManager; private final LocalDate defaultServiceDate; public static TransitTestEnvironmentBuilder of() { @@ -51,22 +62,32 @@ public static TransitTestEnvironmentBuilder of(LocalDate serviceDate, ZoneId tim LocalDate defaultServiceDate ) { this.timetableRepository = timetableRepository; + this.defaultServiceDate = defaultServiceDate; this.timetableRepository.index(); - this.timetableRepository.initRaptorTransitData( - RaptorTransitDataMapper.map( - new TestTransitTuningParameters(), - timetableRepository, - transferRepository - ) + var scheduledRaptorData = RaptorTransitDataMapper.map( + new TestTransitTuningParameters(), + timetableRepository, + transferRepository ); - this.snapshotManager = new TimetableSnapshotManager( - TimetableSnapshotParameters.PUBLISH_IMMEDIATELY, - () -> defaultServiceDate, + this.timetableRepository.initRaptorTransitData(scheduledRaptorData); + + this.repositoryRegistry = TransactionFactory.createRepositoryRegistry(); + var timetableSnapshot = new TimetableSnapshot( new RaptorTransitData(timetableRepository.getRaptorTransitData()), timetableRepository.copyTripCalendarForRealTimeUpdates() ); - this.defaultServiceDate = defaultServiceDate; + this.timetableHandle = repositoryRegistry.registerRepositorySnapshot( + timetableSnapshot, + new TimetableSnapshotLifecycle(timetableSnapshot, false, () -> defaultServiceDate) + ); + var threadFactory = new ThreadFactoryBuilder().setNameFormat("test-commit").build(); + // Use atomic commits (commit immediately after each task) so test assertions see results right away + this.updateManager = TransactionFactory.createUpdateManagerWithAtomicCommits( + "test", + repositoryRegistry, + threadFactory + ); } /** @@ -85,10 +106,10 @@ public ZoneId timeZone() { } /** - * Returns a new fresh TransitService + * Returns a new fresh TransitService backed by the current snapshot. */ public TransitService transitService() { - return new DefaultTransitService(timetableRepository, snapshotManager.getTimetableSnapshot()); + return new DefaultTransitService(timetableRepository, timetableSnapshot()); } public String feedId() { @@ -99,12 +120,16 @@ public TimetableRepository timetableRepository() { return timetableRepository; } - public TimetableSnapshotManager timetableSnapshotManager() { - return snapshotManager; + public RepositoryHandle timetableHandle() { + return timetableHandle; + } + + public UpdateManager updateManager() { + return updateManager; } - public TimetableSnapshot timetableSnapshot() { - return snapshotManager.getTimetableSnapshot(); + public ReadOnlyTimetableSnapshot timetableSnapshot() { + return timetableHandle.repositorySnapshot(repositoryRegistry.scope()); } /** diff --git a/application/src/test-fixtures/java/org/opentripplanner/updater/trip/gtfs/GtfsRtTestHelper.java b/application/src/test-fixtures/java/org/opentripplanner/updater/trip/gtfs/GtfsRtTestHelper.java index c671ef41931..a668e0a052d 100644 --- a/application/src/test-fixtures/java/org/opentripplanner/updater/trip/gtfs/GtfsRtTestHelper.java +++ b/application/src/test-fixtures/java/org/opentripplanner/updater/trip/gtfs/GtfsRtTestHelper.java @@ -5,6 +5,7 @@ import com.google.transit.realtime.GtfsRealtime; import java.time.LocalDate; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; import org.opentripplanner.transit.model.TransitTestEnvironment; import org.opentripplanner.updater.spi.UpdateResult; @@ -22,7 +23,6 @@ public class GtfsRtTestHelper { this.gtfsAdapter = new GtfsRealTimeTripUpdateAdapter( transitTestEnvironment.timetableRepository(), DeduplicatorService.NOOP, - transitTestEnvironment.timetableSnapshotManager(), transitTestEnvironment::defaultServiceDate ); } @@ -82,19 +82,29 @@ public UpdateResult applyTripUpdates( List updates, UpdateIncrementality incrementality ) { - UpdateResult updateResult = gtfsAdapter.applyTripUpdates( - null, - ForwardsDelayPropagationType.DEFAULT, - BackwardsDelayPropagationType.REQUIRED_NO_DATA, - incrementality, - updates, - transitTestEnvironment.feedId() - ); - commitTimetableSnapshot(); - return updateResult; - } - - private void commitTimetableSnapshot() { - transitTestEnvironment.timetableSnapshotManager().purgeAndCommit(); + var resultRef = new AtomicReference(); + try { + transitTestEnvironment + .updateManager() + .submit(ctx -> { + var buffer = ctx.repository(transitTestEnvironment.timetableHandle()); + resultRef.set( + gtfsAdapter + .forUpdate(buffer) + .applyTripUpdates( + null, + ForwardsDelayPropagationType.DEFAULT, + BackwardsDelayPropagationType.REQUIRED_NO_DATA, + incrementality, + updates, + transitTestEnvironment.feedId() + ) + ); + }) + .get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return resultRef.get(); } } diff --git a/application/src/test-fixtures/java/org/opentripplanner/updater/trip/siri/SiriTestHelper.java b/application/src/test-fixtures/java/org/opentripplanner/updater/trip/siri/SiriTestHelper.java index 792a31630f9..7be6cc79956 100644 --- a/application/src/test-fixtures/java/org/opentripplanner/updater/trip/siri/SiriTestHelper.java +++ b/application/src/test-fixtures/java/org/opentripplanner/updater/trip/siri/SiriTestHelper.java @@ -3,25 +3,28 @@ import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.opentripplanner.core.framework.deduplicator.DeduplicatorService; -import org.opentripplanner.street.graph.Graph; import org.opentripplanner.transit.model.TransitTestEnvironment; -import org.opentripplanner.updater.DefaultRealTimeUpdateContext; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.updater.spi.UpdateResult; -import org.opentripplanner.updater.trip.siri.updater.EstimatedTimetableHandler; import uk.org.siri.siri21.EstimatedTimetableDeliveryStructure; public class SiriTestHelper { private final TransitTestEnvironment transitTestEnvironment; private final SiriRealTimeTripUpdateAdapter siriAdapter; + private final SiriRealTimeTripUpdateAdapter siriAdapterWithFuzzyMatching; SiriTestHelper(TransitTestEnvironment transitTestEnvironment) { this.transitTestEnvironment = transitTestEnvironment; - this.siriAdapter = new SiriRealTimeTripUpdateAdapter( - transitTestEnvironment.timetableRepository(), + var repo = transitTestEnvironment.timetableRepository(); + this.siriAdapter = new SiriRealTimeTripUpdateAdapter(repo, DeduplicatorService.NOOP, null); + var cache = new SiriFuzzyTripMatcherCache(repo); + this.siriAdapterWithFuzzyMatching = new SiriRealTimeTripUpdateAdapter( + repo, DeduplicatorService.NOOP, - transitTestEnvironment.timetableSnapshotManager() + cache ); } @@ -51,28 +54,33 @@ private UpdateResult applyEstimatedTimetable( List updates, boolean fuzzyMatching ) { - UpdateResult updateResult = getEstimatedTimetableHandler(fuzzyMatching).applyUpdate( - updates, - DIFFERENTIAL, - new DefaultRealTimeUpdateContext( - new Graph(), - transitTestEnvironment.timetableRepository(), - transitTestEnvironment.timetableSnapshotManager().getTimetableSnapshotBuffer() - ) - ); - commitTimetableSnapshot(); - return updateResult; - } - - private EstimatedTimetableHandler getEstimatedTimetableHandler(boolean fuzzyMatching) { - return new EstimatedTimetableHandler( - siriAdapter, - fuzzyMatching, - transitTestEnvironment.feedId() - ); - } - - private void commitTimetableSnapshot() { - transitTestEnvironment.timetableSnapshotManager().purgeAndCommit(); + var resultRef = new AtomicReference(); + var adapter = fuzzyMatching ? siriAdapterWithFuzzyMatching : siriAdapter; + try { + transitTestEnvironment + .updateManager() + .submit(ctx -> { + var buffer = ctx.repository(transitTestEnvironment.timetableHandle()); + var feedId = transitTestEnvironment.feedId(); + var transitService = new DefaultTransitService( + transitTestEnvironment.timetableRepository(), + buffer + ); + resultRef.set( + adapter + .forUpdate(buffer) + .applyEstimatedTimetable( + new EntityResolver(transitService, feedId), + feedId, + DIFFERENTIAL, + updates + ) + ); + }) + .get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return resultRef.get(); } } diff --git a/application/src/test/java/org/opentripplanner/GtfsTest.java b/application/src/test/java/org/opentripplanner/GtfsTest.java index 8da97606fb1..f7a53f4451f 100644 --- a/application/src/test/java/org/opentripplanner/GtfsTest.java +++ b/application/src/test/java/org/opentripplanner/GtfsTest.java @@ -23,7 +23,6 @@ import org.opentripplanner.core.model.id.FeedScopedId; import org.opentripplanner.core.model.time.LocalDateRange; import org.opentripplanner.ext.fares.service.gtfs.v1.DefaultFareService; -import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; import org.opentripplanner.gtfs.graphbuilder.GtfsBundle; import org.opentripplanner.gtfs.graphbuilder.GtfsBundleTestFactory; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; @@ -31,7 +30,6 @@ import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitDataTestFactory; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RaptorTransitDataMapper; import org.opentripplanner.routing.api.request.RequestModes; @@ -48,19 +46,17 @@ import org.opentripplanner.transfer.regular.TransferServiceTestFactory; import org.opentripplanner.transit.model.basic.MainAndSubMode; import org.opentripplanner.transit.model.basic.TransitMode; -import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; import org.opentripplanner.transit.model.framework.Deduplicator; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; import org.opentripplanner.transit.service.SiteRepository; import org.opentripplanner.transit.service.TimetableRepository; -import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; +import org.opentripplanner.updater.GraphWriterService; import org.opentripplanner.updater.alert.gtfs.AlertsUpdateHandler; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.updater.trip.UpdateIncrementality; import org.opentripplanner.updater.trip.gtfs.GtfsRealTimeTripUpdateAdapter; import org.opentripplanner.updater.trip.gtfs.interpolation.BackwardsDelayPropagationType; import org.opentripplanner.updater.trip.gtfs.interpolation.ForwardsDelayPropagationType; +import org.opentripplanner.utils.lang.RunnableUtils; /** Common base class for many test classes which need to load a GTFS feed in preparation for tests. */ public abstract class GtfsTest { @@ -202,15 +198,8 @@ protected void setUp() throws Exception { alertsUpdateHandler = new AlertsUpdateHandler(false); graph = new Graph(); timetableRepository = new TimetableRepository(new SiteRepository()); - timetableRepository.setUpdaterManager( - new GraphUpdaterManager( - new DefaultRealTimeUpdateContext( - new Graph(), - timetableRepository, - new TimetableSnapshot(RaptorTransitDataTestFactory.empty(), new DefaultTripCalendars()) - ), - List.of() - ) + timetableRepository.initUpdaterManager( + new GraphUpdaterManager(GraphWriterService.NOOP, RunnableUtils.NOOP, List.of()) ); TransferRepository transferRepository = TransferServiceTestFactory.defaultTransferRepository(); @@ -232,17 +221,30 @@ protected void setUp() throws Exception { transferRepository ); timetableRepository.initRaptorTransitData(scheduledRaptorData); - var snapshotManager = new TimetableSnapshotManager( - TimetableSnapshotParameters.PUBLISH_IMMEDIATELY, - LocalDate::now, + var registry = + org.opentripplanner.framework.transaction.internal.TransactionFactory.createRepositoryRegistry(); + var timetableSnapshot = new org.opentripplanner.transit.model.timetable.TimetableSnapshot( new RaptorTransitData(scheduledRaptorData), timetableRepository.copyTripCalendarForRealTimeUpdates() ); + var timetableHandle = registry.registerRepositorySnapshot( + timetableSnapshot, + new org.opentripplanner.transit.repository.TimetableSnapshotLifecycle( + timetableSnapshot, + false, + LocalDate::now + ) + ); + var updateManager = + org.opentripplanner.framework.transaction.internal.TransactionFactory.createUpdateManagerWithAtomicCommits( + "test", + registry, + java.util.concurrent.Executors.defaultThreadFactory() + ); tripUpdateAdapter = new GtfsRealTimeTripUpdateAdapter( timetableRepository, new Deduplicator(), - snapshotManager, LocalDate::now ); alertPatchServiceImpl = new TransitAlertServiceImpl(timetableRepository); @@ -257,22 +259,32 @@ protected void setUp() throws Exception { for (FeedEntity feedEntity : feedEntityList) { updates.add(feedEntity.getTripUpdate()); } - tripUpdateAdapter.applyTripUpdates( - null, - ForwardsDelayPropagationType.DEFAULT, - BackwardsDelayPropagationType.REQUIRED_NO_DATA, - UpdateIncrementality.DIFFERENTIAL, - updates, - FEED_ID - ); + updateManager + .submit(ctx -> { + var buffer = ctx.repository(timetableHandle); + tripUpdateAdapter + .forUpdate(buffer) + .applyTripUpdates( + null, + ForwardsDelayPropagationType.DEFAULT, + BackwardsDelayPropagationType.REQUIRED_NO_DATA, + UpdateIncrementality.DIFFERENTIAL, + updates, + FEED_ID + ); + }) + .get(); alertsUpdateHandler.update(feedMessage, null); - } catch (FileNotFoundException _) {} + } catch (FileNotFoundException _) {} catch (Exception e) { + throw new RuntimeException(e); + } serverContext = TestServerContext.createServerContext( graph, timetableRepository, transferRepository, new DefaultFareService(), - snapshotManager, + timetableHandle, + registry, null, null ); diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index 777b7b23c15..9fc292f011e 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -133,7 +133,6 @@ void setup() { TIMETABLE_REPOSITORY, TRANSFER_REPOSITORY, new DefaultFareService(), - null, defaultRequest, null ); diff --git a/application/src/test/java/org/opentripplanner/framework/transaction/moduletest/TransactionFrameworkTest.java b/application/src/test/java/org/opentripplanner/framework/transaction/moduletest/TransactionFrameworkTest.java index efe74011da7..23b3d9d9fd1 100644 --- a/application/src/test/java/org/opentripplanner/framework/transaction/moduletest/TransactionFrameworkTest.java +++ b/application/src/test/java/org/opentripplanner/framework/transaction/moduletest/TransactionFrameworkTest.java @@ -61,6 +61,9 @@ public class TransactionFrameworkTest { private static final String SCOPE_TXN_1 = "Scope(TXN-1)"; private static final String SCOPE_TXN_2 = "Scope(TXN-2)"; private static final String SCOPE_TXN_3 = "Scope(TXN-3)"; + private static final int AWAIT_STATE_TIMEOUT = 2_000; + private static final int AWAIT_STATE_CHECK_INTERVAL_MS = 10; + public static final int AUTO_COMMIT_INTERVAL_MS = 50; private RepositoryRegistry registry; private UpdateManager updateManager; @@ -135,7 +138,7 @@ private void setupUpdateManagerWithPeriodicCommits() { getClass().getSimpleName(), registry, threadFactory, - Duration.ofMillis(40) + Duration.ofMillis(AUTO_COMMIT_INTERVAL_MS) ); updateManager.register(new CustomerEventHandler(), customerRepoHandler); updateManager.register(new OrderEventHandler(), orderRepoHandler); @@ -153,7 +156,6 @@ public void testPeriodicCommits() throws ExecutionException, InterruptedExceptio f = updateManager.submit(c -> publishNewDomainEvent(c, ANNIKA)); f.get(); - awaitState(SCOPE_TXN_2, PIPPI, TOMMY); awaitState(SCOPE_TXN_3, PIPPI, TOMMY, ANNIKA); updateManager.shutdown(); @@ -247,13 +249,13 @@ private > void assertEntities( @SuppressWarnings("BusyWait") private void awaitState(String expScope, TestEvent... expected) throws InterruptedException { - long deadline = System.currentTimeMillis() + 2_000; + long deadline = System.currentTimeMillis() + AWAIT_STATE_TIMEOUT; while (System.currentTimeMillis() < deadline) { try { assertState(expScope, expected); return; } catch (AssertionError ignored) { - Thread.sleep(10); + Thread.sleep(AWAIT_STATE_CHECK_INTERVAL_MS); } } assertState(expScope, expected); diff --git a/application/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/application/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index d2b6798a7b8..90ce99bffb8 100644 --- a/application/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/application/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -15,15 +15,11 @@ import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.plan.leg.ScheduledTransitLeg; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitDataTestFactory; -import org.opentripplanner.street.graph.Graph; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; -import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; -import org.opentripplanner.transit.model.timetable.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimesFactory; @@ -31,8 +27,9 @@ import org.opentripplanner.transit.service.SiteRepository; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; +import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.opentripplanner.utils.lang.RunnableUtils; class ScheduledTransitLegReferenceTest { @@ -72,15 +69,8 @@ static void buildTransitService() { .withRegularStop(stop3b) .build(); TimetableRepository timetableRepository = new TimetableRepository(siteRepository); - timetableRepository.setUpdaterManager( - new GraphUpdaterManager( - new DefaultRealTimeUpdateContext( - new Graph(), - timetableRepository, - new TimetableSnapshot(RaptorTransitDataTestFactory.empty(), new DefaultTripCalendars()) - ), - List.of() - ) + timetableRepository.initUpdaterManager( + new GraphUpdaterManager(WriteToGraphCallback.NOOP, RunnableUtils.NOOP, List.of()) ); // build transit data CalendarServiceData calendarServiceData = new CalendarServiceData(); diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/SnapshotTestBase.java b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/SnapshotTestBase.java index f81a7e957f1..a43f47f1a08 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/SnapshotTestBase.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/mapping/SnapshotTestBase.java @@ -93,7 +93,6 @@ protected OtpServerRequestContext serverContext() { model.transferRepository(), model.fareServiceFactory().makeFareService(), null, - null, null ); } diff --git a/application/src/test/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycleTest.java b/application/src/test/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycleTest.java new file mode 100644 index 00000000000..0fabd373df2 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/repository/TimetableSnapshotLifecycleTest.java @@ -0,0 +1,118 @@ +package org.opentripplanner.transit.repository; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.opentripplanner.transit.repository.TimetableSnapshotLifecycleTest.SameAssert.NotSame; +import static org.opentripplanner.transit.repository.TimetableSnapshotLifecycleTest.SameAssert.Same; + +import java.time.LocalDate; +import java.time.Month; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitDataTestFactory; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeTripUpdate; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.model.timetable.TripTimes; + +/** + * Tests for {@link TimetableSnapshotLifecycle}, specifically the purge-expired-data behavior + * triggered during {@link TimetableSnapshotLifecycle#freeze(MutableTimetableSnapshot)}. + */ +class TimetableSnapshotLifecycleTest { + + private static final LocalDate TODAY = LocalDate.of(2024, Month.MAY, 30); + private static final LocalDate TOMORROW = TODAY.plusDays(1); + private static final LocalDate YESTERDAY = TODAY.minusDays(1); + + private static final TimetableRepositoryForTest TEST_MODEL = TimetableRepositoryForTest.of(); + + private static final TripPattern PATTERN = TimetableRepositoryForTest.tripPattern( + "pattern", + TimetableRepositoryForTest.route("r1").build() + ) + .withStopPattern( + TimetableRepositoryForTest.stopPattern( + TEST_MODEL.stop("1").build(), + TEST_MODEL.stop("2").build() + ) + ) + .build(); + private static final TripTimes TRIP_TIMES = ScheduledTripTimes.of() + .withArrivalTimes("00:00 00:01") + .withTrip(TimetableRepositoryForTest.trip("trip").build()) + .build(); + + enum SameAssert { + Same { + public void test(Object a, Object b) { + assertSame(a, b); + } + }, + NotSame { + public void test(Object a, Object b) { + assertNotSame(a, b); + } + }; + + abstract void test(Object a, Object b); + + SameAssert not() { + return this == Same ? NotSame : Same; + } + } + + static Stream purgeExpiredDataTestCases() { + return Stream.of( + // purgeExpiredData || snapshots PatternSnapshotA PatternSnapshotB + Arguments.of(Boolean.TRUE, NotSame, NotSame), + Arguments.of(Boolean.FALSE, NotSame, Same) + ); + } + + @ParameterizedTest(name = "purgeExpired: {0} || {1} {2}") + @MethodSource("purgeExpiredDataTestCases") + public void testPurgeExpiredData( + boolean purgeExpiredData, + SameAssert expSnapshots, + SameAssert expPatternAeqB + ) { + final AtomicReference clock = new AtomicReference<>(YESTERDAY); + + var buffer = new TimetableSnapshot( + RaptorTransitDataTestFactory.empty(), + new DefaultTripCalendars() + ); + var lifecycle = new TimetableSnapshotLifecycle(buffer, purgeExpiredData, clock::get); + + // Add data for YESTERDAY, freeze to produce snapshot A + buffer.update(RealTimeTripUpdate.of(PATTERN, TRIP_TIMES, YESTERDAY).build()); + var snapshotA = lifecycle.freeze(buffer); + + // Advance the clock to TOMORROW and add data for TODAY + clock.set(TOMORROW); + buffer.update(RealTimeTripUpdate.of(PATTERN, TRIP_TIMES, TODAY).build()); + + // Freeze again — if purge is enabled, YESTERDAY data should be purged + var snapshotB = lifecycle.freeze(buffer); + + expSnapshots.test(snapshotA, snapshotB); + expPatternAeqB.test( + snapshotA.resolve(PATTERN, YESTERDAY), + snapshotB.resolve(PATTERN, YESTERDAY) + ); + expPatternAeqB + .not() + .test(snapshotB.resolve(PATTERN, null), snapshotB.resolve(PATTERN, YESTERDAY)); + + // Expect the same results regardless of the config for these + assertNotSame(snapshotA.resolve(PATTERN, null), snapshotA.resolve(PATTERN, YESTERDAY)); + assertSame(snapshotA.resolve(PATTERN, null), snapshotB.resolve(PATTERN, null)); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 556ddda38de..ddc75a657bb 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -16,6 +16,9 @@ import org.opentripplanner.ext.fares.service.gtfs.v1.DefaultFareService; import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; +import org.opentripplanner.framework.transaction.api.RepositoryHandle; +import org.opentripplanner.framework.transaction.api.TransactionScope; +import org.opentripplanner.framework.transaction.internal.TransactionFactory; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; @@ -40,6 +43,10 @@ import org.opentripplanner.street.graph.Graph; import org.opentripplanner.transfer.regular.TransferRepository; import org.opentripplanner.transfer.regular.TransferServiceTestFactory; +import org.opentripplanner.transit.model.timetable.TimetableSnapshot; +import org.opentripplanner.transit.repository.MutableTimetableSnapshot; +import org.opentripplanner.transit.repository.ReadOnlyTimetableSnapshot; +import org.opentripplanner.transit.repository.TimetableSnapshotLifecycle; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.speed_test.model.SpeedTestProfile; @@ -53,7 +60,6 @@ import org.opentripplanner.transit.speed_test.options.SpeedTestCmdLineOpts; import org.opentripplanner.transit.speed_test.options.SpeedTestConfig; import org.opentripplanner.updater.configure.UpdaterConfigurator; -import org.opentripplanner.updater.trip.TimetableSnapshotManager; /** * Test response times for a large batch of origin/destination points. Also demonstrates how to run @@ -114,14 +120,30 @@ public SpeedTest( ); timetableRepository.initRaptorTransitData(scheduledRaptorData); - TimetableSnapshotManager snapshotManager = new TimetableSnapshotManager( - TimetableSnapshotParameters.DEFAULT, - LocalDate::now, + + var parameters = TimetableSnapshotParameters.DEFAULT; + var registry = TransactionFactory.createRepositoryRegistry(); + var timetableSnapshot = new TimetableSnapshot( new RaptorTransitData(timetableRepository.getRaptorTransitData()), timetableRepository.copyTripCalendarForRealTimeUpdates() ); + RepositoryHandle timetableHandle = + registry.registerRepositorySnapshot( + timetableSnapshot, + new TimetableSnapshotLifecycle( + timetableSnapshot, + parameters.purgeExpiredData(), + LocalDate::now + ) + ); + var threadFactory = java.util.concurrent.Executors.defaultThreadFactory(); + var updateManager = TransactionFactory.createUpdateManagerWithPeriodicCommits( + "speedtest", + registry, + threadFactory, + parameters.maxSnapshotFrequency() + ); - snapshotManager.purgeAndCommit(); UpdaterConfigurator.configure( graph, DeduplicatorService.NOOP, @@ -131,7 +153,8 @@ public SpeedTest( new DefaultVehicleParkingRepository(), timetableRepository, new DefaultCarpoolingRepository(), - snapshotManager, + updateManager, + timetableHandle, routerConfig.updaterConfig() ); if (timetableRepository.getUpdaterManager() != null) { @@ -162,8 +185,12 @@ public SpeedTest( routerConfig.routingRequestDefaults(), TestServerContext.createStreetLimitationParametersService(), TransferServiceTestFactory.transferService(transferRepository), + new TransactionScope() {}, routerConfig.transitTuningConfig(), - new DefaultTransitService(timetableRepository, snapshotManager.getTimetableSnapshot()), + new DefaultTransitService( + timetableRepository, + timetableHandle.repositorySnapshot(registry.scope()) + ), null, null, VectorTileConfig.DEFAULT, diff --git a/application/src/test/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandlerTest.java b/application/src/test/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandlerTest.java index 54f897a8fff..667984e3ff3 100644 --- a/application/src/test/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandlerTest.java +++ b/application/src/test/java/org/opentripplanner/updater/alert/siri/SiriAlertsUpdateHandlerTest.java @@ -33,7 +33,6 @@ import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.DefaultRealTimeUpdateContext; -import org.opentripplanner.updater.GraphUpdaterManager; import org.opentripplanner.updater.RealTimeUpdateContext; import uk.org.ifopt.siri21.StopPlaceRef; import uk.org.siri.siri21.AffectedLineStructure; @@ -85,9 +84,6 @@ public void setUp() throws Exception { realTimeUpdateContext = new DefaultRealTimeUpdateContext(graph, timetableRepository); if (transitService == null) { transitService = new DefaultTransitService(timetableRepository); - timetableRepository.setUpdaterManager( - new GraphUpdaterManager(realTimeUpdateContext, List.of()) - ); } else { transitAlertService.getAllAlerts().clear(); } @@ -96,7 +92,8 @@ public void setUp() throws Exception { alertsUpdateHandler = new SiriAlertsUpdateHandler( FEED_ID, transitAlertService, - Duration.ZERO + Duration.ZERO, + null ); } } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java b/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateApplierTest.java similarity index 56% rename from application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java rename to application/src/test/java/org/opentripplanner/updater/trip/TripUpdateApplierTest.java index 231d670ff1d..26c7715738f 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateApplierTest.java @@ -2,23 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.NotSame; -import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.Same; import java.time.LocalDate; import java.time.Month; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.opentripplanner.framework.transaction.TimetableSnapshotParameters; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitDataTestFactory; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.calendar.DefaultTripCalendars; @@ -29,36 +18,17 @@ import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.transit.model.timetable.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; -class TimetableSnapshotManagerTest { +/** + * Tests for {@link TripUpdateApplier}, covering the three-phase update logic that was + * previously in {@code TimetableSnapshotManager.updateBuffer()}. + */ +class TripUpdateApplierTest { private static final LocalDate TODAY = LocalDate.of(2024, Month.MAY, 30); - private static final LocalDate TOMORROW = TODAY.plusDays(1); - private static final LocalDate YESTERDAY = TODAY.minusDays(1); private static final TimetableRepositoryForTest TEST_MODEL = TimetableRepositoryForTest.of(); - // --- Shared objects for the purge test --- - - private static final TripPattern PATTERN = TimetableRepositoryForTest.tripPattern( - "pattern", - TimetableRepositoryForTest.route("r1").build() - ) - .withStopPattern( - TimetableRepositoryForTest.stopPattern( - TEST_MODEL.stop("1").build(), - TEST_MODEL.stop("2").build() - ) - ) - .build(); - private static final TripTimes TRIP_TIMES = ScheduledTripTimes.of() - .withArrivalTimes("00:00 00:01") - .withTrip(TimetableRepositoryForTest.trip("trip").build()) - .build(); - - // --- Shared objects for the three-phase updateBuffer tests --- - private static final Route ROUTE = TimetableRepositoryForTest.route("route1").build(); private static final Trip TRIP = TimetableRepositoryForTest.trip("trip1").build(); private static final RegularStop STOP_1 = TEST_MODEL.stop("S1").build(); @@ -72,7 +42,7 @@ class TimetableSnapshotManagerTest { /** * Scheduled pattern (stops S1, S2) with the trip in its scheduled timetable. Phase 2 of - * updateBuffer() reads from the scheduled timetable to create a DELETED entry. + * apply() reads from the scheduled timetable to create a DELETED entry. */ private static final TripPattern SCHEDULED_PATTERN = TimetableRepositoryForTest.tripPattern( "sched", @@ -83,8 +53,7 @@ class TimetableSnapshotManagerTest { .build(); /** - * Modified pattern (stops S1, S3) flagged as real-time stop-pattern-modified. When an update - * targets this pattern, TimetableSnapshot registers it in realTimeNewTripPatternsForModifiedTrips. + * Modified pattern (stops S1, S3) flagged as real-time stop-pattern-modified. */ private static final TripPattern MODIFIED_PATTERN = TimetableRepositoryForTest.tripPattern( "modified", @@ -103,75 +72,8 @@ class TimetableSnapshotManagerTest { .withRealTimeStopPatternModified() .build(); - enum SameAssert { - Same { - public void test(Object a, Object b) { - assertSame(a, b); - } - }, - NotSame { - public void test(Object a, Object b) { - assertNotSame(a, b); - } - }; - - abstract void test(Object a, Object b); - - SameAssert not() { - return this == Same ? NotSame : Same; - } - } - - static Stream purgeExpiredDataTestCases() { - return Stream.of( - // purgeExpiredData || snapshots PatternSnapshotA PatternSnapshotB - Arguments.of(Boolean.TRUE, NotSame, NotSame), - Arguments.of(Boolean.FALSE, NotSame, Same) - ); - } - - @ParameterizedTest(name = "purgeExpired: {0} || {1} {2}") - @MethodSource("purgeExpiredDataTestCases") - public void testPurgeExpiredData( - boolean purgeExpiredData, - SameAssert expSnapshots, - SameAssert expPatternAeqB - ) { - // We will simulate the clock turning midnight into tomorrow, data on - // yesterday is candidate to expire - final AtomicReference clock = new AtomicReference<>(YESTERDAY); - - var snapshotManager = createManager( - TimetableSnapshotParameters.DEFAULT.withPurgeExpiredData(purgeExpiredData), - clock::get - ); - - snapshotManager.updateBuffer(RealTimeTripUpdate.of(PATTERN, TRIP_TIMES, YESTERDAY).build()); - - snapshotManager.commitTimetableSnapshot(true); - final TimetableSnapshot snapshotA = snapshotManager.getTimetableSnapshot(); - - // Turn the clock to tomorrow - clock.set(TOMORROW); - - snapshotManager.updateBuffer(RealTimeTripUpdate.of(PATTERN, TRIP_TIMES, TODAY).build()); - - snapshotManager.purgeAndCommit(); - - final TimetableSnapshot snapshotB = snapshotManager.getTimetableSnapshot(); - - expSnapshots.test(snapshotA, snapshotB); - expPatternAeqB.test( - snapshotA.resolve(PATTERN, YESTERDAY), - snapshotB.resolve(PATTERN, YESTERDAY) - ); - expPatternAeqB - .not() - .test(snapshotB.resolve(PATTERN, null), snapshotB.resolve(PATTERN, YESTERDAY)); - - // Expect the same results regardless of the config for these - assertNotSame(snapshotA.resolve(PATTERN, null), snapshotA.resolve(PATTERN, YESTERDAY)); - assertSame(snapshotA.resolve(PATTERN, null), snapshotB.resolve(PATTERN, null)); + private static TimetableSnapshot createBuffer() { + return new TimetableSnapshot(RaptorTransitDataTestFactory.empty(), new DefaultTripCalendars()); } /** @@ -179,8 +81,8 @@ public void testPurgeExpiredData( * reverting or deleting anything. */ @Test - void updateBufferPhase3Only() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void phase3Only() { + var buffer = createBuffer(); var rtTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withArrivalDelay(0, 60) .withDepartureDelay(0, 60) @@ -188,9 +90,12 @@ void updateBufferPhase3Only() { .withDepartureDelay(1, 60) .build(); - manager.updateBuffer(RealTimeTripUpdate.of(SCHEDULED_PATTERN, rtTripTimes, TODAY).build()); + TripUpdateApplier.apply( + buffer, + RealTimeTripUpdate.of(SCHEDULED_PATTERN, rtTripTimes, TODAY).build() + ); - var timetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var timetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); var tripTimes = timetable.getTripTimes(TRIP); assertNotNull(tripTimes); assertTrue(tripTimes.hasAnyUpdates()); @@ -201,8 +106,8 @@ void updateBufferPhase3Only() { * and applying the update on the modified pattern. */ @Test - void updateBufferWithDeleteFromScheduled() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void phase2And3() { + var buffer = createBuffer(); var rtTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withArrivalDelay(0, 60) .withDepartureDelay(0, 60) @@ -210,22 +115,23 @@ void updateBufferWithDeleteFromScheduled() { .withDepartureDelay(1, 60) .build(); - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(MODIFIED_PATTERN, rtTripTimes, TODAY) .withHideTripInScheduledPattern(SCHEDULED_PATTERN) .build() ); // Trip should be DELETED in the scheduled pattern - var scheduledTimetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var scheduledTimetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); assertTrue(scheduledTimetable.getTripTimes(TRIP).isDeleted()); // Trip should be UPDATED in the modified pattern - var modifiedTimetable = manager.resolve(MODIFIED_PATTERN, TODAY); + var modifiedTimetable = buffer.resolve(MODIFIED_PATTERN, TODAY); assertTrue(modifiedTimetable.getTripTimes(TRIP).hasAnyUpdates()); // Modified pattern should be registered for this trip - assertEquals(MODIFIED_PATTERN, manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertEquals(MODIFIED_PATTERN, buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); } /** @@ -233,8 +139,8 @@ void updateBufferWithDeleteFromScheduled() { * scheduled pattern. */ @Test - void updateBufferWithRevertThenUpdate() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void phase1And3() { + var buffer = createBuffer(); var rtTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withArrivalDelay(0, 60) .withDepartureDelay(0, 60) @@ -243,27 +149,29 @@ void updateBufferWithRevertThenUpdate() { .build(); // Setup: move the trip to the modified pattern - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(MODIFIED_PATTERN, rtTripTimes, TODAY) .withHideTripInScheduledPattern(SCHEDULED_PATTERN) .build() ); // Precondition: trip is on modified pattern - assertEquals(MODIFIED_PATTERN, manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertEquals(MODIFIED_PATTERN, buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); // Now revert and update on the scheduled pattern - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(SCHEDULED_PATTERN, rtTripTimes, TODAY) .withRevertPreviousRealTimeUpdates(true) .build() ); // Trip should no longer be registered on the modified pattern - assertNull(manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertNull(buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); // Trip should be UPDATED in the scheduled pattern - var timetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var timetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); assertTrue(timetable.getTripTimes(TRIP).hasAnyUpdates()); } @@ -272,8 +180,8 @@ void updateBufferWithRevertThenUpdate() { * scheduled pattern, and move it to a different modified pattern. */ @Test - void updateBufferAllThreePhases() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void allThreePhases() { + var buffer = createBuffer(); var rtTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withArrivalDelay(0, 60) .withDepartureDelay(0, 60) @@ -282,18 +190,20 @@ void updateBufferAllThreePhases() { .build(); // Setup: move the trip to the first modified pattern - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(MODIFIED_PATTERN, rtTripTimes, TODAY) .withHideTripInScheduledPattern(SCHEDULED_PATTERN) .build() ); // Precondition: trip is on the first modified pattern - assertEquals(MODIFIED_PATTERN, manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertEquals(MODIFIED_PATTERN, buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); // Now revert from the first modified pattern, delete from scheduled, and update on the // second modified pattern - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(SECOND_MODIFIED_PATTERN, rtTripTimes, TODAY) .withRevertPreviousRealTimeUpdates(true) .withHideTripInScheduledPattern(SCHEDULED_PATTERN) @@ -301,61 +211,63 @@ void updateBufferAllThreePhases() { ); // Trip should be DELETED in the scheduled pattern - var scheduledTimetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var scheduledTimetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); assertTrue(scheduledTimetable.getTripTimes(TRIP).isDeleted()); // Trip should be UPDATED in the second modified pattern - var secondModifiedTimetable = manager.resolve(SECOND_MODIFIED_PATTERN, TODAY); + var secondModifiedTimetable = buffer.resolve(SECOND_MODIFIED_PATTERN, TODAY); assertTrue(secondModifiedTimetable.getTripTimes(TRIP).hasAnyUpdates()); // Modified pattern registration should point to the second modified pattern assertEquals( SECOND_MODIFIED_PATTERN, - manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY) + buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY) ); } /** - * Cancel a scheduled trip: build canceled trip times before calling updateBuffer, as the + * Cancel a scheduled trip: build canceled trip times before calling apply, as the * adapter does. */ @Test - void updateBufferCancelScheduledTrip() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void cancelScheduledTrip() { + var buffer = createBuffer(); var canceledTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withCanceled() .build(); - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(SCHEDULED_PATTERN, canceledTripTimes, TODAY) .withRevertPreviousRealTimeUpdates(true) .build() ); - var timetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var timetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); var tripTimes = timetable.getTripTimes(TRIP); assertNotNull(tripTimes); assertTrue(tripTimes.isCanceled()); } /** - * Delete a scheduled trip: build deleted trip times before calling updateBuffer, as the + * Delete a scheduled trip: build deleted trip times before calling apply, as the * adapter does. */ @Test - void updateBufferDeleteScheduledTrip() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void deleteScheduledTrip() { + var buffer = createBuffer(); var deletedTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withDeleted() .build(); - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(SCHEDULED_PATTERN, deletedTripTimes, TODAY) .withRevertPreviousRealTimeUpdates(true) .build() ); - var timetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var timetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); var tripTimes = timetable.getTripTimes(TRIP); assertNotNull(tripTimes); assertTrue(tripTimes.isDeleted()); @@ -366,8 +278,8 @@ void updateBufferDeleteScheduledTrip() { * back to the scheduled pattern and is canceled there. */ @Test - void updateBufferCancelWithRevert() { - var manager = createManager(TimetableSnapshotParameters.DEFAULT, () -> TODAY); + void cancelWithRevert() { + var buffer = createBuffer(); var rtTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withArrivalDelay(0, 60) .withDepartureDelay(0, 60) @@ -376,42 +288,32 @@ void updateBufferCancelWithRevert() { .build(); // Setup: move the trip to the modified pattern - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(MODIFIED_PATTERN, rtTripTimes, TODAY) .withHideTripInScheduledPattern(SCHEDULED_PATTERN) .build() ); // Precondition: trip is on modified pattern - assertEquals(MODIFIED_PATTERN, manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertEquals(MODIFIED_PATTERN, buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); // Now revert and cancel on the scheduled pattern var canceledTripTimes = SCHEDULED_TRIP_TIMES.createRealTimeFromScheduledTimes() .withCanceled() .build(); - manager.updateBuffer( + TripUpdateApplier.apply( + buffer, RealTimeTripUpdate.of(SCHEDULED_PATTERN, canceledTripTimes, TODAY) .withRevertPreviousRealTimeUpdates(true) .build() ); // Trip should no longer be registered on the modified pattern - assertNull(manager.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); + assertNull(buffer.getNewTripPatternForModifiedTrip(TRIP.getId(), TODAY)); // Trip should be CANCELED in the scheduled pattern - var timetable = manager.resolve(SCHEDULED_PATTERN, TODAY); + var timetable = buffer.resolve(SCHEDULED_PATTERN, TODAY); assertTrue(timetable.getTripTimes(TRIP).isCanceled()); } - - private static TimetableSnapshotManager createManager( - TimetableSnapshotParameters snapshotParameters, - Supplier dateSupplier - ) { - return new TimetableSnapshotManager( - snapshotParameters, - dateSupplier, - RaptorTransitDataTestFactory.empty(), - new DefaultTripCalendars() - ); - } } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherTest.java b/application/src/test/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherTest.java index e89f330968a..b5b33f1b930 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/siri/SiriFuzzyTripMatcherTest.java @@ -109,7 +109,8 @@ void noDepartureTime() { private static TripAndPattern match(EstimatedVehicleJourney evj, TransitTestEnvironment env) throws UpdateException { var transitService = env.transitService(); - var fuzzyMatcher = new SiriFuzzyTripMatcher(transitService); + var cache = new SiriFuzzyTripMatcherCache(env.timetableRepository()); + var fuzzyMatcher = new SiriFuzzyTripMatcher(cache, transitService); return fuzzyMatcher.match( EstimatedVehicleJourneyWrapper.of(evj), new EntityResolver(transitService, env.feedId()), diff --git a/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java b/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java index 51964453b09..bab09dd71c1 100644 --- a/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java +++ b/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java @@ -5,9 +5,8 @@ import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id; import static org.opentripplanner.standalone.config.framework.json.JsonSupport.newNodeAdapterForTest; -import com.google.common.util.concurrent.Futures; import java.util.List; -import java.util.concurrent.Future; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; import org.opentripplanner.core.model.i18n.I18NString; import org.opentripplanner.core.model.id.FeedScopedId; @@ -21,9 +20,9 @@ import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; -import org.opentripplanner.updater.GraphWriterRunnable; import org.opentripplanner.updater.spi.DataSource; -import org.opentripplanner.updater.spi.GraphUpdater; +import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.opentripplanner.utils.lang.RunnableUtils; class VehicleParkingAvailabilityUpdaterTest { @@ -116,25 +115,16 @@ private static VehicleParking.VehicleParkingBuilder parkingBuilder() { } private void runUpdaterOnce(VehicleParkingAvailabilityUpdater updater) { - class GraphUpdaterMock extends GraphUpdaterManager { - - private static final Graph GRAPH = new Graph(); - private static final TimetableRepository TRANSIT_MODEL = new TimetableRepository(); - public static final DefaultRealTimeUpdateContext REAL_TIME_UPDATE_CONTEXT = - new DefaultRealTimeUpdateContext(GRAPH, TRANSIT_MODEL); - - public GraphUpdaterMock(List updaters) { - super(REAL_TIME_UPDATE_CONTEXT, updaters); - } - - @Override - public Future execute(GraphWriterRunnable runnable) { - runnable.run(REAL_TIME_UPDATE_CONTEXT); - return Futures.immediateVoidFuture(); - } - } - - var graphUpdaterManager = new GraphUpdaterMock(List.of(updater)); + var context = new DefaultRealTimeUpdateContext(new Graph(), new TimetableRepository()); + WriteToGraphCallback callback = runnable -> { + runnable.run(context); + return CompletableFuture.completedFuture(null); + }; + var graphUpdaterManager = new GraphUpdaterManager( + callback, + RunnableUtils.NOOP, + List.of(updater) + ); graphUpdaterManager.startUpdaters(); graphUpdaterManager.stop(false); } diff --git a/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java b/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java index 344931c80de..53210c92fd0 100644 --- a/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java +++ b/application/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java @@ -3,10 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; -import com.google.common.util.concurrent.Futures; import java.time.Duration; import java.util.List; -import java.util.concurrent.Future; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -26,9 +25,9 @@ import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; -import org.opentripplanner.updater.GraphWriterRunnable; import org.opentripplanner.updater.spi.DataSource; -import org.opentripplanner.updater.spi.GraphUpdater; +import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.opentripplanner.utils.lang.RunnableUtils; class VehicleParkingUpdaterTest { @@ -265,20 +264,15 @@ private void assertVehicleParkingsInGraph(int vehicleParkingNumber) { } private void runUpdaterOnce() { - class GraphUpdaterMock extends GraphUpdaterManager { - - public GraphUpdaterMock(List updaters) { - super(realTimeUpdateContext, updaters); - } - - @Override - public Future execute(GraphWriterRunnable runnable) { - runnable.run(realTimeUpdateContext); - return Futures.immediateVoidFuture(); - } - } - - var graphUpdaterManager = new GraphUpdaterMock(List.of(vehicleParkingUpdater)); + WriteToGraphCallback callback = runnable -> { + runnable.run(realTimeUpdateContext); + return CompletableFuture.completedFuture(null); + }; + var graphUpdaterManager = new GraphUpdaterManager( + callback, + RunnableUtils.NOOP, + List.of(vehicleParkingUpdater) + ); graphUpdaterManager.startUpdaters(); graphUpdaterManager.stop(false); } diff --git a/application/src/test/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdaterTest.java b/application/src/test/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdaterTest.java index a638747f9db..2f68cf34b1d 100644 --- a/application/src/test/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdaterTest.java +++ b/application/src/test/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdaterTest.java @@ -3,11 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.google.common.util.concurrent.Futures; import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.jupiter.api.Disabled; @@ -15,15 +13,13 @@ import org.opentripplanner.framework.io.HttpHeaders; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; -import org.opentripplanner.street.graph.Graph; -import org.opentripplanner.transit.service.TimetableRepository; -import org.opentripplanner.updater.DefaultRealTimeUpdateContext; import org.opentripplanner.updater.GraphUpdaterManager; -import org.opentripplanner.updater.GraphWriterRunnable; import org.opentripplanner.updater.spi.UpdaterConstructionException; +import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDataSource; import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.opentripplanner.updater.vehicle_rental.datasources.params.VehicleRentalDataSourceParameters; +import org.opentripplanner.utils.lang.RunnableUtils; class VehicleRentalUpdaterTest { @@ -68,15 +64,7 @@ void failingSetup() { static class MockManager extends GraphUpdaterManager { public MockManager(VehicleRentalUpdater updater) { - super( - new DefaultRealTimeUpdateContext(new Graph(), new TimetableRepository()), - List.of(updater) - ); - } - - @Override - public Future execute(GraphWriterRunnable runnable) { - return Futures.immediateVoidFuture(); + super(WriteToGraphCallback.NOOP, RunnableUtils.NOOP, List.of(updater)); } } diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index a44321d62da..06051d3b805 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -94,6 +94,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Markdown prettier formatting [#7815](https://github.com/opentripplanner/OpenTripPlanner/pull/7815) - Report invalid line ref as DataImportIssue in NeTEx TripMapper [#7790](https://github.com/opentripplanner/OpenTripPlanner/pull/7790) - Fix ArrayIndexOutOfBoundsException in pass-through via chaining [#7747](https://github.com/opentripplanner/OpenTripPlanner/pull/7747) +- Snapshot framework [#7689](https://github.com/opentripplanner/OpenTripPlanner/pull/7689) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.9.0 (2026-03-18) diff --git a/pom.xml b/pom.xml index 6f4c454f6d7..6f141121c83 100644 --- a/pom.xml +++ b/pom.xml @@ -58,15 +58,15 @@ - 266 + 267 34.4 - 2.59.1 + 2.59.2 2.22.0 4.0.9 3.1.12 - 6.1.0 + 6.1.2 1.14.1 5.6.0 1.5.37 diff --git a/utils/src/main/java/org/opentripplanner/utils/lang/RunnableUtils.java b/utils/src/main/java/org/opentripplanner/utils/lang/RunnableUtils.java new file mode 100644 index 00000000000..8f755f66f12 --- /dev/null +++ b/utils/src/main/java/org/opentripplanner/utils/lang/RunnableUtils.java @@ -0,0 +1,14 @@ +package org.opentripplanner.utils.lang; + +/** + * Utilities extending Java {@link Runnable} features. + */ +public class RunnableUtils { + + private RunnableUtils() {} + + /** + * A runnable that does nothing. + */ + public static final Runnable NOOP = () -> {}; +} diff --git a/utils/src/test/java/org/opentripplanner/utils/lang/RunnableUtilsTest.java b/utils/src/test/java/org/opentripplanner/utils/lang/RunnableUtilsTest.java new file mode 100644 index 00000000000..3d5e4b1e17c --- /dev/null +++ b/utils/src/test/java/org/opentripplanner/utils/lang/RunnableUtilsTest.java @@ -0,0 +1,11 @@ +package org.opentripplanner.utils.lang; + +import org.junit.jupiter.api.Test; + +class RunnableUtilsTest { + + @Test + void runningNOOPShouldNotThrow() { + RunnableUtils.NOOP.run(); + } +}