From 0b8dfc69817bc19bef947ed7d191d8b6de54fdb8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 16 Dec 2023 03:02:54 +0800 Subject: [PATCH 01/24] add javadoc to some realtime classes --- .../routing/alertpatch/TransitAlert.java | 8 +++ .../updater/spi/GraphUpdater.java | 52 +++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java index d5d260a8218..0d188df5ef4 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java @@ -15,6 +15,14 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.TransitBuilder; +/** + * Internal representation of a GTFS-RT Service Alert or SIRI Situation Exchange (SX) message. + * These are text descriptions of problems affecting specific stops, routes, or other components + * of the transit system which will be displayed to users as text. + * Although they have flags describing the effect of the problem described in the text, convention + * is that these messages do not modify routing behavior on their own. They must be accompanied by + * other messages such as + */ public class TransitAlert extends AbstractTransitEntity { private final I18NString headerText; diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index 959827813cb..022d37537b1 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -1,39 +1,46 @@ package org.opentripplanner.updater.spi; /** - * Interface for graph updaters. Objects that implement this interface should always be configured - * via PreferencesConfigurable.configure after creating the object. GraphUpdaterConfigurator should - * take care of that. Beware that updaters run in separate threads at the same time. + * Interface for classes that fetch or receive information while the OTP instance is running and + * make changes to the Graph and associated transit data to reflect the current situation. This is + * typically information about disruptions to service, bicycle or parking availability, etc. *

- * The only allowed way to make changes to the graph in an updater is by executing (anonymous) - * GraphWriterRunnable objects via GraphUpdaterManager.execute. + * Each GraphUpdater implementation will be run in a separate thread, allowing it to make blocking + * calls to fetch data or even sleep between periodic polling operations without affecting the rest + * of the OTP instance. *

- * Example implementations can be found in ExampleGraphUpdater and ExamplePollingGraphUpdater. + * GraphUpdater implementations are instantiated by UpdaterConfigurator. Each updater configuration + * item in the router-config for a ThingUpdater is mapped to a corresponding configuration class + * ThingUpdaterParameters, which is passed to the ThingUpdater constructor. + *

+ * GraphUpdater implementations are only allowed to make changes to the Graph and related structures + * by submitting instances implementing GraphWriterRunnable (often anonymous functions) to the + * Graph writing callback function supplied to them by the GraphUpdaterManager after they're + * constructed. In this way, changes are queued up by many GraphUpdaters running in parallel on + * different threads, but are applied sequentially in a single-threaded manner to simplify reasoning + * about concurrent reads and writes to the Graph. */ public interface GraphUpdater { - /** - * Graph updaters must be aware of their manager to be able to execute GraphWriterRunnables. - * GraphUpdaterConfigurator should take care of calling this function. - */ - void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph); /** - * Here the updater can be initialized. If it throws, the updater won't be started (i.e. the run - * method won't be called). All updaters' setup methods will be run sequentially in a - * single-threaded manner before updates begin, in order to avoid concurrent reads/writes. + * After a GraphUpdater is instantiated, the GraphUpdaterManager that instantiated it will + * immediately supply a callback via this method. The GraphUpdater will employ that callback + * every time it wants to queue up a write modification to the Graph or related data structures. */ - //void setup(Graph graph, TransitModel transitModel) throws Exception; + void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph); /** - * This method will run in its own thread. It pulls or receives updates and applies them to the - * graph. It must perform any writes to the graph by passing GraphWriterRunnables to - * GraphUpdaterManager.execute(). This queues up the write operations, ensuring that only one - * updater performs writes at a time. + * The GraphUpdaterManager will run this method in its own long-running thread. This method then + * pulls or receives updates and applies them to the graph. It must perform any writes to the + * graph by passing GraphWriterRunnables to the WriteToGraphCallback, which queues up the write + * operations, ensuring that only one submitted update performs writes at a time. */ void run() throws Exception; /** - * Here the updater can clean up after itself. + * When the GraphUpdaterManager wants to stop all GraphUpdaters (for example when OTP is shutting + * down) it will call this method, allowing the GraphUpdater implementation to shut down cleanly + * and release resources. */ default void teardown() {} @@ -49,8 +56,9 @@ default boolean isPrimed() { } /** - * This is the updater "type" used in the configuration file. It should ONLY be used to provide - * human friendly messages while logging and debugging. + * A GraphUpdater implementation uses this method to report its corresponding value of the "type" + * field in the configuration file. This value should ONLY be used when providing human-friendly + * messages while logging and debugging. Association of configuration to particular types is */ String getConfigRef(); } From 2bb60a0cbbc4a07a33e4072e157771ed93b3af22 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 16 Dec 2023 03:04:11 +0800 Subject: [PATCH 02/24] add a few incomplete javadoc ideas --- .../ext/siri/SiriTimetableSnapshotSource.java | 2 ++ .../ext/siri/updater/EstimatedTimetableSource.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 345f8deba20..683c4615292 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -133,10 +133,12 @@ public TimetableSnapshot getTimetableSnapshot() { /** * Method to apply a trip update list to the most recent version of the timetable snapshot. + * FIXME TripUpdate is the GTFS term, and these SIRI ETs are never converted into that same internal model. * * @param fullDataset true iff the list with updates represent all updates that are active right * now, i.e. all previous updates should be disregarded * @param updates SIRI VehicleMonitoringDeliveries that should be applied atomically + * FIXME aren't these ET deliveries, not VM? */ public UpdateResult applyEstimatedTimetable( @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java index ac98839fb42..3c5a387ca09 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java @@ -3,6 +3,11 @@ import java.util.Optional; import uk.org.siri.siri20.Siri; +/** + * Interface for a blocking, polling approach + * TODO should the methods return as fast as possible? + * Or do they intentionally wait for refreshed data? + */ public interface EstimatedTimetableSource { /** * Wait for one message to arrive, and decode it into a List of TripUpdates. Blocking call. From 3f79a1db3dd6fedb4af5717798a1150b096a7dc8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Mon, 18 Dec 2023 23:19:59 +0800 Subject: [PATCH 03/24] javadoc and code comment updates --- .../ext/siri/SiriTimetableSnapshotSource.java | 5 +- .../ext/siri/SiriTripPatternCache.java | 124 ++++++++++++------ .../ext/siri/SiriTripPatternIdGenerator.java | 15 +-- .../org/opentripplanner/model/Timetable.java | 9 +- .../model/TimetableSnapshot.java | 106 +++++++++++---- .../model/TimetableSnapshotProvider.java | 10 +- .../transit/model/network/StopPattern.java | 1 + .../transit/model/network/TripPattern.java | 40 +++++- .../updater/spi/GraphUpdater.java | 9 +- .../updater/trip/TripPatternCache.java | 24 +++- 10 files changed, 239 insertions(+), 104 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 683c4615292..08752a50b89 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -375,11 +375,10 @@ private Result addTripToGraphAndBuffer(TripUpdate tr serviceDate ); - // Add new trip times to the buffer and return success + // Add new trip times to the buffer and return result with success or error. The update method + // will perform protective copies as needed whether TripPattern is from realtime data or not. var result = buffer.update(pattern, tripUpdate.tripTimes(), serviceDate); - LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); - return result; } diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 8b33d71ab96..bb2de82a281 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -12,7 +12,6 @@ import javax.annotation.Nonnull; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; @@ -20,23 +19,42 @@ import org.slf4j.LoggerFactory; /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime messages. + * Threadsafe mechanism for tracking any TripPatterns added to the graph via SIRI realtime messages. + * This tracks only patterns added by realtime messages, not ones that already existed from the + * scheduled NeTEx. This is a "cache" in the sense that it will keep returning the same TripPattern + * when presented with the same StopPattern, so if realtime messages add many trips passing through + * the same sequence of stops, they will all end up on this same TripPattern. + *

+ * Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. + * See additional comments in the Javadoc of the GTFS-RT version of this class. */ public class SiriTripPatternCache { private static final Logger log = LoggerFactory.getLogger(SiriTripPatternCache.class); + // Seems to be the primary collection of added TripPatterns, with other collections serving as + // indexes. Similar to TripPatternCache.cache but with service date as part of the key. private final Map cache = new HashMap<>(); + // Apparently a SIRI-specific index for use in GraphQL APIs (missing on GTFS-RT version). private final ListMultimap patternsForStop = Multimaps.synchronizedListMultimap( ArrayListMultimap.create() ); + // TODO clarify name and add documentation to this field private final Map updatedTripPatternsForTripCache = new HashMap<>(); + // TODO generalize this so we can generate IDs for SIRI or GTFS-RT sources private final SiriTripPatternIdGenerator tripPatternIdGenerator; + + // TODO clarify name and add documentation to this field, and why it's constructor injected private final Function getPatternForTrip; + /** + * Constructor. + * TODO: clarify why the ID generator and pattern fetching function are injected. Potentially + * make the class usable for GTFS-RT cases by injecting different ID generator etc. + */ public SiriTripPatternCache( SiriTripPatternIdGenerator tripPatternIdGenerator, Function getPatternForTrip @@ -45,9 +63,15 @@ public SiriTripPatternCache( this.getPatternForTrip = getPatternForTrip; } + // Below was clearly derived from a method from TripPatternCache, down to the obsolete Javadoc + // mentioning transit vertices and edges (which don't exist since raptor was adopted). + // Note that this is the only non-dead-code public method on this class, and mirrors the only + // public method on the GTFS-RT version of TripPatternCache. + // It also explains why this class is called a "cache". It allows reusing the same TripPattern + // instance when many different trips are created or updated with the same pattern. + /** - * Get cached trip pattern or create one if it doesn't exist yet. If a trip pattern is created, - * vertices and edges for this trip pattern are also created in the transitModel. + * Get cached trip pattern or create one if it doesn't exist yet. * * @param stopPattern stop pattern to retrieve/create trip pattern * @param trip Trip containing route of new trip pattern in case a new trip pattern will be @@ -61,6 +85,9 @@ public synchronized TripPattern getOrCreateTripPattern( ) { TripPattern originalTripPattern = getPatternForTrip.apply(trip); + // TODO: verify, this is different than GTFS-RT version + // It can return a TripPattern from the scheduled data, but protective copies are handled + // in TimetableSnapshot.update. Document better this aspect of the contract in this method's Javadoc. if (originalTripPattern.getStopPattern().equals(stopPattern)) { return originalTripPattern; } @@ -90,38 +117,41 @@ public synchronized TripPattern getOrCreateTripPattern( cache.put(key, tripPattern); } - /** - * - * When the StopPattern is first modified (e.g. change of platform), then updated (or vice versa), the stopPattern is altered, and - * the StopPattern-object for the different states will not be equal. - * - * This causes both tripPatterns to be added to all unchanged stops along the route, which again causes duplicate results - * in departureRow-searches (one departure for "updated", one for "modified"). - * - * Full example: - * Planned stops: Stop 1 - Platform 1, Stop 2 - Platform 1 - * - * StopPattern #rt1: "updated" stopPattern cached in 'patternsForStop': - * - Stop 1, Platform 1 - * - StopPattern #rt1 - * - Stop 2, Platform 1 - * - StopPattern #rt1 - * - * "modified" stopPattern: Stop 1 - Platform 1, Stop 2 - Platform 2 - * - * StopPattern #rt2: "modified" stopPattern cached in 'patternsForStop' will then be: - * - Stop 1, Platform 1 - * - StopPattern #rt1, StopPattern #rt2 - * - Stop 2, Platform 1 - * - StopPattern #rt1 - * - Stop 2, Platform 2 - * - StopPattern #rt2 - * - * - * Therefore, we must cleanup the duplicates by deleting the previously added (and thus outdated) - * tripPattern for all affected stops. In example above, "StopPattern #rt1" should be removed from all stops - * - */ + /* + When the StopPattern is first modified (e.g. change of platform), then updated (or vice + versa), the stopPattern is altered, and the StopPattern-object for the different states will + not be equal. + + This causes both tripPatterns to be added to all unchanged stops along the route, which again + causes duplicate results in departureRow-searches (one departure for "updated", one for + "modified"). + + Full example: + Planned stops: Stop 1 - Platform 1, Stop 2 - Platform 1 + + StopPattern #rt1: "updated" stopPattern cached in 'patternsForStop': + - Stop 1, Platform 1 + - StopPattern #rt1 + - Stop 2, Platform 1 + - StopPattern #rt1 + + "modified" stopPattern: Stop 1 - Platform 1, Stop 2 - Platform 2 + + StopPattern #rt2: "modified" stopPattern cached in 'patternsForStop' will then be: + - Stop 1, Platform 1 + - StopPattern #rt1, StopPattern #rt2 + - Stop 2, Platform 1 + - StopPattern #rt1 + - Stop 2, Platform 2 + - StopPattern #rt2 + + Therefore, we must clean up the duplicates by deleting the previously added (and thus + outdated) tripPattern for all affected stops. In example above, "StopPattern #rt1" should be + removed from all stops. + + TODO explore why this particular case is handled in an ad-hoc manner. It seems like all such + indexes should be constantly rebuilt and versioned along with the TimetableSnapshot. + */ TripServiceDateKey tripServiceDateKey = new TripServiceDateKey(trip, serviceDate); if (updatedTripPatternsForTripCache.containsKey(tripServiceDateKey)) { // Remove previously added TripPatterns for the trip currently being updated - if the stopPattern does not match @@ -138,10 +168,8 @@ public synchronized TripPattern getOrCreateTripPattern( (System.currentTimeMillis() - t1), trip.getId() ); - /* - TODO: Also remove previously updated - now outdated - TripPattern from cache ? - cache.remove(new StopPatternServiceDateKey(cachedTripPattern.stopPattern, serviceDate)); - */ + // TODO: Also remove previously updated - now outdated - TripPattern from cache ? + // cache.remove(new StopPatternServiceDateKey(cachedTripPattern.stopPattern, serviceDate)); } } @@ -160,6 +188,7 @@ public synchronized TripPattern getOrCreateTripPattern( /** * Returns any new TripPatterns added by real time information for a given stop. + * TODO: this appears to be currently unused. Perhaps remove it if the API has changed. * * @param stop the stop * @return list of TripPatterns created by real time sources for the stop. @@ -169,6 +198,16 @@ public List getAddedTripPatternsForStop(RegularStop stop) { } } +//// Below here are multiple additional private classes defined in the same top-level class file. +//// TODO: move these private classes inside the above class as private static inner classes. + +/** + * Serves as the key for the collection of realtime-added TripPatterns. + * Must define hashcode and equals to confer semantic identity. + * It seems like there's a separate TripPattern instance for each StopPattern and service date, + * rather a single TripPattern instance associated with a separate timetable for each date. + * TODO: clarify why each date has a different TripPattern instead of a different Timetable. + */ class StopPatternServiceDateKey { StopPattern stopPattern; @@ -194,6 +233,11 @@ public boolean equals(Object thatObject) { } } +/** + * An alternative key for looking up realtime-added TripPatterns by trip and service date instead + * of stop pattern and service date. Must define hashcode and equals to confer semantic identity. + * TODO verify whether one map is considered the definitive collection and the other an index. + */ class TripServiceDateKey { Trip trip; diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java index dc03550086f..e0882bd6215 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java @@ -8,20 +8,19 @@ import org.opentripplanner.transit.model.timetable.Trip; /** - * This class generate a new id for new TripPatterns created real-time by the SIRI updaters. It is - * important to creat only on instance of this class, and inject it where it is needed. - *

- * The id generation is thread-safe, even if that is probably not needed. + * This class generates new unique IDs for TripPatterns created in response to real-time updates + * from the SIRI updaters. It is important to create only one instance of this class, and inject + * that single instance wherever it is needed. The ID generation is threadsafe, even if that is + * probably not needed. */ class SiriTripPatternIdGenerator { private final AtomicInteger counter = new AtomicInteger(0); /** - * Generate unique trip pattern code for real-time added trip pattern. This function roughly - * follows the format of {@link GenerateTripPatternsOperation}. - *

- * The generator add a postfix 'RT' to indicate that this trip pattern is generated at REAL-TIME. + * Generate a unique ID for a trip pattern added in response to a realtime message. This function + * roughly follows the format of {@link GenerateTripPatternsOperation}. The generator suffixes the + * ID with 'RT' to indicate that this trip pattern is generated in response to a realtime message. */ FeedScopedId generateUniqueTripPatternId(Trip trip) { Route route = trip.getRoute(); diff --git a/src/main/java/org/opentripplanner/model/Timetable.java b/src/main/java/org/opentripplanner/model/Timetable.java index a8f0f1bbf44..53b4a24678f 100644 --- a/src/main/java/org/opentripplanner/model/Timetable.java +++ b/src/main/java/org/opentripplanner/model/Timetable.java @@ -42,14 +42,15 @@ import org.slf4j.LoggerFactory; /** + * A Timetable is a TripTimes (stop-level details like arrival and departure times) for each of the + * trips on a particular TripPattern. * Timetables provide most of the TripPattern functionality. Each TripPattern may possess more than * one Timetable when stop time updates are being applied: one for the scheduled stop times, one for * each snapshot of updated stop times, another for a working buffer of updated stop times, etc. *

- * TODO OTP2 - Move this to package: org.opentripplanner.model - * - after as Entur NeTEx PRs are merged. - * - Also consider moving its dependencies in: org.opentripplanner.routing - * - The NEW Timetable should not have any dependencies to + * TODO OTP2 - Move this to package: org.opentripplanner.model after as Entur NeTEx PRs are merged. + * Also consider moving its dependencies into package org.opentripplanner.routing. The NEW + * Timetable should not have any dependencies to [?] */ public class Timetable implements Serializable { diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 066a27ba15a..3c5beddb322 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -20,7 +20,6 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateSuccess; @@ -28,59 +27,107 @@ import org.slf4j.LoggerFactory; /** - * Part of concurrency control for stoptime updates. + * A TimetableSnapshot holds a set of realtime-updated Timetables frozen at a moment in time. It + * can return a Timetable for any TripPattern in the public transit network considering all + * accumulated realtime updates, falling back on the scheduled Timetable if no updates have been + * applied for a given TripPattern. *

- * All updates should be performed on a snapshot before it is handed off to any searches. A single + * This is a central part of managing concurrency when many routing searches may be happening, but + * realtime updates are also streaming in which change the vehicle arrival and departure times. + * Any given request will only see one unchanging TimetableSnapshot over the course of its search. + *

+ * An instance of TimetableSnapshot first serves as a buffer to accumulate a batch of incoming + * updates on top of any already known updates to the base schedules. From time to time such a batch + * of updates is committed (like a database transaction). At this point the TimetableSnapshot is + * treated as immutable and becomes available for use by new incoming routing requests. + *

+ * All updates to a snapshot must be completed before it is handed off to any searches. A single * snapshot should be used for an entire search, and should remain unchanged for that duration to * provide a consistent view not only of trips that have been boarded, but of relative arrival and * departure times of other trips that have not necessarily been boarded. *

- * At this point, only one writing thread at a time is supported. + * A TimetableSnapshot instance may only be modified by a single thread. This makes it easier to + * reason about how the snapshot is built up and used. Write operation are applied one by one in + * order with no concurrent access, then read operations are allowed concurrently by many threads + * once writing is forbidden. *

+ * The fact that TripPattern instances carry a reference only to their scheduled Timetable and not + * to their realtime timetable is largely due to historical path-dependence in OTP development. + * Streaming realtime support was added around 2013 as a sort of sandbox feature that was switched + * off by default. Looking up realtime timetables during routing was a fringe feature that needed + * to impose near-zero cost and avoid introducing complexity into the primary codebase. Now over + * ten years later, the principles of how this system operates are rather stable, but the + * implementation would benefit from some deduplication and cleanup. Once that is complete, looking + * up timetables on this class could conceivably be replaced with snapshotting entire views of the + * transit network. It would also be possible to make the realtime version of Timetables or + * TripTimes the primary view, and include references back to their scheduled versions. */ public class TimetableSnapshot { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshot.class); /** - * A set of all timetables which have been modified and are waiting to be indexed. When - * dirty is null, it indicates that the snapshot is read-only. + * During the construction phase of the TimetableSnapshot, before it is considered immutable and + * used in routing, this Set holds all timetables that have been modified and are waiting to be + * indexed. This field will be set to null when the TimetableSnapshot becomes read-only. */ private final Set dirtyTimetables = new HashSet<>(); /** - * The timetables for different days, for each TripPattern (each sequence of stops on a particular - * Route) for which we have an updated Timetable. The keys include both TripPatterns from the - * scheduled GTFS, and TripPatterns added by realtime messages and tracked by the - * TripPatternCache. Note that the keys will not include all scheduled TripPatterns, only those - * for which we've got an update. We use a HashMap rather than a Map so we can clone it. If this - * turns out to be slow/spacious we can use an array with integer pattern indexes. The SortedSet - * members are copy-on-write. - * FIXME: this could be made into a flat hashtable with compound keys. + * For each TripPattern (sequence of stops on a particular Route) for which we have received a + * realtime update, an ordered set of timetables on different days. The key TripPatterns may + * include ones from the scheduled GTFS, as well as ones added by realtime messages and + * tracked by the TripPatternCache.

+ * Note that the keys do not include all scheduled TripPatterns, only those for which we have at + * least one update. The type of the field is specifically HashMap (rather than the more general + * Map interface) because we need to efficiently clone it.

+ * The members of the SortedSet (the Timetable for a particular day) are treated as copy-on-write + * when we're updating them. If an update will modify the timetable for a particular day, that + * timetable is replicated before any modifications are applied to avoid affecting any previous + * TimetableSnapshots still in circulation which reference that same Timetable instance.

+ * Alternative implementations: A. This could be an array indexed using the integer pattern + * indexes. B. It could be made into a flat hashtable with compound keys (TripPattern, LocalDate). + * The compound key approach better reflects the fact that there should be only one Timetable per + * TripPattern and date. */ private HashMap> timetables = new HashMap(); /** - *

- * Map containing 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. - *

- *

- * This is a HashMap and not a Map so the clone function is available. + * For cases where the trip pattern (sequence of stops visited) has been changed by a realtime + * update, a Map associating the updated trip pattern with a compound key of the feed-scoped + * trip ID and the service date. The type of this field is HashMap rather than the more general + * Map interface because we need to efficiently clone it whenever we start building up a new + * snapshot. TODO: clarify if this is an index or the original source of truth. */ private HashMap realtimeAddedTripPattern = new HashMap<>(); /** - * This maps contains all of the new or updated TripPatterns added by realtime data indexed on - * stop. This has to be kept in order for them to be included in the stop times api call on a - * specific stop. - *

- * This is a SetMultimap, so that each pattern can only be added once. - *

- * TODO Find a generic way to keep all realtime indexes. + * This is an index of TripPatterns, not the primary collection. + * It tracks which TripPatterns that were updated or newly created by realtime messages contain + * which stops. This allows them to be readily found and included in API responses containing + * stop times at a specific stop. This is a SetMultimap, so that each pattern is only retained + * once per stop even if it's added more than once. + * TODO: Better, more general handling of all realtime indexes outside primary data structures. */ private SetMultimap patternsForStop = HashMultimap.create(); + /** + * This is an as-yet unused alternative to the current boolean fields readOnly and dirty, as well + * as setting dirtyTimetables to null. A given instance of TimetableSnapshot should progress + * through all these states in order, and cannot return to a previous state. + */ + private enum TimetableSnapshotState { + WRITABLE_CLEAN, WRITBLE_DIRTY, INDEXING, READ_ONLY + } + + /** + * Which stage of existence this TimetableSnapshot is in, which determines whether it's read-only. + * Writing to TimetableSnapshots is not concurrent and does not happen in hot methods. On the + * other hand, reading is expected to be highly concurrent and happens during core routing + * processes. Therefore, any assertions about state should be concentrated in the writing methods. + */ + private TimetableSnapshotState state; + /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. @@ -164,8 +211,9 @@ public boolean hasRealtimeAddedTripPatterns() { } /** - * Update the trip times of one trip in a timetable of a trip pattern. If the trip of the trip - * times does not exist yet in the timetable, add it. + * 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. * * @param pattern trip pattern * @param updatedTripTimes updated trip times diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java b/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java index 3f3e54dbc30..047f0301141 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java @@ -1,9 +1,13 @@ package org.opentripplanner.model; /** - * This interface is used to retrieve the current instance of the TimetableSnapshot. Any provider - * implementing this interface is responsible for thread-safe access to the latest valid instance of - * the {@code TimetableSnapshot}. + * This interface is used to retrieve the latest available instance of TimetableSnapshot + * that is ready for use in routing. Slightly newer TimetableSnapshots may be available, but still + * in the process of accumulating updates or being indexed and finalized for read-only routing use. + *

+ * Any provider implementing this interface is responsible for ensuring access to the latest + * {@code TimetableSnapshot} is handled in a thread-safe manner, as this method can be called by + * any number of concurrent routing requests at once. *

* Note that in the long run we don't necessarily want multiple snapshot providers. Ideally we'll * just have one way of handling these concurrency concerns, so no need for an interface and diff --git a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index fe1bb84cf3f..c1173994ac7 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -84,6 +84,7 @@ public static StopPatternBuilder create(int length) { return new StopPatternBuilder(new StopPattern(length)); } + // TODO: name is deceptive as this does not mutate the object in place, it mutates a copy public StopPatternBuilder mutate() { return new StopPatternBuilder(this); } diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 7057d9fd56e..aff20e9a1ba 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -56,29 +56,55 @@ public final class TripPattern private static final Logger LOG = LoggerFactory.getLogger(TripPattern.class); private final Route route; + /** - * The stop-pattern help us reuse the same stops in several trip-patterns; Hence saving memory. - * The field should not be accessible outside the class, and all access is done through method - * delegation, like the {@link #numberOfStops()} and {@link #canBoard(int)} methods. + * The Route and StopPattern together form the primary key of the TripPattern. They are the shared + * set of characteristics that group many trips together into one TripPattern. This grouping saves + * memory by not replicating any details shared across all trips in the TripPattern, but it is + * also essential to some optimizations in routing algorithms like Raptor. + *

+ * This field should not be accessed outside this class. All access to the StopPattern is + * performed through method delegation, like the {@link #numberOfStops()} and + * {@link #canBoard(int)} methods. */ private final StopPattern stopPattern; + + /** + * TripPatterns hold a reference to a Timetable (i.e. TripTimes for all Trips in the pattern) for + * only scheduled trips from the GTFS or NeTEx data. If any trips were later updated in real time, + * there will be another Timetable holding those updates and reading through to the scheduled one. + * This realtime Timetable is retrieved from a TimetableSnapshot. + * Also see end of Javadoc on TimetableSnapshot for more details. + */ private final Timetable scheduledTimetable; + + // This TransitMode is a redundant replication/memoization of information on the Route. + // It appears that in the TripPatternBuilder it is only ever set from a Trip which is itself set + // from a Route. TODO confirm whether there is any reason this doesn't just read through to Route. private final TransitMode mode; + private final SubMode netexSubMode; private final boolean containsMultipleModes; private String name; - /** - * Geometries of each inter-stop segment of the tripPattern. - */ + + /** Geometries of each inter-stop segment of the tripPattern. */ private final byte[][] hopGeometries; /** * The original TripPattern this replaces at least for one modified trip. + * + * Currently this seems to only be set (via TripPatternBuilder) from TripPatternCache and + * SiriTripPatternCache. + * + * FIXME this is only used rarely, make that obvious from comments. */ private final TripPattern originalTripPattern; /** - * Has the TripPattern been created by a real-time update. + * When a trip is added or rerouted by a realtime update, this may give rise to a new TripPattern + * that did not exist in the scheduled data. For such TripPatterns this field will be true. If on + * the other hand this TripPattern instance was created from the schedule data, this field will be + * false. */ private final boolean createdByRealtimeUpdater; diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index 022d37537b1..5c8490ae9bb 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -3,16 +3,13 @@ /** * Interface for classes that fetch or receive information while the OTP instance is running and * make changes to the Graph and associated transit data to reflect the current situation. This is - * typically information about disruptions to service, bicycle or parking availability, etc. - *

+ * typically information about disruptions to service, bicycle or parking availability, etc.

* Each GraphUpdater implementation will be run in a separate thread, allowing it to make blocking * calls to fetch data or even sleep between periodic polling operations without affecting the rest - * of the OTP instance. - *

+ * of the OTP instance.

* GraphUpdater implementations are instantiated by UpdaterConfigurator. Each updater configuration * item in the router-config for a ThingUpdater is mapped to a corresponding configuration class - * ThingUpdaterParameters, which is passed to the ThingUpdater constructor. - *

+ * ThingUpdaterParameters, which is passed to the ThingUpdater constructor.

* GraphUpdater implementations are only allowed to make changes to the Graph and related structures * by submitting instances implementing GraphWriterRunnable (often anonymous functions) to the * Graph writing callback function supplied to them by the GraphUpdaterManager after they're diff --git a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java index b2bf7dc410f..4b92ef6861d 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java +++ b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java @@ -13,9 +13,18 @@ import org.opentripplanner.transit.model.timetable.Trip; /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime messages. + * Threadsafe mechanism for tracking any TripPatterns added to the graph via GTFS realtime messages. * This tracks only patterns added by realtime messages, not ones that already existed from the - * scheduled GTFS. + * scheduled GTFS. This is a "cache" in the sense that it will keep returning the same TripPattern + * when presented with the same StopPattern, so if realtime messages add many trips passing through + * the same sequence of stops, they will all end up on this same TripPattern. + *

+ * Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. + * TODO: consolidate TripPatternCache and SiriTripPatternCache. They seem to only be separate + * because SIRI- or GTFS-specific indexes of the added TripPatterns seem to have been added to + * this primary collection. + * FIXME: the name does not make it clear that this has anything to do with elements that are only + * added due to realtime updates, and it is only loosely a cache. RealtimeAddedTripPatterns? */ public class TripPatternCache { @@ -24,11 +33,12 @@ public class TripPatternCache { * already existed from the scheduled GTFS. */ private final Map cache = new HashMap<>(); + + /** Used for producing sequential integers to ensure each added pattern has a unique name. */ private int counter = 0; /** - * Get cached trip pattern or create one if it doesn't exist yet. If a trip pattern is created, - * vertices and edges for this trip pattern are also created in the graph. + * Get cached trip pattern or create one if it doesn't exist yet. * * @param stopPattern stop pattern to retrieve/create trip pattern * @param trip the trip the new trip pattern will be created for @@ -72,6 +82,12 @@ public synchronized TripPattern getOrCreateTripPattern( /** * Generate unique trip pattern code for real-time added trip pattern. This function roughly * follows the format of the {@link GenerateTripPatternsOperation}. + * In the SIRI version of this class, this is provided by a SiriTripPatternIdGenerator. If the + * GTFS-RT and SIRI version of these classes are merged, this function could become a second + * implementation of TripPatternIdGenerator. + * This method is not static because it references a monotonically increasing integer counter. + * But like in SiriTripPatternIdGenerator, this could be encapsulated outside the cache object. + * TODO: create GtfsRtTripPatternIdGenerator as part of merging the two TripPatternCaches */ private FeedScopedId generateUniqueTripPatternCode(Trip trip) { FeedScopedId routeId = trip.getRoute().getId(); From 7a0b56d7f3dd0871de7f0a14d958629060ec6a64 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Mon, 18 Dec 2023 23:27:43 +0800 Subject: [PATCH 04/24] chain builder methods also capitalize constant identifier --- .../ext/siri/SiriTripPatternCache.java | 17 +++++++---------- .../updater/trip/TripPatternCache.java | 12 +++++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index bb2de82a281..165feee7513 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -30,7 +30,7 @@ */ public class SiriTripPatternCache { - private static final Logger log = LoggerFactory.getLogger(SiriTripPatternCache.class); + private static final Logger LOG = LoggerFactory.getLogger(SiriTripPatternCache.class); // Seems to be the primary collection of added TripPatterns, with other collections serving as // indexes. Similar to TripPatternCache.cache but with service date as part of the key. @@ -99,20 +99,17 @@ public synchronized TripPattern getOrCreateTripPattern( // Create TripPattern if it doesn't exist yet if (tripPattern == null) { var id = tripPatternIdGenerator.generateUniqueTripPatternId(trip); - TripPatternBuilder tripPatternBuilder = TripPattern + tripPattern = TripPattern .of(id) .withRoute(trip.getRoute()) .withMode(trip.getMode()) .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern); - + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); // TODO - SIRI: Add pattern to transitModel index? - tripPatternBuilder.withCreatedByRealtimeUpdater(true); - tripPatternBuilder.withOriginalTripPattern(originalTripPattern); - - tripPattern = tripPatternBuilder.build(); - // Add pattern to cache cache.put(key, tripPattern); } @@ -162,7 +159,7 @@ Therefore, we must clean up the duplicates by deleting the previously added (and patternsForStop.values().removeAll(Arrays.asList(cachedTripPattern)); int sizeAfter = patternsForStop.values().size(); - log.debug( + LOG.debug( "Removed outdated TripPattern for {} stops in {} ms - tripId: {}", (sizeBefore - sizeAfter), (System.currentTimeMillis() - t1), diff --git a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java index 4b92ef6861d..c4f437dabde 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java +++ b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java @@ -60,17 +60,15 @@ public synchronized TripPattern getOrCreateTripPattern( // Generate unique code for trip pattern var id = generateUniqueTripPatternCode(trip); - TripPatternBuilder tripPatternBuilder = TripPattern + tripPattern = TripPattern .of(id) .withRoute(route) .withMode(trip.getMode()) .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern); - - tripPatternBuilder.withCreatedByRealtimeUpdater(true); - tripPatternBuilder.withOriginalTripPattern(originalTripPattern); - - tripPattern = tripPatternBuilder.build(); + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); // Add pattern to cache cache.put(stopPattern, tripPattern); From a0c18f4e468394614341eb542a6a418fb29531a8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Wed, 20 Dec 2023 22:29:02 +0800 Subject: [PATCH 05/24] add javadoc (incl. uncertain and open questions) --- .../raptoradapter/transit/TransitLayer.java | 10 ++++ .../transit/mappers/TransitLayerMapper.java | 16 ++--- .../transit/mappers/TransitLayerUpdater.java | 5 ++ .../opentripplanner/routing/graph/Graph.java | 58 +++++++++++++------ .../services/notes/StreetNotesService.java | 12 ++-- .../api/OtpServerRequestContext.java | 8 ++- .../ConstructApplicationFactory.java | 3 +- .../transit/service/TransitModel.java | 50 +++++++++++++--- .../transit/service/TransitService.java | 15 ++++- .../updater/trip/TimetableSnapshotSource.java | 21 +++---- 10 files changed, 145 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index 60cfb72ef7d..4188d5e569b 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -17,6 +17,16 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.service.StopModel; +/** + * This is a replica of public transportation data already present in TransitModel, but rearranged + * and indexed differently for efficient use by the Raptor router. + * Patterns and trips are split out by days, retaining only the services actually running on any + * particular day. + * + * TODO rename - this name appears to be modeled after R5, where the TransportNetwork is split into + * two layers: one for the streets and one for the public transit data. But here, this seems to be + * an indexed and rearranged copy of the main transit data collected under a TransitModel instance. + */ public class TransitLayer { /** diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java index d74e2fbcbdb..6c9bd67a7f5 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java @@ -32,15 +32,15 @@ import org.slf4j.LoggerFactory; /** - * Maps the TransitLayer object from the OTP Graph object. The ServiceDay hierarchy is reversed, + * Maps the TransitLayer object from the TransitModel object. The ServiceDay hierarchy is reversed, * with service days at the top level, which contains TripPatternForDate objects that contain only * TripSchedules running on that particular date. This makes it faster to filter out TripSchedules * when doing Range Raptor searches. *

- * CONCURRENCY: This mapper run part of the mapping in parallel using parallel streams. This improve - * startup time on the Norwegian graph by 20 seconds; reducing the this mapper from 36 seconds to 15 - * seconds, and the total startup time from 80 seconds to 60 seconds. (JAN 2020, MacBook Pro, 3.1 - * GHz i7) + * CONCURRENCY: This mapper runs part of the mapping in parallel using parallel streams. This + * improves startup time on the Norwegian network by 20 seconds, by reducing this mapper from 36 + * seconds to 15 seconds, and the total startup time from 80 seconds to 60 seconds. (JAN 2020, + * MacBook Pro, 3.1 GHz i7) */ public class TransitLayerMapper { @@ -59,8 +59,8 @@ public static TransitLayer map( return new TransitLayerMapper(transitModel).map(tuningParameters); } - // TODO We can save time by either pre-sorting these or use a sorting algorithm that is - // optimized for sorting nearly sorted list + // TODO We could save time by either pre-sorting these, or by using a sorting algorithm that is + // optimized for sorting nearly-sorted lists. static List getSortedTripTimes(Timetable timetable) { return timetable .getTripTimes() @@ -75,7 +75,7 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { ConstrainedTransfersForPatterns constrainedTransfers = null; StopModel stopModel = transitModel.getStopModel(); - LOG.info("Mapping transitLayer from Graph..."); + LOG.info("Mapping transitLayer from TransitModel..."); Collection allTripPatterns = transitModel.getAllTripPatterns(); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index bed27497587..9b9cc9f0c7f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -30,6 +30,11 @@ * id and replaced by their updated versions. The realtime TransitLayer is then switched out with * the updated copy in an atomic operation. This ensures that any TransitLayer that is referenced * from the Graph is never changed. + * + * This is a way of keeping the TransitLayer up to date (in sync with the TransitModel plus its most + * recent TimetableSnapshot) without repeatedly deriving it from scratch every few seconds. The same + * incremental changes are applied to both the TimetableSnapshot and the TransitLayer and they are + * published together. */ public class TransitLayerUpdater { diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 4b2ca75b1a7..42909952178 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -37,18 +37,41 @@ import org.slf4j.LoggerFactory; /** - * A graph is really just one or more indexes into a set of vertexes. It used to keep edgelists for - * each vertex, but those are in the vertex now. + * This is one of the main data structures in OpenTripPlanner. It represents a mathematical object + * called a graph (https://en.wikipedia.org/wiki/Graph_theory) relative to which many routing + * algorithms are defined. A graph is made up of vertices and edges. These are also referred to as + * nodes and arcs or links, but in OTP we always use the vertices and edges terminology. + *

+ * In OTP1, the Graph contained vertices and edges representing the entire transportation network, + * including edges representing both street segments and public transit lines connecting stops. In + * OTP2, the Graph edges now represent only the street network. Transit routing is performed on + * other data structures suited to the Raptor algorithm (the TransitModel). Some transit-related + * vertices are still present in the Graph, specifically those representing transit stops, + * entrances, and elevators. Their presence in the street graph creates a connection between the two + * routable data structures (identifying where stops in the TransitModel are located relative to + * roads). + *

+ * Other data structures related to street routing, such as elevation data and vehicle parking + * information, are also collected here as fields of the Graph. For historical reasons the Graph + * sometimes serves as a catch-all, as it used to be the root of the object tree representing the + * whole transportation network. + *

+ * In some sense the Graph is just some indexes into a set of vertices. The Graph used to hold lists + * of edges for each vertex, but those lists are now attached to the vertices themselves. + *

+ * TODO rename to StreetGraph to emphasize what it represents? */ public class Graph implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(Graph.class); + /** Attaches text notes to street edges, which do not affect routing. */ public final StreetNotesService streetNotesService = new StreetNotesService(); - /* Ideally we could just get rid of vertex labels, but they're used in tests and graph building. */ + // Ideally we could just get rid of vertex labels, but they're used in tests and graph building. private final Map vertices = new ConcurrentHashMap<>(); + /** Conserve memory by reusing immutable instances of Strings, integer arrays, etc. */ public final transient Deduplicator deduplicator; public final Instant buildTime = Instant.now(); @@ -58,10 +81,10 @@ public class Graph implements Serializable { private transient StreetIndex streetIndex; - //ConvexHull of all the graph vertices. Generated at Graph build time. + /** The convex hull of all the graph vertices. Generated at the time the Graph is built. */ private Geometry convexHull = null; - /* The preferences that were used for graph building. */ + /** The preferences that were used for building this Graph instance. */ public Preferences preferences = null; /** True if OSM data was loaded into this Graph. */ @@ -71,26 +94,27 @@ public class Graph implements Serializable { * Have bike parks already been linked to the graph. As the linking happens twice if a base graph * is used, we store information on whether bike park linking should be skipped. */ + public boolean hasLinkedBikeParks = false; /** * The difference in meters between the WGS84 ellipsoid height and geoid height at the graph's * center */ public Double ellipsoidToGeoidDifference = 0.0; - /** - * Does this graph contain elevation data? - */ + + /** True if this graph contains elevation data. */ public boolean hasElevation = false; - /** - * If this graph contains elevation data, the minimum value. - */ + + /** If this graph contains elevation data, the minimum elevation value. Otherwise null. */ public Double minElevation = null; - /** - * If this graph contains elevation data, the maximum value. - */ + + /** If this graph contains elevation data, the maximum elevation value. Otherwise null. */ public Double maxElevation = null; - /** The distance between elevation samples used in CompactElevationProfile. */ + /** + * The horizontal distance across the ground between successive elevation samples in + * CompactElevationProfile. + */ // TODO refactoring transit model: remove and instead always serialize directly from and to the // static variable in CompactElevationProfile in SerializedGraphObject private double distanceBetweenElevationSamples; @@ -132,9 +156,7 @@ public Graph() { this(new Deduplicator(), null); } - /** - * Add the given vertex to the graph. - */ + /** Add the given vertex to the graph. */ public void addVertex(Vertex v) { Vertex old = vertices.put(v.getLabel(), v); if (old != null) { diff --git a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java index 9dd24182461..92d73574530 100644 --- a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java +++ b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java @@ -15,11 +15,11 @@ import org.slf4j.LoggerFactory; /** - * This service manage street edge notes. An edge note is an free-format alert (text) attached to an - * edge, which is returned in the itinerary when this edge is used, and which *does not have any - * impact on routing*. The last restriction is necessary as the edge do not know which notes it is - * attached to (this to prevent having to store note lists in the edge, which is memory consuming as - * only few edges will have notes). + * This service manages street edge notes. An edge note is a free-format alert (text) attached to an + * edge, which is returned along with any itinerary where this edge is used, and which does not have any + * impact on routing. Notes cannot affect routing because edges do not know which notes are + * attached to them. This avoids storing references to notes on the edge, which would probably not + * be worth the memory consumption as only few edges have notes. *

* The service owns a list of StreetNotesSource, with a single static one used for graph building. * "Dynamic" notes can be returned by classes implementing StreetNoteSource, added to this service @@ -32,7 +32,7 @@ * traversed, ie "state back edge"). Usually matcher will match note based on the mode (cycling, * driving) or if a wheelchair access is requested. * - * @author laurent + * @author Laurent Grégoire */ public class StreetNotesService implements Serializable { diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index fa6ead99c5e..ac9da31d00e 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -30,10 +30,16 @@ /** * The purpose of this class is to allow APIs (HTTP Resources) to access the OTP Server Context. + * FIXME circular definition (OtpServerRequestContext provides access to OTP Server Context). + * What exactly does "Server Context" mean here? What does "access" mean? Does this mean it + * "allows individual requests to call a limited number of methods on the components of the server + * without direct access to their internals?" * By using an interface, and not injecting each service class we avoid giving the resources access * to the server implementation. The context is injected by Jersey. An alternative to injecting this * interface is to inject each individual component in the context - hence reducing the dependencies - * further. But there is not a "real" need for this. For example, we do not have unit tests on the + * further. + * TODO clarify how injecting more individual components would "reduce dependencies further". + * But there is not a "real" need for this. For example, we do not have unit tests on the * Resources. If we in the future would decide to write unit tests for the APIs, then we could * eliminate this interface and just inject the components. See the bind method in OTPServer. *

diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index a4f0ee49652..dc04c6220c2 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -35,7 +35,8 @@ import org.opentripplanner.visualizer.GraphVisualizer; /** - * Dagger dependency injection Factory to create components for the OTP construct application phase. + * A Factory used by the Dagger dependency injection system to create the components of OTP, which + * are then wired up to construct the application. */ @Singleton @Component( diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index 2f0162aa8b1..1f90764dda5 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -55,7 +55,25 @@ import org.slf4j.LoggerFactory; /** - * Repository for Transit entities. + * The TransitModel groups together all instances making up OTP's primary internal representation + * of the public transportation network. Although the names of many entities are derived from + * GTFS concepts, these are actually independent of the data source from which they are loaded. + * Both GTFS and NeTEx entities are mapped to these same internal OTP entities. + * + * A TransitModel instance also includes references to some transient indexes of its contents, to + * the TransitLayer derived from it, and to some other services and utilities that operate upon + * its contents. + * + * The TransitModel stands in opposition to two other aggregates: the Graph (representing the + * street network) and the TransitLayer (representing many of the same things in the TransitModel + * but rearranged to be more efficient for Raptor routing). + * + * At this point the TransitModel is not often read directly. Many requests will look at the + * TransitLayer rather than the TransitModel it's derived from. Both are often accessed via the + * TransitService rather than directly reading the fields of TransitModel or TransitLayer. + * + * TODO rename, as this is not really the model, but a top-level object grouping together instances + * of model classes with things that operate on and map those instances. */ public class TransitModel implements Serializable { @@ -79,6 +97,25 @@ public class TransitModel implements Serializable { private ZonedDateTime transitServiceStarts = LocalDate.MAX.atStartOfDay(ZoneId.systemDefault()); private ZonedDateTime transitServiceEnds = LocalDate.MIN.atStartOfDay(ZoneId.systemDefault()); + /** + * The TransitLayer representation (optimized and rearranged for Raptor) of this TransitModel's + * scheduled (non-realtime) contents. + */ + private transient TransitLayer transitLayer; + + /** + * This updater applies realtime changes queued up for the next TimetableSnapshot such that + * this TransitModel.realtimeSnapshot remains aligned with the service represented in + * (this TransitModel instance + that next TimetableSnapshot). This is a way of keeping the + * TransitLayer up to date without repeatedly deriving it from scratch every few seconds. The + * same incremental changes are applied to both sets of data and they are published together. + */ + private transient TransitLayerUpdater transitLayerUpdater; + + /** + * An optionally present second TransitLayer representing the contents of this TransitModel plus + * the results of realtime updates in the latest TimetableSnapshot. + */ private final transient ConcurrentPublished realtimeTransitLayer = new ConcurrentPublished<>(); private final transient Deduplicator deduplicator; @@ -102,9 +139,6 @@ public class TransitModel implements Serializable { private final Map> flexTripsById = new HashMap<>(); - private transient TransitLayer transitLayer; - private transient TransitLayerUpdater transitLayerUpdater; - private transient TransitAlertService transitAlertService; @Inject @@ -113,15 +147,15 @@ public TransitModel(StopModel stopModel, Deduplicator deduplicator) { this.deduplicator = deduplicator; } - /** Constructor for deserialization. */ + /** No-argument constructor, required for deserialization. */ public TransitModel() { this(new StopModel(), new Deduplicator()); } /** - * Perform indexing on timetables, and create transient data structures. This used to be done in - * readObject methods upon deserialization, but stand-alone mode now allows passing graphs from - * graphbuilder to server in memory, without a round trip through serialization. + * Perform indexing on timetables, and create transient data structures. This used to be done + * inline in readObject methods upon deserialization, but it is now possible to pass transit data + * from the graph builder to the server in memory, without a round trip through serialization. */ public void index() { if (index == null) { diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index d0664aa292d..24719239e58 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -43,7 +43,20 @@ import org.opentripplanner.updater.GraphUpdaterStatus; /** - * Entry point for read-only requests towards the transit API. + * TransitService is a read-only interface for retrieving public transport data. It provides a + * frozen view of all these elements at a point in time, which is not affected by incoming realtime + * data, allowing results to remain stable over the course of a request. This can be used for + * fetching tables of specific information like the routes passing through a particular stop, or for + * gaining access to the entirety of the data to perform routing. + *

+ * TODO this interface seems to provide direct access to the TransitLayer but not the TransitModel. + * Is this intentional, because TransitLayer is meant to be read-only and TransitModel is not? + * Should this be renamed TransitDataService since it seems to provide access to the data but + * not to transit routing functionality (which is provided by the RoutingService)? + * The DefaultTransitService implementation has a TransitModel instance and many of its methods + * read through to that TransitModel instance. But that field itself is not exposed, while the + * TransitLayer is here. It seems like exposing the raw TransitLayer is still a risk since it's + * copy-on-write and shares a lot of objects with any other TransitLayer instances. */ public interface TransitService { Collection getFeedIds(); diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 19db2c7e309..7ee181faa15 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -88,20 +88,18 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { /** * The working copy of the timetable snapshot. Should not be visible to routing threads. Should * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that - * might modify this buffer will correctly acquire the lock. + * might modify this buffer will correctly acquire the lock. By design, only one thread should + * ever be writing to this buffer. But we need to suspend writes while we're indexing and swapping + * out the buffer (Or do we? Can't we just make a new copy of the buffer first?) + * TODO: research why this lock is needed since only one thread should ever be writing to this buffer. + * Instead we should throw an exception if a writing section is entered by more than one thread. */ private final TimetableSnapshot buffer = new TimetableSnapshot(); - /** - * Lock to indicate that buffer is in use - */ + /** Lock to indicate that buffer is in use. */ private final ReentrantLock bufferLock = new ReentrantLock(true); - /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime - * messages. - */ - + /** A synchronized cache of trip patterns added to the graph due to GTFS-realtime messages. */ private final TripPatternCache tripPatternCache = new TripPatternCache(); private final ZoneId timeZone; @@ -121,7 +119,10 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { */ private volatile TimetableSnapshot snapshot = null; - /** Should expired real-time data be purged from the graph. */ + /** + * Should expired real-time data be purged from the graph. + * TODO clarify exactly what this means? In what circumstances would you want to turn it off? + */ private final boolean purgeExpiredData; protected LocalDate lastPurgeDate = null; From 710b6785cdd3351691938ae0d1ebf135942b5377 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sun, 21 Jan 2024 16:42:18 +0800 Subject: [PATCH 06/24] Add more Javadoc and questions to realtime code --- .../ext/siri/SiriAlertsUpdateHandler.java | 26 +++++++++++++++---- .../ext/siri/mapper/AffectsMapper.java | 4 +++ .../ext/siri/updater/SiriSXUpdater.java | 26 +++++++++++++++++++ .../framework/retry/OtpRetry.java | 5 ++++ .../routing/alertpatch/EntityKey.java | 6 +++++ .../routing/alertpatch/EntitySelector.java | 8 ++++++ .../DelegatingTransitAlertServiceImpl.java | 16 +++++++++++- .../routing/impl/TransitAlertServiceImpl.java | 15 +++++++++++ .../routing/services/TransitAlertService.java | 18 +++++++++++++ .../config/framework/json/EnumMapper.java | 11 ++++++++ .../framework/json/ParameterBuilder.java | 4 +++ .../updater/alert/TransitAlertProvider.java | 7 +++++ 12 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java index 68b055bda46..9568684105b 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java @@ -35,9 +35,11 @@ import uk.org.siri.siri20.WorkflowStatusEnumeration; /** - * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds. NOTE - * this cannot handle situations where there are multiple feeds with different IDs (for now it may - * only work in single-feed regions). + * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds. + * TODO REALTIME: The name should be clarified, as there is no such thing as "SIRI Alerts", and it + * is referencing the internal model concept of "Alerts" which are derived from GTFS terminology. + * NOTE this cannot handle situations where there are multiple feeds with different IDs (for now it + * may only work in single-feed regions). */ public class SiriAlertsUpdateHandler { @@ -45,8 +47,14 @@ public class SiriAlertsUpdateHandler { private final String feedId; private final Set alerts = new HashSet<>(); private final TransitAlertService transitAlertService; - /** How long before the posted start of an event it should be displayed to users */ + + /** How long before the posted start of an event it should be displayed to users. */ private final Duration earlyStart; + + /** + * This takes the parts of the SIRI SX message saying which transit entities are affected and + * maps them to multiple OTP internal model entities. + */ private final AffectsMapper affectsMapper; public SiriAlertsUpdateHandler( @@ -120,6 +128,12 @@ public void update(ServiceDelivery delivery) { } } + /** + * FIXME REALTIME This does not just "handle" an alert, it builds an internal model Alert from + * an incoming SIRI situation exchange element. It is a mapper or factory. + * It may return null if all of header, description, and detail text are empty or missing in the + * SIRI message. In all other cases it will return a valid TransitAlert instance. + */ private TransitAlert handleAlert(PtSituationElement situation) { TransitAlertBuilder alert = createAlertWithTexts(situation); @@ -199,7 +213,9 @@ private long getEpochSecond(ZonedDateTime startTime) { } /* - * Creates alert from PtSituation with all textual content + * Creates alert from PtSituation with all textual content. + * The feed scoped ID of this alert will be the single feed ID associated with this update handler + * and the situation number provided in the feed. */ private TransitAlertBuilder createAlertWithTexts(PtSituationElement situation) { return TransitAlert diff --git a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java index 734cc8edbd3..a1fb943f60c 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java +++ b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java @@ -33,6 +33,10 @@ /** * Maps a {@link AffectsScopeStructure} to a list of {@link EntitySelector}s + * + * Concretely: this takes the parts of the SIRI SX (Alerts) message describing which transit + * entities are concerned by the alert, and maps them to EntitySelectors, which can match multiple + * OTP internal model entities that should be associated with the message. */ public class AffectsMapper { diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java index 014d5b24061..817505d81c0 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java @@ -31,6 +31,7 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr private final String url; private final String originalRequestorRef; private final TransitAlertService transitAlertService; + // TODO What is this, why does it exist as a persistent instance? private final SiriAlertsUpdateHandler updateHandler; private WriteToGraphCallback saveResultOnGraph; private ZonedDateTime lastTimestamp = ZonedDateTime.now().minusWeeks(1); @@ -85,6 +86,7 @@ public SiriSXUpdater(SiriSXUpdaterParameters config, TransitModel transitModel) @Override public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { + // TODO REALTIME this callback should have a different name, it is currently too verb-like. this.saveResultOnGraph = saveResultOnGraph; } @@ -101,6 +103,9 @@ protected void runPolling() throws InterruptedException { retry.execute(this::updateSiri); } + /** + * This part has been factored out to allow repeated retries in case the connection fails etc. + */ private void updateSiri() { boolean moreData = false; do { @@ -112,6 +117,27 @@ private void updateSiri() { // primitive, because the object moreData persists across iterations. final boolean markPrimed = !moreData; if (serviceDelivery.getSituationExchangeDeliveries() != null) { + // FIXME REALTIME This is submitting a method on a long-lived instance as a runnable. + // These runnables were intended to be small, disposable self-contained update tasks. + // See org/opentripplanner/updater/trip/PollingTripUpdater.java:90 + // Clarify why that is passing in so many other references. It should only contain + // what's needed to operate on the graph. This should be illustrated in documentation + // as a little box labeled "change trip ABC123 by making stop 53 late by 2 minutes." + // Also clarify how this works without even using the supplied graph or TransitModel: + // there are multiple TransitAlertServices and they are not versioned along with the\ + // Graph, they are attached to updaters. + // This is submitting a runnable to an executor, but that runnable only writes back to + // objects owned by this updater itself with no versioning. Why is this happening? + // If this is an intentional choice to live-patch a single server-wide instance of an + // alerts service/index while it's already in use by routing, we should be clear about + // this and document why it differs from the graph-writer design. Currently the code + // seems to go through the a ritual of following the threadsafe copy-on-write pattern + // without actually doing so. + // It's understandable to defer the list-of-alerts processing to another thread than this + // fetching thread, but I don't think we want that happening on the graph writer thread. + // There seems to be a misunderstanding that the tasks are submitted to get them off the + // updater thread, but the real reason is to ensure consistent transactions in graph + // writing and reading. saveResultOnGraph.execute((graph, transitModel) -> { updateHandler.update(serviceDelivery); if (markPrimed) { diff --git a/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java b/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java index c53250c8fd6..1a9136653fd 100644 --- a/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java +++ b/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java @@ -17,6 +17,11 @@ public class OtpRetry { private final Duration initialRetryInterval; private final int backoffMultiplier; private final Runnable onRetry; + + /** + * A predicate to determine whether a particular exception should end the retry cycle or not. + * If the predicate returns true, retries will continue. False, and the retry cycle is broken. + */ private final Predicate retryableException; OtpRetry( diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java b/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java index 69d5a638e93..00f2bdafcd0 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java @@ -3,6 +3,12 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * This encompasses many different kinds of entity keys, all of which are simple record types, all + * grouped together as the only allowed implementations of a sealed marker interface. These key + * types represent various combinations used to look up Alerts that might be associated with a + * particular stop, or a stop on a route, or all routes of a certain type etc. + */ public sealed interface EntityKey { record Agency(FeedScopedId agencyId) implements EntityKey {} diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java b/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java index 9aa44ec8f2f..9fa94a6b795 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java @@ -5,6 +5,14 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * Describes which elements in the internal transit data model are affected by a realtime alert. + * Note that this is specific to alerts and doesn't seem to be used by anything else. + * This is probably because alerts are unique in their ability to attach themselves to many + * different routes, stops, etc. at once, while non-alert elements tend to be associated with very + * specific single other elements. + * @see EntityKey + */ public sealed interface EntitySelector { EntityKey key(); diff --git a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java index d8c5dd19f3b..cfa92ffaff3 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java @@ -18,11 +18,23 @@ * This class is used to combine alerts from multiple {@link TransitAlertService}s. Each * {@link TransitAlertProvider} has its own service, and all need to be queried in order to fetch * all alerts. + * + * Concretely: every realtime updater receiving GTFS Alerts or SIRI Situation Exchange (SX) + * messages currently maintains its own private index of alerts seperate from all other updaters. + * To make the set of all alerts from all updaters available in a single operaion and associate it + * with the graph as a whole, the various indexes are merged in such a way as to have the same + * index as each individual index. */ public class DelegatingTransitAlertServiceImpl implements TransitAlertService { private final ArrayList transitAlertServices = new ArrayList<>(); + /** + * Constructor which scans over all existing GraphUpdaters associated with a TransitModel + * instance and retains references to all their TransitAlertService instances. + * 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(TransitModel transitModel) { if (transitModel.getUpdaterManager() != null) { transitModel @@ -38,7 +50,9 @@ public DelegatingTransitAlertServiceImpl(TransitModel transitModel) { @Override public void setAlerts(Collection alerts) { - throw new UnsupportedOperationException("Not supported"); + throw new UnsupportedOperationException( + "This delegating TransitAlertService is not intended to hold any TransitAlerts of its own." + ); } @Override diff --git a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java index 285b4665c46..b63041d66e7 100644 --- a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java @@ -16,6 +16,16 @@ import org.opentripplanner.transit.service.TransitModel; /** + * This is the primary implementation of TransitAlertService, which actually retains its own set + * of TransitAlerts and indexes them for fast lookup by which transit entity is affected. + * The only other implementation exists just to combine several instances of this primary + * implementation into one. + * TODO REALTIME investigate why each updater has its own service instead of taking turns + * sequentially writing to a single service. Original design was for all data and indexes to be + * associated with the Graph or transit model (i.e. the object graph of instances of the transit + * model) and for updaters to submit write tasks that would patch the current version in a + * sequential way, e.g. "add these 10 alerts", "remove these 5 alerts", etc. + * * When an alert is added with more than one transit entity, e.g. a Stop and a Trip, both conditions * must be met for the alert to be displayed. This is the case in both the Norwegian interpretation * of SIRI, and the GTFS-RT alerts specification. @@ -32,6 +42,11 @@ public TransitAlertServiceImpl(TransitModel transitModel) { @Override public void setAlerts(Collection alerts) { + // NOTE this is being patched live by updaters while in use (being read) by other threads + // performing trip planning. The single-action assignment helps a bit, but the map can be + // swapped out while the delegating service is in the middle of multiple calls that read from it. + // The consistent approach would be to duplicate the entire service, update it copy-on write, + // and swap in the entire service after the update. Multimap newAlerts = HashMultimap.create(); for (TransitAlert alert : alerts) { for (EntitySelector entity : alert.entities()) { diff --git a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java index e80d56e039e..6dd1423ea8c 100644 --- a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java +++ b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java @@ -8,6 +8,24 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * The TransitAlertService stores a set of alerts (passenger-facing textual information associated + * with transit entities such as stops or routes) which are currently active and should be provided + * to end users when their itineraries include the relevant stop, route, etc. + * + * Its primary purpose is to index those alerts, which may be numerous, so they can be looked up + * rapidly and attached to the various pieces of an itinerary as it's being returned to the user. + * + * Most elements in an itinerary will have no alerts attached, so those cases need to return + * quickly. For example, no alerts on board stop A, no alerts on route 1 ridden, no alerts on alight + * stop B, no alerts on route 2 ridden, yes one alert found on alight stop C. + * + * The fact that alerts are relatively sparse (at the scale of the entire transportation system) + * is central to this implementation. Adding a list of alerts to every element in the system would + * mean storing large amounts of null or empty list references. Instead, alerts are looked up in + * maps allowing them to be attached to any object with minimal space overhead, but requiring some + * careful indexing to ensure their presence or absence on each object can be determined quickly. + */ public interface TransitAlertService { void setAlerts(Collection alerts); diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java index ce880058005..381799bd121 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java @@ -4,6 +4,14 @@ import java.util.Optional; import org.opentripplanner.framework.doc.DocumentedEnum; +/** + * This converts strings appearing in configuration files into enum values. + * The values appearing in config files are case-insensitive and can use either dashes + * or underscores indiscriminately. + * Dashes are replaced with underscores, and the string is converted to upper case. + * In practice, this serves to convert from kebab-case to SCREAMING_SNAKE_CASE (which is + * conventional for Java enum values), leaving the latter unchanged if it's used in the config file. + */ public class EnumMapper { @SuppressWarnings("unchecked") @@ -11,6 +19,9 @@ public static > Optional mapToEnum(String text, Class ty return (Optional) mapToEnum2(text, type); } + /** + * Maps an individual value from a config file into its corresponding enum value. + */ public static Optional> mapToEnum2(String text, Class> type) { if (text == null) { return Optional.empty(); diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index f1d90d0ec40..ab19f08e25b 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -42,6 +42,10 @@ import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.framework.FeedScopedId; +/** + * TODO clarify whether this is building a declarative representation of the parameter, or building + * a concrete key-value pair for a parameter in a config file being read at server startup, or both. + */ public class ParameterBuilder { private static final Object UNDEFINED = new Object(); diff --git a/src/main/java/org/opentripplanner/updater/alert/TransitAlertProvider.java b/src/main/java/org/opentripplanner/updater/alert/TransitAlertProvider.java index e7c3bbc5bf6..deae4557c3e 100644 --- a/src/main/java/org/opentripplanner/updater/alert/TransitAlertProvider.java +++ b/src/main/java/org/opentripplanner/updater/alert/TransitAlertProvider.java @@ -2,6 +2,13 @@ import org.opentripplanner.routing.services.TransitAlertService; +/** + * Interface for things that maintain their own individual index associating TransitAlerts with the + * transit entities they affect. In practice, these are always realtime updaters handling GTFS-RT + * Alerts or Siri SX messages. This interface appears to exist only to allow merging multiple such + * services together, which appears to be a workaround for not maintaining snapshots of a single + * instance-wide index. + */ public interface TransitAlertProvider { TransitAlertService getTransitAlertService(); } From 3b94c8f54793dbb84597a044144ab9ee28241ff3 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Wed, 24 Jan 2024 20:18:50 +0800 Subject: [PATCH 07/24] code formatting with mvn prettier:write --- .../ext/siri/SiriTripPatternCache.java | 19 ++++++++++--------- .../model/TimetableSnapshot.java | 5 ++++- .../updater/spi/GraphUpdater.java | 1 - .../updater/trip/TripPatternCache.java | 19 ++++++++++--------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 165feee7513..497aadf0c9a 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -99,15 +99,16 @@ public synchronized TripPattern getOrCreateTripPattern( // Create TripPattern if it doesn't exist yet if (tripPattern == null) { var id = tripPatternIdGenerator.generateUniqueTripPatternId(trip); - tripPattern = TripPattern - .of(id) - .withRoute(trip.getRoute()) - .withMode(trip.getMode()) - .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern) - .withCreatedByRealtimeUpdater(true) - .withOriginalTripPattern(originalTripPattern) - .build(); + tripPattern = + TripPattern + .of(id) + .withRoute(trip.getRoute()) + .withMode(trip.getMode()) + .withNetexSubmode(trip.getNetexSubMode()) + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); // TODO - SIRI: Add pattern to transitModel index? // Add pattern to cache diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 3c5beddb322..dfb1975d818 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -117,7 +117,10 @@ public class TimetableSnapshot { * through all these states in order, and cannot return to a previous state. */ private enum TimetableSnapshotState { - WRITABLE_CLEAN, WRITBLE_DIRTY, INDEXING, READ_ONLY + WRITABLE_CLEAN, + WRITBLE_DIRTY, + INDEXING, + READ_ONLY, } /** diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index 5c8490ae9bb..d8edc651e7e 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -18,7 +18,6 @@ * about concurrent reads and writes to the Graph. */ public interface GraphUpdater { - /** * After a GraphUpdater is instantiated, the GraphUpdaterManager that instantiated it will * immediately supply a callback via this method. The GraphUpdater will employ that callback diff --git a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java index c4f437dabde..f43bf5e3b99 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java +++ b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java @@ -60,15 +60,16 @@ public synchronized TripPattern getOrCreateTripPattern( // Generate unique code for trip pattern var id = generateUniqueTripPatternCode(trip); - tripPattern = TripPattern - .of(id) - .withRoute(route) - .withMode(trip.getMode()) - .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern) - .withCreatedByRealtimeUpdater(true) - .withOriginalTripPattern(originalTripPattern) - .build(); + tripPattern = + TripPattern + .of(id) + .withRoute(route) + .withMode(trip.getMode()) + .withNetexSubmode(trip.getNetexSubMode()) + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); // Add pattern to cache cache.put(stopPattern, tripPattern); From 873ceec0bd27bf15c3bf54e786118e013814b4c9 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 23 Feb 2024 00:03:19 +0800 Subject: [PATCH 08/24] add realtime updater concurrency diagram and md --- .../images/updater-threads-queues.excalidraw | 4643 +++++++++++++++++ .../updater/images/updater-threads-queues.svg | 21 + .../org/opentripplanner/updater/package.md | 26 + 3 files changed, 4690 insertions(+) create mode 100644 src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw create mode 100644 src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg create mode 100644 src/main/java/org/opentripplanner/updater/package.md diff --git a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw new file mode 100644 index 00000000000..274a194919e --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw @@ -0,0 +1,4643 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "arrow", + "version": 659, + "versionNonce": 648948858, + "isDeleted": false, + "id": "VM8X0qyPeoxkTBRdLIM5J", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1066.5000002891657, + "y": 804.5844640879644, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 131.56873415422308, + "seed": 734371294, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "AGWy-WRNKq1GRwlq27Uat", + "focus": 0.5550847433121556, + "gap": 3.5844640879644203 + }, + "endBinding": { + "elementId": "ia9PMtL1s9vwB3H7pqe7w", + "focus": -0.9999999981283776, + "gap": 4.3468017578125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 131.56873415422308 + ] + ] + }, + { + "type": "arrow", + "version": 920, + "versionNonce": 66351142, + "isDeleted": false, + "id": "3GtZ4aSs9ueb9GU2jLsdc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 401.3685043358803, + "y": 328, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1116.759994506836, + "height": 0, + "seed": 1607395522, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "E-mK9Dz0MlVGI0zCEjmgE", + "focus": 0.04, + "gap": 2.848500063419351 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1116.759994506836, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 611, + "versionNonce": 54677370, + "isDeleted": false, + "id": "ATmo4gWymhk2HAtTZvwr8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 403.54024888033973, + "y": 406.4884850323545, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1112.2184718299377, + "height": 5.511514967645496, + "seed": 356024450, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594332225, + "link": null, + "locked": false, + "startBinding": { + "elementId": "NxucM5UWiFJSHCUFadK_p", + "focus": 0.03803602904865689, + "gap": 8.840251932097544 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1112.2184718299377, + 5.511514967645496 + ] + ] + }, + { + "type": "arrow", + "version": 587, + "versionNonce": 159349606, + "isDeleted": false, + "id": "zb4NyAsYy13XFu293d0GB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 400.6507002377508, + "y": 603.6986805801255, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1112, + "height": 0, + "seed": 1629036610, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "D7-QRZxcX9A0a-2HK7Nex", + "focus": 0.41589444641003864, + "gap": 8.950703289508624 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1112, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 992, + "versionNonce": 1825887738, + "isDeleted": false, + "id": "jFldS1dvD1KlUNloJjncO", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 393.004754807949, + "y": 873.6986805801256, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1112, + "height": 0, + "seed": 1867499522, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "8q-Tc9B-UEz9icdriq9yS", + "focus": 0.25589444641004777, + "gap": 10.304757859706797 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1112, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 873, + "versionNonce": 403119782, + "isDeleted": false, + "id": "7LqY4x9DQ6sxq0rqNvLa6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 395.96104555368424, + "y": 961.6986805801255, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1112, + "height": 0, + "seed": 400259010, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "jb52v3pX5rahA8qitNheB", + "focus": 0.09589444641003865, + "gap": 9.261048605442056 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1112, + 0 + ] + ] + }, + { + "type": "text", + "version": 468, + "versionNonce": 645662394, + "isDeleted": false, + "id": "E-mK9Dz0MlVGI0zCEjmgE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 226.01998901367188, + "y": 315, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 172.50001525878906, + "height": 25, + "seed": 1746517598, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "3GtZ4aSs9ueb9GU2jLsdc", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Polling Updater 2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Polling Updater 2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 204, + "versionNonce": 1726513530, + "isDeleted": false, + "id": "ZVYycY9GjFTR8gQbd1wot", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 613, + "y": 138, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 573.0200805664062, + "height": 35, + "seed": 1131654366, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594379770, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Threads, Queues, and Buffers Over Time", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Threads, Queues, and Buffers Over Time", + "lineHeight": 1.25, + "baseline": 25 + }, + { + "type": "text", + "version": 271, + "versionNonce": 763305850, + "isDeleted": false, + "id": "D7-QRZxcX9A0a-2HK7Nex", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 244.53997802734375, + "y": 586, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 147.16001892089844, + "height": 25, + "seed": 1121505758, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "zb4NyAsYy13XFu293d0GB", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Graph Updater", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Graph Updater", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 432, + "versionNonce": 1159736614, + "isDeleted": false, + "id": "8q-Tc9B-UEz9icdriq9yS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 231, + "y": 858, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 151.6999969482422, + "height": 25, + "seed": 883374494, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "jFldS1dvD1KlUNloJjncO", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "HTTP Handler 1", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "HTTP Handler 1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 385, + "versionNonce": 1042358330, + "isDeleted": false, + "id": "jb52v3pX5rahA8qitNheB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 226.17999267578125, + "y": 948, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 160.52000427246094, + "height": 25, + "seed": 2076275522, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "7LqY4x9DQ6sxq0rqNvLa6", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "HTTP Handler 2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "HTTP Handler 2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 287, + "versionNonce": 1088763578, + "isDeleted": false, + "id": "NxucM5UWiFJSHCUFadK_p", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 212.25997924804688, + "y": 393, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 182.4400177001953, + "height": 25, + "seed": 2038655710, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "ATmo4gWymhk2HAtTZvwr8", + "type": "arrow" + } + ], + "updated": 1708594332225, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Streaming Updater", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Streaming Updater", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1029, + "versionNonce": 340068602, + "isDeleted": false, + "id": "pbgCp_v4XB-4ur4z6O3Bc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 398.26203282117854, + "y": 246.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1119.5800018310547, + "height": 0, + "seed": 1903169154, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "d9AAv6uLddhWu-tS7LU1d", + "focus": 0.04, + "gap": 1.5620358729363488 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1119.5800018310547, + 0 + ] + ] + }, + { + "type": "text", + "version": 507, + "versionNonce": 86481830, + "isDeleted": false, + "id": "d9AAv6uLddhWu-tS7LU1d", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 233.01998901367188, + "y": 233.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 163.6800079345703, + "height": 25, + "seed": 724495938, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "pbgCp_v4XB-4ur4z6O3Bc", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Polling Updater 1", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Polling Updater 1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 471, + "versionNonce": 2003687866, + "isDeleted": false, + "id": "2Sp_rAnCP5DVtZtanChb6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 466, + "y": 388.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 43, + "height": 35, + "seed": 1921756702, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "k97D4vwVRWmZjD3yCJXe5" + }, + { + "id": "DCT-vq3X8w4-9CK0i1WK1", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 407, + "versionNonce": 1648441062, + "isDeleted": false, + "id": "k97D4vwVRWmZjD3yCJXe5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 477.57999992370605, + "y": 393.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 19.84000015258789, + "height": 25, + "seed": 493958302, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rx", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2Sp_rAnCP5DVtZtanChb6", + "originalText": "rx", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 266, + "versionNonce": 1186894458, + "isDeleted": false, + "id": "R1aGd7ZcthGGm4ndwxhLB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 425, + "y": 226, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 221, + "height": 38, + "seed": 559128414, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "_hDI7ak2sTyZPUBS0b8w-" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 191, + "versionNonce": 2013620774, + "isDeleted": false, + "id": "_hDI7ak2sTyZPUBS0b8w-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 509.5599994659424, + "y": 232.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 51.880001068115234, + "height": 25, + "seed": 1824035934, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "fetch", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "R1aGd7ZcthGGm4ndwxhLB", + "originalText": "fetch", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 439, + "versionNonce": 83505978, + "isDeleted": false, + "id": "RnHvTnp5Odw8X2f_unjuu", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 649, + "y": 226, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 193.00000000000003, + "height": 38, + "seed": 867685982, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "3UyzMOlvG4IIHqPHRbztE" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 408, + "versionNonce": 90256742, + "isDeleted": false, + "id": "3UyzMOlvG4IIHqPHRbztE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 718.7000007629395, + "y": 232.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 53.599998474121094, + "height": 25, + "seed": 8050462, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "parse", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "RnHvTnp5Odw8X2f_unjuu", + "originalText": "parse", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 629, + "versionNonce": 1686354938, + "isDeleted": false, + "id": "rpCTi5Ye67KDSe26PwR0a", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 578.5, + "y": 307.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 85, + "height": 36, + "seed": 599844702, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "3Pt6BooWESwxB1gZIA-3v" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 639, + "versionNonce": 1671534758, + "isDeleted": false, + "id": "3Pt6BooWESwxB1gZIA-3v", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 595.0599994659424, + "y": 313, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 51.880001068115234, + "height": 25, + "seed": 1747471262, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "fetch", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "rpCTi5Ye67KDSe26PwR0a", + "originalText": "fetch", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 638, + "versionNonce": 1398680762, + "isDeleted": false, + "id": "p6aB7QEw7RUZGP-S1w8uW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 668.5, + "y": 306.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 70, + "height": 38, + "seed": 714680286, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "VaMUIZ6R7q0fVQIF_1H-m" + }, + { + "id": "fipdoX_2jWMMttHq0biwj", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 641, + "versionNonce": 980653030, + "isDeleted": false, + "id": "VaMUIZ6R7q0fVQIF_1H-m", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 676.7000007629395, + "y": 313, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 53.599998474121094, + "height": 25, + "seed": 1529117726, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "parse", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "p6aB7QEw7RUZGP-S1w8uW", + "originalText": "parse", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1131, + "versionNonce": 120358266, + "isDeleted": false, + "id": "fipdoX_2jWMMttHq0biwj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 739.5, + "y": 345.04285714285714, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 0, + "height": 143.1040273571428, + "seed": 1526565570, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "p6aB7QEw7RUZGP-S1w8uW", + "focus": -1.0285714285714285, + "gap": 1 + }, + "endBinding": { + "elementId": "uow6_ERpKpHt29RiN9MNZ", + "focus": -0.75, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 143.1040273571428 + ] + ] + }, + { + "type": "rectangle", + "version": 497, + "versionNonce": 870590246, + "isDeleted": false, + "id": "nRkSDPyke6S415ANJpqSO", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 696.5, + "y": 393.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 43, + "height": 35, + "seed": 1540799298, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "RHdQtWzei63bDNP8lbDoR" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 434, + "versionNonce": 154256954, + "isDeleted": false, + "id": "RHdQtWzei63bDNP8lbDoR", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 708.079999923706, + "y": 398.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 19.84000015258789, + "height": 25, + "seed": 362939138, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rx", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "nRkSDPyke6S415ANJpqSO", + "originalText": "rx", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 601, + "versionNonce": 811618918, + "isDeleted": false, + "id": "DYijQiOE-bzqq2gIyWP7y", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 916.25, + "y": 389.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 43, + "height": 35, + "seed": 1425947074, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "OZwQJ1czpEHUJf-5mS2sy" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 538, + "versionNonce": 1112600314, + "isDeleted": false, + "id": "OZwQJ1czpEHUJf-5mS2sy", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 927.829999923706, + "y": 394.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 19.84000015258789, + "height": 25, + "seed": 710259074, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rx", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "DYijQiOE-bzqq2gIyWP7y", + "originalText": "rx", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 759, + "versionNonce": 2058687910, + "isDeleted": false, + "id": "dRe5zdw_i7ke_ERGqgHJ-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1184, + "y": 309, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 85, + "height": 36, + "seed": 1594082142, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "F4Lr--RTHsm_N-Lrsm8Mu" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 769, + "versionNonce": 118292410, + "isDeleted": false, + "id": "F4Lr--RTHsm_N-Lrsm8Mu", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1200.5599994659424, + "y": 314.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 51.880001068115234, + "height": 25, + "seed": 1857005470, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "fetch", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "dRe5zdw_i7ke_ERGqgHJ-", + "originalText": "fetch", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 766, + "versionNonce": 2081386726, + "isDeleted": false, + "id": "vPZEUjjPYESqGvFv-Rp4W", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1274, + "y": 308, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 70, + "height": 38, + "seed": 534053854, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "v4M2EX6oGwv9mypkE3nf4" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 771, + "versionNonce": 596179066, + "isDeleted": false, + "id": "v4M2EX6oGwv9mypkE3nf4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1282.2000007629395, + "y": 314.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 53.599998474121094, + "height": 25, + "seed": 1954863134, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "parse", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vPZEUjjPYESqGvFv-Rp4W", + "originalText": "parse", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 174, + "versionNonce": 1878811686, + "isDeleted": false, + "id": "FLqrds_LtrcgMMNkcCu2q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 232, + "y": 489, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 156.74000549316406, + "height": 25, + "seed": 310320450, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Executor Queue", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Executor Queue", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 976, + "versionNonce": 1736956218, + "isDeleted": false, + "id": "DCT-vq3X8w4-9CK0i1WK1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 508.98472459870027, + "y": 424.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 0.23213825368219432, + "height": 61.98677459546377, + "seed": 2050105346, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2Sp_rAnCP5DVtZtanChb6", + "focus": -0.99304009380763, + "gap": 1 + }, + "endBinding": { + "elementId": "vqgYUA0JXn0AfAfEy6BH4", + "focus": -1.0290997398675656, + "gap": 2.6601099045361707 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.23213825368219432, + 61.98677459546377 + ] + ] + }, + { + "type": "rectangle", + "version": 528, + "versionNonce": 409530214, + "isDeleted": false, + "id": "Y3PoGw9NOC5IBQUKp3cR-", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 406, + "y": 489.64688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 103, + "height": 34, + "seed": 576806082, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 727, + "versionNonce": 845980154, + "isDeleted": false, + "id": "vqgYUA0JXn0AfAfEy6BH4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 510, + "y": 489.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 44, + "height": 35, + "seed": 703773406, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "DCT-vq3X8w4-9CK0i1WK1", + "type": "arrow" + }, + { + "id": "CH5dSO6dJ53vGEmqY2aWs", + "type": "arrow" + }, + { + "type": "text", + "id": "xP6C3cArVpheLzV5AFg_E" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 163, + "versionNonce": 1906447014, + "isDeleted": false, + "id": "xP6C3cArVpheLzV5AFg_E", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 529.289999961853, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 5.420000076293945, + "height": 25, + "seed": 226082462, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vqgYUA0JXn0AfAfEy6BH4", + "originalText": "1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 653, + "versionNonce": 2134528698, + "isDeleted": false, + "id": "zrqQOL7PgwaKzxPCDeS4N", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 556, + "y": 489.64688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 176.99999999999997, + "height": 34, + "seed": 1754666306, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "CH5dSO6dJ53vGEmqY2aWs", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 849, + "versionNonce": 1716833766, + "isDeleted": false, + "id": "uow6_ERpKpHt29RiN9MNZ", + "fillStyle": "cross-hatch", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 734, + "y": 489.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 44, + "height": 35, + "seed": 1164320002, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "sjFWKA8ES33JvxT0dVcvh" + }, + { + "id": "fipdoX_2jWMMttHq0biwj", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 163, + "versionNonce": 1888742266, + "isDeleted": false, + "id": "sjFWKA8ES33JvxT0dVcvh", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 748.8800001144409, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 14.239999771118164, + "height": 25, + "seed": 833466398, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "uow6_ERpKpHt29RiN9MNZ", + "originalText": "2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 394, + "versionNonce": 59646246, + "isDeleted": false, + "id": "OdVAxEmxdP0Y8q-094nMN", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 556, + "y": 584.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 221.00000000000003, + "height": 41, + "seed": 773076318, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "ZR6W2CO-B65WopEP2jrS1" + }, + { + "id": "CH5dSO6dJ53vGEmqY2aWs", + "type": "arrow" + }, + { + "id": "twNjmIr9f-l-aubZs4GnI", + "type": "arrow" + }, + { + "id": "5CrHigwwqK1VKc6Ca3gxy", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 283, + "versionNonce": 1555852346, + "isDeleted": false, + "id": "ZR6W2CO-B65WopEP2jrS1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 642.6499996185303, + "y": 592.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 1923846622, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "OdVAxEmxdP0Y8q-094nMN", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 549, + "versionNonce": 750455910, + "isDeleted": false, + "id": "CH5dSO6dJ53vGEmqY2aWs", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 555, + "y": 525.2500000000001, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0, + "height": 59.06447963800895, + "seed": 638457026, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "zrqQOL7PgwaKzxPCDeS4N", + "focus": 1.011299435028249, + "gap": 1.6031155000001718 + }, + "endBinding": { + "elementId": "OdVAxEmxdP0Y8q-094nMN", + "focus": -1.009049773755656, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 59.06447963800895 + ] + ] + }, + { + "type": "rectangle", + "version": 561, + "versionNonce": 1503057146, + "isDeleted": false, + "id": "hw9PKBQkD_MFDyEU0uuka", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 778.5, + "y": 584.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 233.99999999999997, + "height": 41, + "seed": 1056339102, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "OqBJjQ-07RyubDNHABeyS" + }, + { + "id": "vEyXTRJW7n_hrgPRIt7d3", + "type": "arrow" + }, + { + "id": "rYbr17RAyFgxzOKrfTzeq", + "type": "arrow" + }, + { + "id": "twNjmIr9f-l-aubZs4GnI", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 446, + "versionNonce": 1336446886, + "isDeleted": false, + "id": "OqBJjQ-07RyubDNHABeyS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 871.6499996185303, + "y": 592.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 934949086, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hw9PKBQkD_MFDyEU0uuka", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 835, + "versionNonce": 1218848186, + "isDeleted": false, + "id": "hZnDTRiQOaPQLk9O8COl6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 779.5, + "y": 489.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 65.99999999999997, + "height": 35, + "seed": 377455106, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "RG8D9DfwueruyUSdOLsr5" + }, + { + "id": "uxbykYOvdtL0HYuSmUU6U", + "type": "arrow" + }, + { + "id": "twNjmIr9f-l-aubZs4GnI", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 296, + "versionNonce": 411282150, + "isDeleted": false, + "id": "RG8D9DfwueruyUSdOLsr5", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 809.789999961853, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 5.420000076293945, + "height": 25, + "seed": 1643067138, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hZnDTRiQOaPQLk9O8COl6", + "originalText": "1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 768, + "versionNonce": 1661820538, + "isDeleted": false, + "id": "N9_cITJx4vKH4lSu_tt_A", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 958.5000001636438, + "y": 416.9672055477426, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 0, + "height": 71.07538481780904, + "seed": 1586569758, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "vpqws0a9Jh-wWPUB3jB4Z", + "focus": 1.0105613645758171, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 71.07538481780904 + ] + ] + }, + { + "type": "rectangle", + "version": 794, + "versionNonce": 635002406, + "isDeleted": false, + "id": "-A08osWH4z9kW6phuwwOP", + "fillStyle": "cross-hatch", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 959.5, + "y": 489.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#1971c2", + "width": 52.99999999999995, + "height": 35, + "seed": 338595202, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "vZnROuYPsK34LqIU31yf7" + }, + { + "id": "vEyXTRJW7n_hrgPRIt7d3", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 260, + "versionNonce": 1167032122, + "isDeleted": false, + "id": "vZnROuYPsK34LqIU31yf7", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 979.1900000572205, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 13.619999885559082, + "height": 25, + "seed": 264604994, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "3", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "-A08osWH4z9kW6phuwwOP", + "originalText": "3", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1147, + "versionNonce": 1016506726, + "isDeleted": false, + "id": "twNjmIr9f-l-aubZs4GnI", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 776.4389828921583, + "y": 524.25, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0, + "height": 63, + "seed": 787633410, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": { + "elementId": "hZnDTRiQOaPQLk9O8COl6", + "focus": 1.0927580941770225, + "gap": 3.061017107841735 + }, + "endBinding": { + "elementId": "hw9PKBQkD_MFDyEU0uuka", + "focus": -1.0176155308362542, + "gap": 2.061017107841735 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 63 + ] + ] + }, + { + "type": "arrow", + "version": 931, + "versionNonce": 2089781242, + "isDeleted": false, + "id": "uxbykYOvdtL0HYuSmUU6U", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 841.9999996309284, + "y": 238.23901038943114, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 1, + "height": 251.71830357452671, + "seed": 1488393374, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "vpqws0a9Jh-wWPUB3jB4Z", + "focus": -1.0695688639425673, + "gap": 4.000000369071586 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1, + 251.71830357452671 + ] + ] + }, + { + "type": "text", + "version": 217, + "versionNonce": 2080935078, + "isDeleted": false, + "id": "xMxb5iz16pT1UiTIBqPDG", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 168, + "y": 682, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 212.36000061035156, + "height": 25, + "seed": 1947891714, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Buffer Transit Data", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Buffer Transit Data", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 254, + "versionNonce": 269514938, + "isDeleted": false, + "id": "rUiVeXZmQgBmVGY4bUGiT", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 190, + "y": 769, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 185.24000549316406, + "height": 25, + "seed": 2073208962, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Live Transit Data", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Live Transit Data", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 938, + "versionNonce": 299632614, + "isDeleted": false, + "id": "vpqws0a9Jh-wWPUB3jB4Z", + "fillStyle": "cross-hatch", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 847, + "y": 488.8531159999999, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 110.91429700000009, + "height": 35.58753700000011, + "seed": 681382082, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "m7L49SlkIqM3UX3pIO0v1" + }, + { + "id": "N9_cITJx4vKH4lSu_tt_A", + "type": "arrow" + }, + { + "id": "uxbykYOvdtL0HYuSmUU6U", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 248, + "versionNonce": 263541114, + "isDeleted": false, + "id": "m7L49SlkIqM3UX3pIO0v1", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 895.3371486144409, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 14.239999771118164, + "height": 25, + "seed": 1912711298, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "vpqws0a9Jh-wWPUB3jB4Z", + "originalText": "2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 661, + "versionNonce": 1847466790, + "isDeleted": false, + "id": "YQXusl7FZ5mjVmkwS7nTt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1014, + "y": 584.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 76.99999999999999, + "height": 41, + "seed": 991711234, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "SbUQ0qpP3hURdF-JpkzcT" + }, + { + "id": "8VmQ5RzV8MqKRX9MrvJ3c", + "type": "arrow" + }, + { + "id": "vEyXTRJW7n_hrgPRIt7d3", + "type": "arrow" + }, + { + "id": "j_qtXumsQfWi0I_VI5PkL", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 547, + "versionNonce": 515969594, + "isDeleted": false, + "id": "SbUQ0qpP3hURdF-JpkzcT", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1028.6499996185303, + "y": 592.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 1222049730, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "YQXusl7FZ5mjVmkwS7nTt", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 697, + "versionNonce": 2000406118, + "isDeleted": false, + "id": "r6US9SjrUjcRP26-bNJLb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1093.5, + "y": 584.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 76.99999999999999, + "height": 41, + "seed": 1650600030, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "5HrAXQLJ-WCJb8_C8a4uo" + }, + { + "id": "s1NGF8K9oUvWEXmBzmufP", + "type": "arrow" + }, + { + "id": "8VmQ5RzV8MqKRX9MrvJ3c", + "type": "arrow" + }, + { + "id": "vSrc221actOuf9WusrgZt", + "type": "arrow" + } + ], + "updated": 1708594325688, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 583, + "versionNonce": 1489658, + "isDeleted": false, + "id": "5HrAXQLJ-WCJb8_C8a4uo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1108.1499996185303, + "y": 592.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 586771614, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "r6US9SjrUjcRP26-bNJLb", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 708, + "versionNonce": 1040320934, + "isDeleted": false, + "id": "ye55wrbQ2ruSrJJx9tXx3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1173.5, + "y": 584.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 76.99999999999999, + "height": 41, + "seed": 1476901634, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "fbE1QhbN4s8LgICVlAepH" + }, + { + "id": "S736oegZ-NP_Jn8T2r-5l", + "type": "arrow" + }, + { + "id": "s1NGF8K9oUvWEXmBzmufP", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 595, + "versionNonce": 574180282, + "isDeleted": false, + "id": "fbE1QhbN4s8LgICVlAepH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1188.1499996185303, + "y": 592.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 723323586, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ye55wrbQ2ruSrJJx9tXx3", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 1014, + "versionNonce": 832431334, + "isDeleted": false, + "id": "oLUKp1kMgqNdxEPEkb372", + "fillStyle": "cross-hatch", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1013.5428514999999, + "y": 488.8531159999999, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 76.91429700000009, + "height": 35.58753700000011, + "seed": 129805314, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "TCgAzrDxcbwYL9YI6wNBf" + }, + { + "id": "8VmQ5RzV8MqKRX9MrvJ3c", + "type": "arrow" + }, + { + "id": "vEyXTRJW7n_hrgPRIt7d3", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 323, + "versionNonce": 726385786, + "isDeleted": false, + "id": "TCgAzrDxcbwYL9YI6wNBf", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1044.880000114441, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 14.239999771118164, + "height": 25, + "seed": 1493949378, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "oLUKp1kMgqNdxEPEkb372", + "originalText": "2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 898, + "versionNonce": 1402755110, + "isDeleted": false, + "id": "y5kRBL1VIbCsvey9iszvB", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1091.5, + "y": 488.7499999999999, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 83.01295899999985, + "height": 35.79376900000011, + "seed": 1815920194, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "qGVL3pVMGuja1QEF58BVa" + }, + { + "id": "s1NGF8K9oUvWEXmBzmufP", + "type": "arrow" + }, + { + "id": "8VmQ5RzV8MqKRX9MrvJ3c", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 360, + "versionNonce": 85009722, + "isDeleted": false, + "id": "qGVL3pVMGuja1QEF58BVa", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1130.2964794618529, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 5.420000076293945, + "height": 25, + "seed": 1347397122, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "y5kRBL1VIbCsvey9iszvB", + "originalText": "1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 719, + "versionNonce": 1810522982, + "isDeleted": false, + "id": "DwJbh4L0U48rxYKAqNfpB", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1174.512959, + "y": 488.6468844999998, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 168.48704100000006, + "height": 36.000000000000114, + "seed": 1675830494, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "s1NGF8K9oUvWEXmBzmufP", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 877, + "versionNonce": 1760325114, + "isDeleted": false, + "id": "OIqNLYrG2vKqjdPIBjJEQ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1343, + "y": 489.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 32.00000000000001, + "height": 35, + "seed": 1098016542, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "2NpZ0UtU8si40TXPeKtsD" + }, + { + "id": "VJAv_AA4wh9HPi5ElZQsZ", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 322, + "versionNonce": 1020593830, + "isDeleted": false, + "id": "2NpZ0UtU8si40TXPeKtsD", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1356.289999961853, + "y": 494.14688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "#4dabf7", + "width": 5.420000076293945, + "height": 25, + "seed": 454284126, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "OIqNLYrG2vKqjdPIBjJEQ", + "originalText": "1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 893, + "versionNonce": 1809882810, + "isDeleted": false, + "id": "pAR3k0emFpck8rYDIDQXw", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1375.5, + "y": 488.64688449999994, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 118, + "height": 36, + "seed": 1685386718, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "iXoy2RGlWeoP2O-W4w0Ep", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 1002, + "versionNonce": 923353574, + "isDeleted": false, + "id": "vEyXTRJW7n_hrgPRIt7d3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1012.5000000738087, + "y": 529.0097662199997, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0, + "height": 54.69153252059971, + "seed": 33441502, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "oLUKp1kMgqNdxEPEkb372", + "focus": 1.0271172322147408, + "gap": 4.5691132199997355 + }, + "endBinding": { + "elementId": "YQXusl7FZ5mjVmkwS7nTt", + "focus": -1.0389610370439306, + "gap": 1.4999999261913217 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 54.69153252059971 + ] + ] + }, + { + "type": "arrow", + "version": 1003, + "versionNonce": 177256314, + "isDeleted": false, + "id": "8VmQ5RzV8MqKRX9MrvJ3c", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1093.1291821802736, + "y": 530.78926145086, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0, + "height": 52.78203292055082, + "seed": 1506154910, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "y5kRBL1VIbCsvey9iszvB", + "focus": 0.9607487264663421, + "gap": 6.245492450859956 + }, + "endBinding": { + "elementId": "r6US9SjrUjcRP26-bNJLb", + "focus": -1.009631631681206, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 52.78203292055082 + ] + ] + }, + { + "type": "arrow", + "version": 1030, + "versionNonce": 764679462, + "isDeleted": false, + "id": "s1NGF8K9oUvWEXmBzmufP", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1174.755011493805, + "y": 529.4119814508599, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0, + "height": 52.822363078412764, + "seed": 36753182, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "DwJbh4L0U48rxYKAqNfpB", + "focus": 0.9971267523915376, + "gap": 4.76509695085997 + }, + "endBinding": { + "elementId": "ye55wrbQ2ruSrJJx9tXx3", + "focus": -0.967402298862211, + "gap": 2.2656554707273244 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 52.822363078412764 + ] + ] + }, + { + "type": "arrow", + "version": 686, + "versionNonce": 893368378, + "isDeleted": false, + "id": "VJAv_AA4wh9HPi5ElZQsZ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1342.9999996309284, + "y": 326.00101338943114, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 0, + "height": 157.39688450000017, + "seed": 1157064926, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "OIqNLYrG2vKqjdPIBjJEQ", + "focus": -1.0000000230669739, + "gap": 5.748986610568636 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 157.39688450000017 + ] + ] + }, + { + "type": "arrow", + "version": 1095, + "versionNonce": 165838950, + "isDeleted": false, + "id": "iXoy2RGlWeoP2O-W4w0Ep", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1374.1675279436915, + "y": 525.0534013985348, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 0.18550849620919507, + "height": 53.44659860146521, + "seed": 1606301918, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "pAR3k0emFpck8rYDIDQXw", + "focus": 1.0225842721408214, + "gap": 1.332472056308461 + }, + "endBinding": { + "elementId": "FG0xyEC6bjrVvfXXFkhGr", + "focus": -0.9752121769956111, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.18550849620919507, + 53.44659860146521 + ] + ] + }, + { + "type": "rectangle", + "version": 829, + "versionNonce": 1540399354, + "isDeleted": false, + "id": "FG0xyEC6bjrVvfXXFkhGr", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1373.5, + "y": 579.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 80.99999999999997, + "height": 43, + "seed": 1211220994, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "7UOhjpWFIqXzoeKvZrdUr" + }, + { + "id": "iXoy2RGlWeoP2O-W4w0Ep", + "type": "arrow" + }, + { + "id": "-QpmtHvCIPviWT2XJZRJV", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 719, + "versionNonce": 2143388582, + "isDeleted": false, + "id": "7UOhjpWFIqXzoeKvZrdUr", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1390.1499996185303, + "y": 588.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 47.70000076293945, + "height": 25, + "seed": 2065560514, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "apply", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "FG0xyEC6bjrVvfXXFkhGr", + "originalText": "apply", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 329, + "versionNonce": 2043048378, + "isDeleted": false, + "id": "20h5C81H-CYI27Tpd74ou", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 402, + "y": 761, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 367.99999999999994, + "height": 42, + "seed": 97377310, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "yz-IQ63iT5cK2GSyekOdh" + }, + { + "id": "F4a-HNfDUp8O41pcv3trV", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 255, + "versionNonce": 1096687334, + "isDeleted": false, + "id": "yz-IQ63iT5cK2GSyekOdh", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 579.4400000572205, + "y": 769.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 13.119999885559082, + "height": 25, + "seed": 2077060638, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "A", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "20h5C81H-CYI27Tpd74ou", + "originalText": "A", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 370, + "versionNonce": 574892666, + "isDeleted": false, + "id": "IqMFPR6D4iUuFlmFg2h3z", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 402, + "y": 676, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 367.99999999999994, + "height": 42, + "seed": 445551618, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "G4Er45oJTuBlpebe08edC" + }, + { + "id": "5CrHigwwqK1VKc6Ca3gxy", + "type": "arrow" + }, + { + "id": "BYY2sqc6N-uTx5iTOoG2R", + "type": "arrow" + }, + { + "id": "dtokkLQC1XKcHX81-QdOg", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 298, + "versionNonce": 860181030, + "isDeleted": false, + "id": "G4Er45oJTuBlpebe08edC", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 578.7300000190735, + "y": 684.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 14.539999961853027, + "height": 25, + "seed": 157336514, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "B", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "IqMFPR6D4iUuFlmFg2h3z", + "originalText": "B", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 454, + "versionNonce": 202165050, + "isDeleted": false, + "id": "5CrHigwwqK1VKc6Ca3gxy", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 668, + "y": 630, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 43, + "seed": 1315250050, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "OdVAxEmxdP0Y8q-094nMN", + "focus": -0.013574660633484162, + "gap": 4.5 + }, + "endBinding": { + "elementId": "IqMFPR6D4iUuFlmFg2h3z", + "focus": 0.44565217391304357, + "gap": 3 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 43 + ] + ] + }, + { + "type": "rectangle", + "version": 458, + "versionNonce": 543432038, + "isDeleted": false, + "id": "VNoc_jPVCooBwfSqYSdhh", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 770, + "y": 759, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 241.99999999999994, + "height": 42, + "seed": 2022920862, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "49TJbaW8lSUjt9MraulEE" + }, + { + "id": "RP9Ow_xaBH5G-VFYLVlAX", + "type": "arrow" + }, + { + "id": "dtokkLQC1XKcHX81-QdOg", + "type": "arrow" + }, + { + "id": "FhwMVCyPxe3HDRW1nTF5C", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 384, + "versionNonce": 495145978, + "isDeleted": false, + "id": "49TJbaW8lSUjt9MraulEE", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 883.7300000190735, + "y": 767.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 14.539999961853027, + "height": 25, + "seed": 638275294, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "B", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "VNoc_jPVCooBwfSqYSdhh", + "originalText": "B", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 564, + "versionNonce": 1845985446, + "isDeleted": false, + "id": "AGWy-WRNKq1GRwlq27Uat", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1014, + "y": 756, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 235.99999999999994, + "height": 45, + "seed": 1481685762, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "ENTILzjLGHiVQxm21FA85" + }, + { + "id": "VM8X0qyPeoxkTBRdLIM5J", + "type": "arrow" + }, + { + "id": "nXrTlzpTYojAwNzux2iX6", + "type": "arrow" + }, + { + "id": "5rqqd0bhXqDAnyrpwoGre", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 489, + "versionNonce": 2024457402, + "isDeleted": false, + "id": "ENTILzjLGHiVQxm21FA85", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1125.5599999427795, + "y": 766, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 12.880000114440918, + "height": 25, + "seed": 977668802, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "C", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "AGWy-WRNKq1GRwlq27Uat", + "originalText": "C", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 508, + "versionNonce": 1339849702, + "isDeleted": false, + "id": "DRdIzIA5aSRPeNpP2pex-", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 770, + "y": 676, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 241.99999999999994, + "height": 42, + "seed": 1884858078, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "ETsUVzbEoDEH6Wh5dDIE_" + }, + { + "id": "rYbr17RAyFgxzOKrfTzeq", + "type": "arrow" + }, + { + "id": "dtokkLQC1XKcHX81-QdOg", + "type": "arrow" + }, + { + "id": "FhwMVCyPxe3HDRW1nTF5C", + "type": "arrow" + }, + { + "id": "nXrTlzpTYojAwNzux2iX6", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 428, + "versionNonce": 119938426, + "isDeleted": false, + "id": "ETsUVzbEoDEH6Wh5dDIE_", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 884.5599999427795, + "y": 684.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 12.880000114440918, + "height": 25, + "seed": 1520179998, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "C", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "DRdIzIA5aSRPeNpP2pex-", + "originalText": "C", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 643, + "versionNonce": 104357434, + "isDeleted": false, + "id": "hWRH46gwh1PqF21q9C4zp", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1013.073454, + "y": 674.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 235.99999999999994, + "height": 45, + "seed": 1774178334, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "0N99yapinSp1_mDnKRh6N" + }, + { + "id": "S736oegZ-NP_Jn8T2r-5l", + "type": "arrow" + }, + { + "id": "j_qtXumsQfWi0I_VI5PkL", + "type": "arrow" + }, + { + "id": "vSrc221actOuf9WusrgZt", + "type": "arrow" + }, + { + "id": "5rqqd0bhXqDAnyrpwoGre", + "type": "arrow" + }, + { + "id": "qxLbNSc9nH3MjXNRGDNpH", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 568, + "versionNonce": 1588542054, + "isDeleted": false, + "id": "0N99yapinSp1_mDnKRh6N", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1123.273453809265, + "y": 684.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 15.600000381469727, + "height": 25, + "seed": 130817118, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "D", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "hWRH46gwh1PqF21q9C4zp", + "originalText": "D", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 506, + "versionNonce": 1323761062, + "isDeleted": false, + "id": "rYbr17RAyFgxzOKrfTzeq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 893.9011232891656, + "y": 628.5371760879644, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 43, + "seed": 933729730, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "hw9PKBQkD_MFDyEU0uuka", + "focus": 0.013665612913114198, + "gap": 3.03717608796444 + }, + "endBinding": { + "elementId": "DRdIzIA5aSRPeNpP2pex-", + "focus": 0.023976225530294545, + "gap": 4.46282391203556 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 43 + ] + ] + }, + { + "type": "arrow", + "version": 630, + "versionNonce": 894277562, + "isDeleted": false, + "id": "j_qtXumsQfWi0I_VI5PkL", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1059.202579342441, + "y": 629.3859425029369, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 43, + "seed": 1000093698, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "YQXusl7FZ5mjVmkwS7nTt", + "focus": -0.17409296993353024, + "gap": 3.8859425029369277 + }, + "endBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": -0.6090752089623642, + "gap": 2.1140574970630723 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 43 + ] + ] + }, + { + "type": "arrow", + "version": 612, + "versionNonce": 1103386854, + "isDeleted": false, + "id": "vSrc221actOuf9WusrgZt", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1135.073454342441, + "y": 626.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 42.99999950293693, + "seed": 236078722, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "r6US9SjrUjcRP26-bNJLb", + "focus": -0.07982998292054501, + "gap": 1 + }, + "endBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": 0.033898307986789215, + "gap": 5.00000049706307 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 42.99999950293693 + ] + ] + }, + { + "type": "arrow", + "version": 639, + "versionNonce": 1844875386, + "isDeleted": false, + "id": "S736oegZ-NP_Jn8T2r-5l", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1210.664620342441, + "y": 630.580001502937, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 42.919998497063034, + "seed": 1800030402, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ye55wrbQ2ruSrJJx9tXx3", + "focus": 0.03468518591062566, + "gap": 5.080001502936966 + }, + "endBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": 0.6745014096817041, + "gap": 1 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 42.919998497063034 + ] + ] + }, + { + "type": "rectangle", + "version": 246, + "versionNonce": 2127842342, + "isDeleted": false, + "id": "tCFT7TaS4GWxUTPH3e9dM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 660, + "y": 853, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 469, + "height": 45, + "seed": 751476638, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "pM0euEcA3O04vH8cF6euv" + }, + { + "id": "F4a-HNfDUp8O41pcv3trV", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 146, + "versionNonce": 1082290490, + "isDeleted": false, + "id": "pM0euEcA3O04vH8cF6euv", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 834.689998626709, + "y": 863, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 119.62000274658203, + "height": 25, + "seed": 1366808386, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "routing on A", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tCFT7TaS4GWxUTPH3e9dM", + "originalText": "routing on A", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 494, + "versionNonce": 1409537894, + "isDeleted": false, + "id": "ia9PMtL1s9vwB3H7pqe7w", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1066.5, + "y": 940.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 309, + "height": 45, + "seed": 1851997342, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "vniZK_pa9hyb8VzJ0L1hw" + }, + { + "id": "VM8X0qyPeoxkTBRdLIM5J", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 393, + "versionNonce": 1079261690, + "isDeleted": false, + "id": "vniZK_pa9hyb8VzJ0L1hw", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1161.310001373291, + "y": 950.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 119.37999725341797, + "height": 25, + "seed": 1850603742, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "routing on C", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ia9PMtL1s9vwB3H7pqe7w", + "originalText": "routing on C", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 478, + "versionNonce": 1235221158, + "isDeleted": false, + "id": "F4a-HNfDUp8O41pcv3trV", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 664.5000002891657, + "y": 805.4365820879644, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 43.71661615422306, + "seed": 1694420098, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "20h5C81H-CYI27Tpd74ou", + "focus": -0.42663043635416126, + "gap": 2.436582087964439 + }, + "endBinding": { + "elementId": "tCFT7TaS4GWxUTPH3e9dM", + "focus": -0.9808102333084621, + "gap": 3.8468017578125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 43.71661615422306 + ] + ] + }, + { + "type": "rectangle", + "version": 555, + "versionNonce": 477031098, + "isDeleted": false, + "id": "9R0Z6nTrFuTnbu8bnCl12", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1250.9169381654642, + "y": 757, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 241.99999999999994, + "height": 44, + "seed": 1289608734, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "4sB5qUMBsDqFPfGtsMHz0" + }, + { + "id": "qxLbNSc9nH3MjXNRGDNpH", + "type": "arrow" + }, + { + "id": "TKQL22FgfD_EaFnrCOSUr", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 481, + "versionNonce": 1448457702, + "isDeleted": false, + "id": "4sB5qUMBsDqFPfGtsMHz0", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1364.1169379747294, + "y": 766.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 15.600000381469727, + "height": 25, + "seed": 939027038, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "D", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9R0Z6nTrFuTnbu8bnCl12", + "originalText": "D", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 599, + "versionNonce": 729121658, + "isDeleted": false, + "id": "kX2Vp26fOxXlTY3cVukwK", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1250.9169381654642, + "y": 676, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 241.99999999999994, + "height": 42, + "seed": 813133470, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "-iIGa2IVrKzgz65Cw-4Tx" + }, + { + "id": "-QpmtHvCIPviWT2XJZRJV", + "type": "arrow" + }, + { + "id": "TKQL22FgfD_EaFnrCOSUr", + "type": "arrow" + } + ], + "updated": 1708594325689, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 523, + "versionNonce": 322046246, + "isDeleted": false, + "id": "-iIGa2IVrKzgz65Cw-4Tx", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1365.176938394346, + "y": 684.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 13.479999542236328, + "height": 25, + "seed": 51112670, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "E", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "kX2Vp26fOxXlTY3cVukwK", + "originalText": "E", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 672, + "versionNonce": 1872884838, + "isDeleted": false, + "id": "-QpmtHvCIPviWT2XJZRJV", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1413.9999998420708, + "y": 627.7803268401005, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 42.919998497063034, + "seed": 191001374, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "FG0xyEC6bjrVvfXXFkhGr", + "focus": 3.899486604675561e-9, + "gap": 5.2803268401005425 + }, + "endBinding": { + "elementId": "kX2Vp26fOxXlTY3cVukwK", + "focus": 0.3477938981537734, + "gap": 5.2996746628364235 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 42.919998497063034 + ] + ] + }, + { + "type": "arrow", + "version": 895, + "versionNonce": 1702000550, + "isDeleted": false, + "id": "dtokkLQC1XKcHX81-QdOg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 752.1482077789306, + "y": 725.5228344071656, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 28.727165592834353, + "height": 28.727165592834353, + "seed": 1768552294, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "IqMFPR6D4iUuFlmFg2h3z", + "focus": -0.6713432847403166, + "gap": 7.5228344071656466 + }, + "endBinding": { + "elementId": "VNoc_jPVCooBwfSqYSdhh", + "focus": -0.5941875114664444, + "gap": 4.75 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 28.727165592834353, + 28.727165592834353 + ] + ] + }, + { + "type": "arrow", + "version": 872, + "versionNonce": 986627514, + "isDeleted": false, + "id": "FhwMVCyPxe3HDRW1nTF5C", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 789.6236215285376, + "y": 753.6359425029369, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 28.59168158024795, + "height": 28.591681580247723, + "seed": 148629990, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "VNoc_jPVCooBwfSqYSdhh", + "focus": -0.8995805349896169, + "gap": 5.364057497063072 + }, + "endBinding": { + "elementId": "DRdIzIA5aSRPeNpP2pex-", + "focus": 0.31507349273609164, + "gap": 7.044260922689205 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 28.59168158024795, + -28.591681580247723 + ] + ] + }, + { + "type": "arrow", + "version": 995, + "versionNonce": 751866598, + "isDeleted": false, + "id": "nXrTlzpTYojAwNzux2iX6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 993.0599579806902, + "y": 723.795967694437, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 28.727165592834353, + "height": 28.727165592834353, + "seed": 1466845030, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "DRdIzIA5aSRPeNpP2pex-", + "focus": -0.5300281006074176, + "gap": 5.795967694436968 + }, + "endBinding": { + "elementId": "AGWy-WRNKq1GRwlq27Uat", + "focus": -0.5995445531227526, + "gap": 3.476866712728679 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 28.727165592834353, + 28.727165592834353 + ] + ] + }, + { + "type": "arrow", + "version": 944, + "versionNonce": 878900858, + "isDeleted": false, + "id": "5rqqd0bhXqDAnyrpwoGre", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1030.5353717302974, + "y": 751.9090757902081, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 28.59168158024795, + "height": 28.591681580247723, + "seed": 1446800038, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "AGWy-WRNKq1GRwlq27Uat", + "focus": -0.9114274197828796, + "gap": 4.090924209791865 + }, + "endBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": 0.32476161195369313, + "gap": 3.817394209960412 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 28.59168158024795, + -28.591681580247723 + ] + ] + }, + { + "type": "arrow", + "version": 1000, + "versionNonce": 1883850278, + "isDeleted": false, + "id": "qxLbNSc9nH3MjXNRGDNpH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dotted", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1230.1984920751363, + "y": 723.795967694437, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 26.829463375617934, + "height": 26.727165592834353, + "seed": 149713658, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": -0.5137527808081439, + "gap": 4.295967694436968 + }, + "endBinding": { + "elementId": "9R0Z6nTrFuTnbu8bnCl12", + "focus": -0.6031631617717584, + "gap": 6.476866712728679 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 26.829463375617934, + 26.727165592834353 + ] + ] + }, + { + "type": "arrow", + "version": 961, + "versionNonce": 141694778, + "isDeleted": false, + "id": "TKQL22FgfD_EaFnrCOSUr", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1269.8469813268543, + "y": 751.9090757902081, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 27.330342883103867, + "height": 26.591681580247723, + "seed": 2006451130, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708594325689, + "link": null, + "locked": false, + "startBinding": { + "elementId": "9R0Z6nTrFuTnbu8bnCl12", + "focus": -0.904619473082073, + "gap": 5.090924209791865 + }, + "endBinding": { + "elementId": "kX2Vp26fOxXlTY3cVukwK", + "focus": 0.32006279982418195, + "gap": 7.317394209960412 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 27.330342883103867, + -26.591681580247723 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg new file mode 100644 index 00000000000..36f9fc8ddfe --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg @@ -0,0 +1,21 @@ + + + + + + + + Polling Updater 2Threads, Queues, and Buffers Over TimeGraph UpdaterHTTP Handler 1HTTP Handler 2Streaming UpdaterPolling Updater 1rxfetchparsefetchparserxrxfetchparseExecutor Queue12applyapply13Buffer Transit DataLive Transit Data2applyapplyapply211applyABBCCDrouting on Arouting on CDE \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md new file mode 100644 index 00000000000..26120cfea9b --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -0,0 +1,26 @@ +# Realtime Updaters + +## Realtime Concurrency Overview + +The following approach to realtime concurrency was devised around 2013 when OTP first started consuming realtime data that affected routing results rather than just displaying messages. At first, the whole realtime system followed this approach. Some aspects of this system were maintained in subsequent work over the years, but because the details and rationale were not fully documented, misinterpretations and subtle inconsistencies were introduced. + +On 11 January 2024 a team of OTP developers reviewed this realtime concurrency approach together. The conclusion was that this approach remains sound, and that any consistency problems were not due to the approach itself, but rather due to its partial or erroneous implementation in realtime updater classes. Therefore, we decided to continue applying this approach in any new work on the realtime subsystem, at least until we encounter some situation that does not fit within this model. All existing realtime code that is not consistent with this approach should progressively be brought in line with it. + +The following is a sequence diagram showing how threads are intended to communicate. Unlike some common forms of sequence diagrams, time is on the horizontal axis here. Each horizontal line represents either a thread of execution (handling incoming realtime messages or routing requests) or a queue or buffer data structure. Dotted lines represent object references being handed off, and solid lines represent data being copied. + +![Architecture diagram](images/updater-threads-queues.svg) + +At the top of the diagram are the GraphUpdater implementations. These fall broadly into two categories: polling updaters and streaming updaters. Polling updaters periodically send a request to server (often just a simple HTTP server) which returns a file containing the latest version of the updates. Streaming updaters are generally built around libraries implementing message-oriented protocols such as AMQP or WebSockets, which fire a callback each time a new message is received. Polling updaters tend to return a full dataset describing the entire system state on each polling operation, while streaming updaters tend to receive incremental messages targeting individual transit trips. As such, polling updaters execute relatively infrequently (perhaps every minute or two) and process large responses, while streaming updaters execute very frequently (often many times per second) and operate on small messages in short bursts. Polling updaters are simpler in many ways and make use of common HTTP server components, but they introduce significant latency and redundant communication. Streaming updaters require more purpose-built or custom-configured components including message brokers, but bandwidth consumption and latency are lower, allowing routing results to reflect vehicle delays and positions immediately after they're reported. + +The GraphUpdaterManager coordinates all these updaters, and each runs freely in its own thread, receiving, deserializing, and validating data on its own schedule. Importantly, the GraphUpdaters are _not allowed to directly modify the transit data (Graph)_. Instead, they submit instances of GraphWriterRunnable which are queued up using the WriteToGraphCallback interface. These instances are essentially deferred code snippets that _are allowed_ to write to the Graph, but in a very controlled way. In short, there is exactly one thread that is allowed to make changes to the transit data, and those changes are queued up and executed in sequence, one at a time. + +As mentioned above, these GraphWriterRunnable instances must write to the transit data model in a very controlled way, following specific rules. They operate on a buffer containing a shallow copy of the whole transit data structure, and apply a copy-on-write strategy to avoid corrupting existing objects that may be visible to other parts of the system. When an instance is copied for writing, any references to it in parent objects must also be updated. Therefore, writes cause cascading copy operations, and all instances in the object tree back up to the root of the transit data structure must also be copied. As an optimization, if a GraphWriterRunnable is able to determine that the protective copy has already been made in this buffer (the part of the structure it needs to modify is somehow marked as being "dirty") it does not need to make another copy. If the update involves reading the existing data structure before making a change, those reads should be performed within the same contiguous chunk of deferred logic that performs the corresponding write, ensuring that there are no data races between write operations. + +This writable buffer of transit data is periodically made immutable and swapped into the role of a live snapshot, which is ready to be handed off to any incoming routing requests. Each time an immutable snapshot is created, a new writable buffer is created by making a shallow copy of the root instance in the transit data aggreagate. This functions like a double-buffering system, except that any number of snapshots can exist at once, and large subsets of the data can be shared across snapshots. As older snapshots (and their component parts) fall out of use, they are dereferenced and become eligible for garbage collection. Although the buffer swap could in principle occur after every write operation, it can incur significant copying and indexing overhead. When incremental message-oriented updaters are present this overhead would be incurred more often than necesary. Snapshots can be throttled to occur at most every few seconds, thereby reducing the total overhead at no perceptible cost to realtime visibility latency. + +This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where 1) writing operations are simple to reason about and cannot conflict because only one write happens at a time; 2) multiple read operations (including routing requests) can occur concurrently; 3) read operations do not need to pause while writes are happening; 4) read operations see only fully completed write operations, never partial writes; and 5) each read operation sees a consistent, unchanging view of the transit data. + +Importantly, no locking is necessary, though some form of synchronization is applied during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. (While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off.) + +Arguably the process of creating an immutable live snapshot (and a corresponding new writable buffer) should be handled by a GraphWriterRunnable on the single graph updater thread. This would serve to defer any queued modifications until the new buffer is in place, without introducing any further locking mechanisms. + From e3b5326e6174b63d8938194fd61229890cd5587c Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 23 Feb 2024 00:24:10 +0800 Subject: [PATCH 09/24] update realtime concurrency diagram add updater reads from buffer add garbage collection --- .../images/updater-threads-queues.excalidraw | 1826 ++++++++++------- .../updater/images/updater-threads-queues.svg | 4 +- 2 files changed, 1099 insertions(+), 731 deletions(-) diff --git a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw index 274a194919e..beafb30c385 100644 --- a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw +++ b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.excalidraw @@ -5,13 +5,61 @@ "elements": [ { "type": "arrow", - "version": 659, - "versionNonce": 648948858, + "version": 772, + "versionNonce": 1584975214, + "isDeleted": false, + "id": "UiGgSvKQYLhHvq9f8X1v6", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1126.5145762839165, + "y": 900.8500688172288, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 2.2737367544323206e-13, + "height": 134.3999311827714, + "seed": 191626350, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708618510776, + "link": null, + "locked": false, + "startBinding": { + "elementId": "tCFT7TaS4GWxUTPH3e9dM", + "focus": -0.989401178183013, + "gap": 2.8500688172288164 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -2.2737367544323206e-13, + 134.3999311827714 + ] + ] + }, + { + "type": "arrow", + "version": 673, + "versionNonce": 1878291762, "isDeleted": false, "id": "VM8X0qyPeoxkTBRdLIM5J", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -28,7 +76,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -57,8 +105,8 @@ }, { "type": "arrow", - "version": 920, - "versionNonce": 66351142, + "version": 1012, + "versionNonce": 262024942, "isDeleted": false, "id": "3GtZ4aSs9ueb9GU2jLsdc", "fillStyle": "solid", @@ -80,7 +128,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -105,8 +153,8 @@ }, { "type": "arrow", - "version": 611, - "versionNonce": 54677370, + "version": 718, + "versionNonce": 1674772210, "isDeleted": false, "id": "ATmo4gWymhk2HAtTZvwr8", "fillStyle": "solid", @@ -128,7 +176,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594332225, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -153,8 +201,8 @@ }, { "type": "arrow", - "version": 587, - "versionNonce": 159349606, + "version": 729, + "versionNonce": 1093057326, "isDeleted": false, "id": "zb4NyAsYy13XFu293d0GB", "fillStyle": "solid", @@ -163,11 +211,11 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 400.6507002377508, + "x": 390.4210421860963, "y": 603.6986805801255, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 1112, + "width": 1122.2296580516545, "height": 0, "seed": 1629036610, "groupIds": [], @@ -176,13 +224,13 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618565655, "link": null, "locked": false, "startBinding": { "elementId": "D7-QRZxcX9A0a-2HK7Nex", - "focus": 0.41589444641003864, - "gap": 8.950703289508624 + "focus": 0.10423309392395506, + "gap": 13.586142283752565 }, "endBinding": null, "lastCommittedPoint": null, @@ -194,15 +242,15 @@ 0 ], [ - 1112, + 1122.2296580516545, 0 ] ] }, { "type": "arrow", - "version": 992, - "versionNonce": 1825887738, + "version": 1047, + "versionNonce": 1991921842, "isDeleted": false, "id": "jFldS1dvD1KlUNloJjncO", "fillStyle": "solid", @@ -224,7 +272,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -249,8 +297,8 @@ }, { "type": "arrow", - "version": 873, - "versionNonce": 403119782, + "version": 928, + "versionNonce": 626379630, "isDeleted": false, "id": "7LqY4x9DQ6sxq0rqNvLa6", "fillStyle": "solid", @@ -272,7 +320,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -297,8 +345,8 @@ }, { "type": "text", - "version": 468, - "versionNonce": 645662394, + "version": 539, + "versionNonce": 1548839538, "isDeleted": false, "id": "E-mK9Dz0MlVGI0zCEjmgE", "fillStyle": "solid", @@ -311,8 +359,8 @@ "y": 315, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 172.50001525878906, - "height": 25, + "width": 153.427734375, + "height": 23, "seed": 1746517598, "groupIds": [], "frameId": null, @@ -323,23 +371,23 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Polling Updater 2", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Polling Updater 2", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 204, - "versionNonce": 1726513530, + "version": 222, + "versionNonce": 1537617326, "isDeleted": false, "id": "ZVYycY9GjFTR8gQbd1wot", "fillStyle": "solid", @@ -352,30 +400,30 @@ "y": 138, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 573.0200805664062, - "height": 35, + "width": 512.0390625, + "height": 32.199999999999996, "seed": 1131654366, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594379770, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 28, - "fontFamily": 1, + "fontFamily": 2, "text": "Threads, Queues, and Buffers Over Time", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Threads, Queues, and Buffers Over Time", - "lineHeight": 1.25, - "baseline": 25 + "lineHeight": 1.15, + "baseline": 26 }, { "type": "text", - "version": 271, - "versionNonce": 763305850, + "version": 371, + "versionNonce": 2091765806, "isDeleted": false, "id": "D7-QRZxcX9A0a-2HK7Nex", "fillStyle": "solid", @@ -385,11 +433,11 @@ "opacity": 100, "angle": 0, "x": 244.53997802734375, - "y": 586, + "y": 591, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 147.16001892089844, - "height": 25, + "width": 132.294921875, + "height": 23, "seed": 1121505758, "groupIds": [], "frameId": null, @@ -400,23 +448,23 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618547987, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Graph Updater", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Graph Updater", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 432, - "versionNonce": 1159736614, + "version": 481, + "versionNonce": 640698350, "isDeleted": false, "id": "8q-Tc9B-UEz9icdriq9yS", "fillStyle": "solid", @@ -429,8 +477,8 @@ "y": 858, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 151.6999969482422, - "height": 25, + "width": 144.140625, + "height": 23, "seed": 883374494, "groupIds": [], "frameId": null, @@ -441,23 +489,23 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "HTTP Handler 1", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "HTTP Handler 1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 385, - "versionNonce": 1042358330, + "version": 435, + "versionNonce": 1238038002, "isDeleted": false, "id": "jb52v3pX5rahA8qitNheB", "fillStyle": "solid", @@ -470,8 +518,8 @@ "y": 948, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 160.52000427246094, - "height": 25, + "width": 144.140625, + "height": 23, "seed": 2076275522, "groupIds": [], "frameId": null, @@ -482,23 +530,23 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "HTTP Handler 2", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "HTTP Handler 2", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 287, - "versionNonce": 1088763578, + "version": 352, + "versionNonce": 1157437998, "isDeleted": false, "id": "NxucM5UWiFJSHCUFadK_p", "fillStyle": "solid", @@ -511,8 +559,8 @@ "y": 393, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 182.4400177001953, - "height": 25, + "width": 167.861328125, + "height": 23, "seed": 2038655710, "groupIds": [], "frameId": null, @@ -523,23 +571,23 @@ "type": "arrow" } ], - "updated": 1708594332225, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Streaming Updater", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Streaming Updater", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 1029, - "versionNonce": 340068602, + "version": 1104, + "versionNonce": 235388082, "isDeleted": false, "id": "pbgCp_v4XB-4ur4z6O3Bc", "fillStyle": "solid", @@ -548,11 +596,11 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 398.26203282117854, + "x": 396.42104218609643, "y": 246.5, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 1119.5800018310547, + "width": 1121.4209924661368, "height": 0, "seed": 1903169154, "groupIds": [], @@ -561,13 +609,13 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618595764, "link": null, "locked": false, "startBinding": { "elementId": "d9AAv6uLddhWu-tS7LU1d", - "focus": 0.04, - "gap": 1.5620358729363488 + "focus": 0.13043478260869562, + "gap": 14.973318797424554 }, "endBinding": null, "lastCommittedPoint": null, @@ -579,15 +627,15 @@ 0 ], [ - 1119.5800018310547, + 1121.4209924661368, 0 ] ] }, { "type": "text", - "version": 507, - "versionNonce": 86481830, + "version": 562, + "versionNonce": 1398663922, "isDeleted": false, "id": "d9AAv6uLddhWu-tS7LU1d", "fillStyle": "solid", @@ -596,12 +644,12 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 233.01998901367188, + "x": 228.01998901367188, "y": 233.5, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 163.6800079345703, - "height": 25, + "width": 153.427734375, + "height": 23, "seed": 724495938, "groupIds": [], "frameId": null, @@ -612,23 +660,23 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618595763, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Polling Updater 1", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Polling Updater 1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 471, - "versionNonce": 2003687866, + "version": 483, + "versionNonce": 1073193330, "isDeleted": false, "id": "2Sp_rAnCP5DVtZtanChb6", "fillStyle": "solid", @@ -659,14 +707,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 407, - "versionNonce": 1648441062, + "version": 425, + "versionNonce": 367191726, "isDeleted": false, "id": "k97D4vwVRWmZjD3yCJXe5", "fillStyle": "solid", @@ -675,34 +723,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 477.57999992370605, - "y": 393.5, + "x": 479.169921875, + "y": 394.5, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 19.84000015258789, - "height": 25, + "width": 16.66015625, + "height": 23, "seed": 493958302, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "rx", "textAlign": "center", "verticalAlign": "middle", "containerId": "2Sp_rAnCP5DVtZtanChb6", "originalText": "rx", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 266, - "versionNonce": 1186894458, + "version": 295, + "versionNonce": 1529258802, "isDeleted": false, "id": "R1aGd7ZcthGGm4ndwxhLB", "fillStyle": "solid", @@ -729,14 +777,14 @@ "id": "_hDI7ak2sTyZPUBS0b8w-" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 191, - "versionNonce": 2013620774, + "version": 230, + "versionNonce": 1248714990, "isDeleted": false, "id": "_hDI7ak2sTyZPUBS0b8w-", "fillStyle": "solid", @@ -745,34 +793,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 509.5599994659424, - "y": 232.5, + "x": 513.8203125, + "y": 233.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 51.880001068115234, - "height": 25, + "width": 43.359375, + "height": 23, "seed": 1824035934, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "fetch", "textAlign": "center", "verticalAlign": "middle", "containerId": "R1aGd7ZcthGGm4ndwxhLB", "originalText": "fetch", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 439, - "versionNonce": 83505978, + "version": 451, + "versionNonce": 1150250226, "isDeleted": false, "id": "RnHvTnp5Odw8X2f_unjuu", "fillStyle": "solid", @@ -799,14 +847,14 @@ "id": "3UyzMOlvG4IIHqPHRbztE" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 408, - "versionNonce": 90256742, + "version": 426, + "versionNonce": 538940206, "isDeleted": false, "id": "3UyzMOlvG4IIHqPHRbztE", "fillStyle": "solid", @@ -815,34 +863,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 718.7000007629395, - "y": 232.5, + "x": 720.4853515625, + "y": 233.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 53.599998474121094, - "height": 25, + "width": 50.029296875, + "height": 23, "seed": 8050462, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "parse", "textAlign": "center", "verticalAlign": "middle", "containerId": "RnHvTnp5Odw8X2f_unjuu", "originalText": "parse", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 629, - "versionNonce": 1686354938, + "version": 641, + "versionNonce": 565771954, "isDeleted": false, "id": "rpCTi5Ye67KDSe26PwR0a", "fillStyle": "solid", @@ -869,14 +917,14 @@ "id": "3Pt6BooWESwxB1gZIA-3v" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 639, - "versionNonce": 1671534758, + "version": 657, + "versionNonce": 828439918, "isDeleted": false, "id": "3Pt6BooWESwxB1gZIA-3v", "fillStyle": "solid", @@ -885,34 +933,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 595.0599994659424, - "y": 313, + "x": 599.3203125, + "y": 314, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 51.880001068115234, - "height": 25, + "width": 43.359375, + "height": 23, "seed": 1747471262, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "fetch", "textAlign": "center", "verticalAlign": "middle", "containerId": "rpCTi5Ye67KDSe26PwR0a", "originalText": "fetch", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 638, - "versionNonce": 1398680762, + "version": 650, + "versionNonce": 1330272370, "isDeleted": false, "id": "p6aB7QEw7RUZGP-S1w8uW", "fillStyle": "solid", @@ -943,14 +991,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 641, - "versionNonce": 980653030, + "version": 659, + "versionNonce": 1669013422, "isDeleted": false, "id": "VaMUIZ6R7q0fVQIF_1H-m", "fillStyle": "solid", @@ -959,39 +1007,39 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 676.7000007629395, - "y": 313, + "x": 678.4853515625, + "y": 314, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 53.599998474121094, - "height": 25, + "width": 50.029296875, + "height": 23, "seed": 1529117726, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "parse", "textAlign": "center", "verticalAlign": "middle", "containerId": "p6aB7QEw7RUZGP-S1w8uW", "originalText": "parse", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 1131, - "versionNonce": 120358266, + "version": 1145, + "versionNonce": 2097838642, "isDeleted": false, "id": "fipdoX_2jWMMttHq0biwj", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -1008,7 +1056,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -1037,8 +1085,8 @@ }, { "type": "rectangle", - "version": 497, - "versionNonce": 870590246, + "version": 509, + "versionNonce": 177666542, "isDeleted": false, "id": "nRkSDPyke6S415ANJpqSO", "fillStyle": "solid", @@ -1065,14 +1113,14 @@ "id": "RHdQtWzei63bDNP8lbDoR" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 434, - "versionNonce": 154256954, + "version": 452, + "versionNonce": 1239442418, "isDeleted": false, "id": "RHdQtWzei63bDNP8lbDoR", "fillStyle": "solid", @@ -1081,34 +1129,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 708.079999923706, - "y": 398.5, + "x": 709.669921875, + "y": 399.5, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 19.84000015258789, - "height": 25, + "width": 16.66015625, + "height": 23, "seed": 362939138, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "rx", "textAlign": "center", "verticalAlign": "middle", "containerId": "nRkSDPyke6S415ANJpqSO", "originalText": "rx", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 601, - "versionNonce": 811618918, + "version": 613, + "versionNonce": 230525998, "isDeleted": false, "id": "DYijQiOE-bzqq2gIyWP7y", "fillStyle": "solid", @@ -1135,14 +1183,14 @@ "id": "OZwQJ1czpEHUJf-5mS2sy" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 538, - "versionNonce": 1112600314, + "version": 556, + "versionNonce": 1607669170, "isDeleted": false, "id": "OZwQJ1czpEHUJf-5mS2sy", "fillStyle": "solid", @@ -1151,34 +1199,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 927.829999923706, - "y": 394.5, + "x": 929.419921875, + "y": 395.5, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 19.84000015258789, - "height": 25, + "width": 16.66015625, + "height": 23, "seed": 710259074, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "rx", "textAlign": "center", "verticalAlign": "middle", "containerId": "DYijQiOE-bzqq2gIyWP7y", "originalText": "rx", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 759, - "versionNonce": 2058687910, + "version": 771, + "versionNonce": 181917294, "isDeleted": false, "id": "dRe5zdw_i7ke_ERGqgHJ-", "fillStyle": "solid", @@ -1205,14 +1253,14 @@ "id": "F4Lr--RTHsm_N-Lrsm8Mu" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 769, - "versionNonce": 118292410, + "version": 787, + "versionNonce": 269062002, "isDeleted": false, "id": "F4Lr--RTHsm_N-Lrsm8Mu", "fillStyle": "solid", @@ -1221,34 +1269,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1200.5599994659424, - "y": 314.5, + "x": 1204.8203125, + "y": 315.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 51.880001068115234, - "height": 25, + "width": 43.359375, + "height": 23, "seed": 1857005470, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "fetch", "textAlign": "center", "verticalAlign": "middle", "containerId": "dRe5zdw_i7ke_ERGqgHJ-", "originalText": "fetch", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 766, - "versionNonce": 2081386726, + "version": 778, + "versionNonce": 2058279086, "isDeleted": false, "id": "vPZEUjjPYESqGvFv-Rp4W", "fillStyle": "solid", @@ -1275,14 +1323,14 @@ "id": "v4M2EX6oGwv9mypkE3nf4" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 771, - "versionNonce": 596179066, + "version": 789, + "versionNonce": 1858785586, "isDeleted": false, "id": "v4M2EX6oGwv9mypkE3nf4", "fillStyle": "solid", @@ -1291,34 +1339,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1282.2000007629395, - "y": 314.5, + "x": 1283.9853515625, + "y": 315.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 53.599998474121094, - "height": 25, + "width": 50.029296875, + "height": 23, "seed": 1954863134, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "parse", "textAlign": "center", "verticalAlign": "middle", "containerId": "vPZEUjjPYESqGvFv-Rp4W", "originalText": "parse", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 174, - "versionNonce": 1878811686, + "version": 246, + "versionNonce": 1146002094, "isDeleted": false, "id": "FLqrds_LtrcgMMNkcCu2q", "fillStyle": "solid", @@ -1328,38 +1376,38 @@ "opacity": 100, "angle": 0, "x": 232, - "y": 489, + "y": 495, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 156.74000549316406, - "height": 25, + "width": 144.53125, + "height": 23, "seed": 310320450, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618530809, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Executor Queue", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Executor Queue", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 976, - "versionNonce": 1736956218, + "version": 990, + "versionNonce": 1870519026, "isDeleted": false, "id": "DCT-vq3X8w4-9CK0i1WK1", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -1376,7 +1424,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -1405,8 +1453,8 @@ }, { "type": "rectangle", - "version": 528, - "versionNonce": 409530214, + "version": 540, + "versionNonce": 873923886, "isDeleted": false, "id": "Y3PoGw9NOC5IBQUKp3cR-", "fillStyle": "solid", @@ -1428,14 +1476,14 @@ "type": 3 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "rectangle", - "version": 727, - "versionNonce": 845980154, + "version": 739, + "versionNonce": 11119794, "isDeleted": false, "id": "vqgYUA0JXn0AfAfEy6BH4", "fillStyle": "hachure", @@ -1470,14 +1518,14 @@ "id": "xP6C3cArVpheLzV5AFg_E" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 163, - "versionNonce": 1906447014, + "version": 181, + "versionNonce": 2040147822, "isDeleted": false, "id": "xP6C3cArVpheLzV5AFg_E", "fillStyle": "cross-hatch", @@ -1486,34 +1534,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 529.289999961853, - "y": 494.14688449999994, + "x": 526.4384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 5.420000076293945, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 226082462, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "1", "textAlign": "center", "verticalAlign": "middle", "containerId": "vqgYUA0JXn0AfAfEy6BH4", "originalText": "1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 653, - "versionNonce": 2134528698, + "version": 665, + "versionNonce": 670385778, "isDeleted": false, "id": "zrqQOL7PgwaKzxPCDeS4N", "fillStyle": "solid", @@ -1540,14 +1588,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "rectangle", - "version": 849, - "versionNonce": 1716833766, + "version": 861, + "versionNonce": 1908281774, "isDeleted": false, "id": "uow6_ERpKpHt29RiN9MNZ", "fillStyle": "cross-hatch", @@ -1578,14 +1626,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 163, - "versionNonce": 1888742266, + "version": 181, + "versionNonce": 505915442, "isDeleted": false, "id": "sjFWKA8ES33JvxT0dVcvh", "fillStyle": "cross-hatch", @@ -1594,34 +1642,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 748.8800001144409, - "y": 494.14688449999994, + "x": 750.4384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 14.239999771118164, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 833466398, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "2", "textAlign": "center", "verticalAlign": "middle", "containerId": "uow6_ERpKpHt29RiN9MNZ", "originalText": "2", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 394, - "versionNonce": 59646246, + "version": 407, + "versionNonce": 2007739374, "isDeleted": false, "id": "OdVAxEmxdP0Y8q-094nMN", "fillStyle": "solid", @@ -1658,16 +1706,20 @@ { "id": "5CrHigwwqK1VKc6Ca3gxy", "type": "arrow" + }, + { + "id": "f2GbV3PcX4-EuddFNyF8B", + "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 283, - "versionNonce": 1555852346, + "version": 301, + "versionNonce": 1797771762, "isDeleted": false, "id": "ZR6W2CO-B65WopEP2jrS1", "fillStyle": "solid", @@ -1676,39 +1728,39 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 642.6499996185303, - "y": 592.5, + "x": 642.59375, + "y": 593.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 1923846622, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "OdVAxEmxdP0Y8q-094nMN", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 549, - "versionNonce": 750455910, + "version": 563, + "versionNonce": 426507822, "isDeleted": false, "id": "CH5dSO6dJ53vGEmqY2aWs", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -1725,7 +1777,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": { @@ -1754,8 +1806,8 @@ }, { "type": "rectangle", - "version": 561, - "versionNonce": 1503057146, + "version": 574, + "versionNonce": 33342386, "isDeleted": false, "id": "hw9PKBQkD_MFDyEU0uuka", "fillStyle": "solid", @@ -1792,16 +1844,20 @@ { "id": "twNjmIr9f-l-aubZs4GnI", "type": "arrow" + }, + { + "id": "Ehl2ZfbTr49ll8jA6pvS7", + "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 446, - "versionNonce": 1336446886, + "version": 464, + "versionNonce": 1005062254, "isDeleted": false, "id": "OqBJjQ-07RyubDNHABeyS", "fillStyle": "solid", @@ -1810,34 +1866,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 871.6499996185303, - "y": 592.5, + "x": 871.59375, + "y": 593.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 934949086, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "hw9PKBQkD_MFDyEU0uuka", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 835, - "versionNonce": 1218848186, + "version": 847, + "versionNonce": 1556970866, "isDeleted": false, "id": "hZnDTRiQOaPQLk9O8COl6", "fillStyle": "hachure", @@ -1872,14 +1928,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 296, - "versionNonce": 411282150, + "version": 314, + "versionNonce": 1055443630, "isDeleted": false, "id": "RG8D9DfwueruyUSdOLsr5", "fillStyle": "cross-hatch", @@ -1888,39 +1944,39 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 809.789999961853, - "y": 494.14688449999994, + "x": 806.9384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 5.420000076293945, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 1643067138, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "1", "textAlign": "center", "verticalAlign": "middle", "containerId": "hZnDTRiQOaPQLk9O8COl6", "originalText": "1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 768, - "versionNonce": 1661820538, + "version": 782, + "versionNonce": 417398578, "isDeleted": false, "id": "N9_cITJx4vKH4lSu_tt_A", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -1937,7 +1993,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": null, @@ -1962,8 +2018,8 @@ }, { "type": "rectangle", - "version": 794, - "versionNonce": 635002406, + "version": 806, + "versionNonce": 555173102, "isDeleted": false, "id": "-A08osWH4z9kW6phuwwOP", "fillStyle": "cross-hatch", @@ -1994,14 +2050,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 260, - "versionNonce": 1167032122, + "version": 278, + "versionNonce": 312363250, "isDeleted": false, "id": "vZnROuYPsK34LqIU31yf7", "fillStyle": "cross-hatch", @@ -2010,39 +2066,39 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 979.1900000572205, - "y": 494.14688449999994, + "x": 980.4384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 13.619999885559082, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 264604994, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "3", "textAlign": "center", "verticalAlign": "middle", "containerId": "-A08osWH4z9kW6phuwwOP", "originalText": "3", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 1147, - "versionNonce": 1016506726, + "version": 1169, + "versionNonce": 2071148974, "isDeleted": false, "id": "twNjmIr9f-l-aubZs4GnI", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -2051,7 +2107,7 @@ "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", "width": 0, - "height": 63, + "height": 58, "seed": 787633410, "groupIds": [], "frameId": null, @@ -2059,18 +2115,18 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618578828, "link": null, "locked": false, "startBinding": { "elementId": "hZnDTRiQOaPQLk9O8COl6", - "focus": 1.0927580941770225, + "focus": 1.0927580941770227, "gap": 3.061017107841735 }, "endBinding": { "elementId": "hw9PKBQkD_MFDyEU0uuka", "focus": -1.0176155308362542, - "gap": 2.061017107841735 + "gap": 2.25 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -2082,19 +2138,19 @@ ], [ 0, - 63 + 58 ] ] }, { "type": "arrow", - "version": 931, - "versionNonce": 2089781242, + "version": 945, + "versionNonce": 25975474, "isDeleted": false, "id": "uxbykYOvdtL0HYuSmUU6U", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -2111,7 +2167,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "startBinding": null, @@ -2136,8 +2192,8 @@ }, { "type": "text", - "version": 217, - "versionNonce": 2080935078, + "version": 345, + "versionNonce": 1879138670, "isDeleted": false, "id": "xMxb5iz16pT1UiTIBqPDG", "fillStyle": "cross-hatch", @@ -2146,34 +2202,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 168, + "x": 199, "y": 682, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 212.36000061035156, - "height": 25, + "width": 166.7578125, + "height": 23, "seed": 1947891714, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Buffer Transit Data", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Buffer Transit Data", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "text", - "version": 254, - "versionNonce": 269514938, + "version": 382, + "versionNonce": 340931698, "isDeleted": false, "id": "rUiVeXZmQgBmVGY4bUGiT", "fillStyle": "cross-hatch", @@ -2182,34 +2238,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 190, + "x": 221, "y": 769, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 185.24000549316406, - "height": 25, + "width": 150.439453125, + "height": 23, "seed": 2073208962, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "Live Transit Data", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Live Transit Data", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 938, - "versionNonce": 299632614, + "version": 950, + "versionNonce": 946799534, "isDeleted": false, "id": "vpqws0a9Jh-wWPUB3jB4Z", "fillStyle": "cross-hatch", @@ -2244,14 +2300,14 @@ "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 248, - "versionNonce": 263541114, + "version": 266, + "versionNonce": 213308978, "isDeleted": false, "id": "m7L49SlkIqM3UX3pIO0v1", "fillStyle": "cross-hatch", @@ -2260,34 +2316,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 895.3371486144409, - "y": 494.14688449999994, + "x": 896.8956250625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 14.239999771118164, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 1912711298, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "2", "textAlign": "center", "verticalAlign": "middle", "containerId": "vpqws0a9Jh-wWPUB3jB4Z", "originalText": "2", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 661, - "versionNonce": 1847466790, + "version": 676, + "versionNonce": 1601068526, "isDeleted": false, "id": "YQXusl7FZ5mjVmkwS7nTt", "fillStyle": "solid", @@ -2324,16 +2380,20 @@ { "id": "j_qtXumsQfWi0I_VI5PkL", "type": "arrow" + }, + { + "id": "PRTeWBK6vtriqNmaS6MK6", + "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 547, - "versionNonce": 515969594, + "version": 565, + "versionNonce": 1537294322, "isDeleted": false, "id": "SbUQ0qpP3hURdF-JpkzcT", "fillStyle": "solid", @@ -2342,34 +2402,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1028.6499996185303, - "y": 592.5, + "x": 1028.59375, + "y": 593.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 1222049730, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "YQXusl7FZ5mjVmkwS7nTt", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 697, - "versionNonce": 2000406118, + "version": 712, + "versionNonce": 436760622, "isDeleted": false, "id": "r6US9SjrUjcRP26-bNJLb", "fillStyle": "solid", @@ -2406,16 +2466,20 @@ { "id": "vSrc221actOuf9WusrgZt", "type": "arrow" + }, + { + "id": "j_qtXumsQfWi0I_VI5PkL", + "type": "arrow" } ], - "updated": 1708594325688, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 583, - "versionNonce": 1489658, + "version": 601, + "versionNonce": 1025012146, "isDeleted": false, "id": "5HrAXQLJ-WCJb8_C8a4uo", "fillStyle": "solid", @@ -2424,34 +2488,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1108.1499996185303, - "y": 592.5, + "x": 1108.09375, + "y": 593.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 586771614, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "r6US9SjrUjcRP26-bNJLb", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 708, - "versionNonce": 1040320934, + "version": 723, + "versionNonce": 338049646, "isDeleted": false, "id": "ye55wrbQ2ruSrJJx9tXx3", "fillStyle": "solid", @@ -2484,16 +2548,20 @@ { "id": "s1NGF8K9oUvWEXmBzmufP", "type": "arrow" + }, + { + "id": "vSrc221actOuf9WusrgZt", + "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 595, - "versionNonce": 574180282, + "version": 613, + "versionNonce": 1413704562, "isDeleted": false, "id": "fbE1QhbN4s8LgICVlAepH", "fillStyle": "solid", @@ -2502,34 +2570,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1188.1499996185303, - "y": 592.5, + "x": 1188.09375, + "y": 593.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 723323586, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "ye55wrbQ2ruSrJJx9tXx3", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 1014, - "versionNonce": 832431334, + "version": 1026, + "versionNonce": 345029806, "isDeleted": false, "id": "oLUKp1kMgqNdxEPEkb372", "fillStyle": "cross-hatch", @@ -2564,14 +2632,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 323, - "versionNonce": 726385786, + "version": 341, + "versionNonce": 1198075186, "isDeleted": false, "id": "TCgAzrDxcbwYL9YI6wNBf", "fillStyle": "cross-hatch", @@ -2580,34 +2648,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1044.880000114441, - "y": 494.14688449999994, + "x": 1046.4384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 14.239999771118164, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 1493949378, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "2", "textAlign": "center", "verticalAlign": "middle", "containerId": "oLUKp1kMgqNdxEPEkb372", "originalText": "2", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 898, - "versionNonce": 1402755110, + "version": 910, + "versionNonce": 552662766, "isDeleted": false, "id": "y5kRBL1VIbCsvey9iszvB", "fillStyle": "hachure", @@ -2642,14 +2710,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435140, "link": null, "locked": false }, { "type": "text", - "version": 360, - "versionNonce": 85009722, + "version": 378, + "versionNonce": 1542502130, "isDeleted": false, "id": "qGVL3pVMGuja1QEF58BVa", "fillStyle": "cross-hatch", @@ -2658,34 +2726,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1130.2964794618529, - "y": 494.14688449999994, + "x": 1127.4449560624998, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 5.420000076293945, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 1347397122, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "1", "textAlign": "center", "verticalAlign": "middle", "containerId": "y5kRBL1VIbCsvey9iszvB", "originalText": "1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 719, - "versionNonce": 1810522982, + "version": 731, + "versionNonce": 567273774, "isDeleted": false, "id": "DwJbh4L0U48rxYKAqNfpB", "fillStyle": "solid", @@ -2712,14 +2780,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "rectangle", - "version": 877, - "versionNonce": 1760325114, + "version": 889, + "versionNonce": 308349106, "isDeleted": false, "id": "OIqNLYrG2vKqjdPIBjJEQ", "fillStyle": "hachure", @@ -2750,14 +2818,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 322, - "versionNonce": 1020593830, + "version": 340, + "versionNonce": 110531438, "isDeleted": false, "id": "2NpZ0UtU8si40TXPeKtsD", "fillStyle": "cross-hatch", @@ -2766,34 +2834,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1356.289999961853, - "y": 494.14688449999994, + "x": 1353.4384765625, + "y": 495.14688449999994, "strokeColor": "#1e1e1e", "backgroundColor": "#4dabf7", - "width": 5.420000076293945, - "height": 25, + "width": 11.123046875, + "height": 23, "seed": 454284126, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "1", "textAlign": "center", "verticalAlign": "middle", "containerId": "OIqNLYrG2vKqjdPIBjJEQ", "originalText": "1", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 893, - "versionNonce": 1809882810, + "version": 905, + "versionNonce": 1113662066, "isDeleted": false, "id": "pAR3k0emFpck8rYDIDQXw", "fillStyle": "solid", @@ -2820,19 +2888,19 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "arrow", - "version": 1002, - "versionNonce": 923353574, + "version": 1016, + "versionNonce": 1770911150, "isDeleted": false, "id": "vEyXTRJW7n_hrgPRIt7d3", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -2849,7 +2917,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -2878,13 +2946,13 @@ }, { "type": "arrow", - "version": 1003, - "versionNonce": 177256314, + "version": 1017, + "versionNonce": 918029362, "isDeleted": false, "id": "8VmQ5RzV8MqKRX9MrvJ3c", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -2901,7 +2969,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -2930,13 +2998,13 @@ }, { "type": "arrow", - "version": 1030, - "versionNonce": 764679462, + "version": 1044, + "versionNonce": 870256622, "isDeleted": false, "id": "s1NGF8K9oUvWEXmBzmufP", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -2953,7 +3021,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -2982,13 +3050,13 @@ }, { "type": "arrow", - "version": 686, - "versionNonce": 893368378, + "version": 700, + "versionNonce": 1229762034, "isDeleted": false, "id": "VJAv_AA4wh9HPi5ElZQsZ", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -3005,7 +3073,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": null, @@ -3030,13 +3098,13 @@ }, { "type": "arrow", - "version": 1095, - "versionNonce": 165838950, + "version": 1109, + "versionNonce": 1100145198, "isDeleted": false, "id": "iXoy2RGlWeoP2O-W4w0Ep", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -3053,7 +3121,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -3082,8 +3150,8 @@ }, { "type": "rectangle", - "version": 829, - "versionNonce": 1540399354, + "version": 843, + "versionNonce": 2133204914, "isDeleted": false, "id": "FG0xyEC6bjrVvfXXFkhGr", "fillStyle": "solid", @@ -3116,16 +3184,24 @@ { "id": "-QpmtHvCIPviWT2XJZRJV", "type": "arrow" + }, + { + "id": "EOpnxfLaitr0veS9fx-7X", + "type": "arrow" + }, + { + "id": "PRTeWBK6vtriqNmaS6MK6", + "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 719, - "versionNonce": 2143388582, + "version": 737, + "versionNonce": 93482094, "isDeleted": false, "id": "7UOhjpWFIqXzoeKvZrdUr", "fillStyle": "solid", @@ -3134,34 +3210,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1390.1499996185303, - "y": 588.5, + "x": 1390.09375, + "y": 589.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 47.70000076293945, - "height": 25, + "width": 47.8125, + "height": 23, "seed": 2065560514, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "apply", "textAlign": "center", "verticalAlign": "middle", "containerId": "FG0xyEC6bjrVvfXXFkhGr", "originalText": "apply", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 329, - "versionNonce": 2043048378, + "version": 341, + "versionNonce": 611015026, "isDeleted": false, "id": "20h5C81H-CYI27Tpd74ou", "fillStyle": "solid", @@ -3192,14 +3268,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 255, - "versionNonce": 1096687334, + "version": 273, + "versionNonce": 765898414, "isDeleted": false, "id": "yz-IQ63iT5cK2GSyekOdh", "fillStyle": "solid", @@ -3208,34 +3284,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 579.4400000572205, - "y": 769.5, + "x": 579.330078125, + "y": 770.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 13.119999885559082, - "height": 25, + "width": 13.33984375, + "height": 23, "seed": 2077060638, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "A", "textAlign": "center", "verticalAlign": "middle", "containerId": "20h5C81H-CYI27Tpd74ou", "originalText": "A", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 370, - "versionNonce": 574892666, + "version": 383, + "versionNonce": 1751341874, "isDeleted": false, "id": "IqMFPR6D4iUuFlmFg2h3z", "fillStyle": "solid", @@ -3266,22 +3342,22 @@ "type": "arrow" }, { - "id": "BYY2sqc6N-uTx5iTOoG2R", + "id": "dtokkLQC1XKcHX81-QdOg", "type": "arrow" }, { - "id": "dtokkLQC1XKcHX81-QdOg", + "id": "f2GbV3PcX4-EuddFNyF8B", "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 298, - "versionNonce": 860181030, + "version": 316, + "versionNonce": 1182312686, "isDeleted": false, "id": "G4Er45oJTuBlpebe08edC", "fillStyle": "solid", @@ -3290,34 +3366,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 578.7300000190735, - "y": 684.5, + "x": 579.330078125, + "y": 685.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 14.539999961853027, - "height": 25, + "width": 13.33984375, + "height": 23, "seed": 157336514, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "B", "textAlign": "center", "verticalAlign": "middle", "containerId": "IqMFPR6D4iUuFlmFg2h3z", "originalText": "B", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 454, - "versionNonce": 202165050, + "version": 574, + "versionNonce": 2037720306, "isDeleted": false, "id": "5CrHigwwqK1VKc6Ca3gxy", "fillStyle": "solid", @@ -3326,8 +3402,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 668, - "y": 630, + "x": 746, + "y": 628, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 0, @@ -3339,18 +3415,18 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { "elementId": "OdVAxEmxdP0Y8q-094nMN", - "focus": -0.013574660633484162, - "gap": 4.5 + "focus": -0.7194570135746605, + "gap": 2.5 }, "endBinding": { "elementId": "IqMFPR6D4iUuFlmFg2h3z", - "focus": 0.44565217391304357, - "gap": 3 + "focus": 0.8695652173913044, + "gap": 5 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -3368,8 +3444,8 @@ }, { "type": "rectangle", - "version": 458, - "versionNonce": 543432038, + "version": 470, + "versionNonce": 1603361582, "isDeleted": false, "id": "VNoc_jPVCooBwfSqYSdhh", "fillStyle": "solid", @@ -3395,10 +3471,6 @@ "type": "text", "id": "49TJbaW8lSUjt9MraulEE" }, - { - "id": "RP9Ow_xaBH5G-VFYLVlAX", - "type": "arrow" - }, { "id": "dtokkLQC1XKcHX81-QdOg", "type": "arrow" @@ -3408,14 +3480,14 @@ "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 384, - "versionNonce": 495145978, + "version": 402, + "versionNonce": 556250802, "isDeleted": false, "id": "49TJbaW8lSUjt9MraulEE", "fillStyle": "solid", @@ -3424,34 +3496,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 883.7300000190735, - "y": 767.5, + "x": 884.330078125, + "y": 768.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 14.539999961853027, - "height": 25, + "width": 13.33984375, + "height": 23, "seed": 638275294, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "B", "textAlign": "center", "verticalAlign": "middle", "containerId": "VNoc_jPVCooBwfSqYSdhh", "originalText": "B", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 564, - "versionNonce": 1845985446, + "version": 577, + "versionNonce": 794412398, "isDeleted": false, "id": "AGWy-WRNKq1GRwlq27Uat", "fillStyle": "solid", @@ -3484,20 +3556,16 @@ { "id": "nXrTlzpTYojAwNzux2iX6", "type": "arrow" - }, - { - "id": "5rqqd0bhXqDAnyrpwoGre", - "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 489, - "versionNonce": 2024457402, + "version": 507, + "versionNonce": 1612845170, "isDeleted": false, "id": "ENTILzjLGHiVQxm21FA85", "fillStyle": "solid", @@ -3506,34 +3574,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1125.5599999427795, - "y": 766, + "x": 1124.7783203125, + "y": 767, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 12.880000114440918, - "height": 25, + "width": 14.443359375, + "height": 23, "seed": 977668802, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "C", "textAlign": "center", "verticalAlign": "middle", "containerId": "AGWy-WRNKq1GRwlq27Uat", "originalText": "C", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 508, - "versionNonce": 1339849702, + "version": 526, + "versionNonce": 924510126, "isDeleted": false, "id": "DRdIzIA5aSRPeNpP2pex-", "fillStyle": "solid", @@ -3567,23 +3635,31 @@ "id": "dtokkLQC1XKcHX81-QdOg", "type": "arrow" }, + { + "id": "nXrTlzpTYojAwNzux2iX6", + "type": "arrow" + }, { "id": "FhwMVCyPxe3HDRW1nTF5C", "type": "arrow" }, { - "id": "nXrTlzpTYojAwNzux2iX6", + "id": "5CrHigwwqK1VKc6Ca3gxy", + "type": "arrow" + }, + { + "id": "Ehl2ZfbTr49ll8jA6pvS7", "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 428, - "versionNonce": 119938426, + "version": 446, + "versionNonce": 170603058, "isDeleted": false, "id": "ETsUVzbEoDEH6Wh5dDIE_", "fillStyle": "solid", @@ -3592,34 +3668,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 884.5599999427795, - "y": 684.5, + "x": 883.7783203125, + "y": 685.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 12.880000114440918, - "height": 25, + "width": 14.443359375, + "height": 23, "seed": 1520179998, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "C", "textAlign": "center", "verticalAlign": "middle", "containerId": "DRdIzIA5aSRPeNpP2pex-", "originalText": "C", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 643, - "versionNonce": 104357434, + "version": 665, + "versionNonce": 1727906286, "isDeleted": false, "id": "hWRH46gwh1PqF21q9C4zp", "fillStyle": "solid", @@ -3658,22 +3734,22 @@ "type": "arrow" }, { - "id": "5rqqd0bhXqDAnyrpwoGre", + "id": "qxLbNSc9nH3MjXNRGDNpH", "type": "arrow" }, { - "id": "qxLbNSc9nH3MjXNRGDNpH", + "id": "PRTeWBK6vtriqNmaS6MK6", "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 568, - "versionNonce": 1588542054, + "version": 586, + "versionNonce": 1646926834, "isDeleted": false, "id": "0N99yapinSp1_mDnKRh6N", "fillStyle": "solid", @@ -3682,34 +3758,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1123.273453809265, - "y": 684.5, + "x": 1123.8517743124999, + "y": 685.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 15.600000381469727, - "height": 25, + "width": 14.443359375, + "height": 23, "seed": 130817118, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "D", "textAlign": "center", "verticalAlign": "middle", "containerId": "hWRH46gwh1PqF21q9C4zp", "originalText": "D", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 506, - "versionNonce": 1323761062, + "version": 609, + "versionNonce": 1108038702, "isDeleted": false, "id": "rYbr17RAyFgxzOKrfTzeq", "fillStyle": "solid", @@ -3718,7 +3794,7 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 893.9011232891656, + "x": 982.9011232891656, "y": 628.5371760879644, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", @@ -3731,17 +3807,17 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { "elementId": "hw9PKBQkD_MFDyEU0uuka", - "focus": 0.013665612913114198, + "focus": -0.7470181477706465, "gap": 3.03717608796444 }, "endBinding": { "elementId": "DRdIzIA5aSRPeNpP2pex-", - "focus": 0.023976225530294545, + "focus": 0.7595134156129394, "gap": 4.46282391203556 }, "lastCommittedPoint": null, @@ -3760,8 +3836,8 @@ }, { "type": "arrow", - "version": 630, - "versionNonce": 894277562, + "version": 681, + "versionNonce": 908447154, "isDeleted": false, "id": "j_qtXumsQfWi0I_VI5PkL", "fillStyle": "solid", @@ -3770,8 +3846,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1059.202579342441, - "y": 629.3859425029369, + "x": 1079.202579342441, + "y": 626.3859425029369, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 0, @@ -3783,18 +3859,18 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { - "elementId": "YQXusl7FZ5mjVmkwS7nTt", - "focus": -0.17409296993353024, - "gap": 3.8859425029369277 + "elementId": "r6US9SjrUjcRP26-bNJLb", + "focus": 1.3713615755210156, + "gap": 14.297420657559087 }, "endBinding": { "elementId": "hWRH46gwh1PqF21q9C4zp", - "focus": -0.6090752089623642, - "gap": 2.1140574970630723 + "focus": -0.4395836835386352, + "gap": 5.114057497063072 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -3812,8 +3888,8 @@ }, { "type": "arrow", - "version": 612, - "versionNonce": 1103386854, + "version": 654, + "versionNonce": 36478574, "isDeleted": false, "id": "vSrc221actOuf9WusrgZt", "fillStyle": "solid", @@ -3822,7 +3898,7 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1135.073454342441, + "x": 1155.073454342441, "y": 626.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", @@ -3835,17 +3911,17 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { "elementId": "r6US9SjrUjcRP26-bNJLb", - "focus": -0.07982998292054501, + "focus": -0.5993105024010645, "gap": 1 }, "endBinding": { "elementId": "hWRH46gwh1PqF21q9C4zp", - "focus": 0.033898307986789215, + "focus": 0.20338983341051808, "gap": 5.00000049706307 }, "lastCommittedPoint": null, @@ -3864,8 +3940,8 @@ }, { "type": "arrow", - "version": 639, - "versionNonce": 1844875386, + "version": 705, + "versionNonce": 2068137842, "isDeleted": false, "id": "S736oegZ-NP_Jn8T2r-5l", "fillStyle": "solid", @@ -3874,8 +3950,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1210.664620342441, - "y": 630.580001502937, + "x": 1233.664620342441, + "y": 627.580001502937, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 0, @@ -3887,18 +3963,18 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { "elementId": "ye55wrbQ2ruSrJJx9tXx3", - "focus": 0.03468518591062566, - "gap": 5.080001502936966 + "focus": -0.562717411491972, + "gap": 2.080001502936966 }, "endBinding": { "elementId": "hWRH46gwh1PqF21q9C4zp", - "focus": 0.6745014096817041, - "gap": 1 + "focus": 0.8694166639189923, + "gap": 4 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -3916,8 +3992,8 @@ }, { "type": "rectangle", - "version": 246, - "versionNonce": 2127842342, + "version": 259, + "versionNonce": 314292146, "isDeleted": false, "id": "tCFT7TaS4GWxUTPH3e9dM", "fillStyle": "solid", @@ -3946,16 +4022,20 @@ { "id": "F4a-HNfDUp8O41pcv3trV", "type": "arrow" + }, + { + "id": "UiGgSvKQYLhHvq9f8X1v6", + "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618460030, "link": null, "locked": false }, { "type": "text", - "version": 146, - "versionNonce": 1082290490, + "version": 164, + "versionNonce": 1775208754, "isDeleted": false, "id": "pM0euEcA3O04vH8cF6euv", "fillStyle": "solid", @@ -3964,34 +4044,34 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 834.689998626709, - "y": 863, + "x": 840.57421875, + "y": 864, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 119.62000274658203, - "height": 25, + "width": 107.8515625, + "height": 23, "seed": 1366808386, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "routing on A", "textAlign": "center", "verticalAlign": "middle", "containerId": "tCFT7TaS4GWxUTPH3e9dM", "originalText": "routing on A", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 494, - "versionNonce": 1409537894, + "version": 507, + "versionNonce": 718043758, "isDeleted": false, "id": "ia9PMtL1s9vwB3H7pqe7w", "fillStyle": "solid", @@ -4020,16 +4100,20 @@ { "id": "VM8X0qyPeoxkTBRdLIM5J", "type": "arrow" + }, + { + "id": "OerFprXRPVst8_BgNyFgG", + "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618485986, "link": null, "locked": false }, { "type": "text", - "version": 393, - "versionNonce": 1079261690, + "version": 411, + "versionNonce": 422286066, "isDeleted": false, "id": "vniZK_pa9hyb8VzJ0L1hw", "fillStyle": "solid", @@ -4038,39 +4122,39 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1161.310001373291, - "y": 950.5, + "x": 1166.5224609375, + "y": 951.5, "strokeColor": "#1e1e1e", "backgroundColor": "#b2f2bb", - "width": 119.37999725341797, - "height": 25, + "width": 108.955078125, + "height": 23, "seed": 1850603742, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "routing on C", "textAlign": "center", "verticalAlign": "middle", "containerId": "ia9PMtL1s9vwB3H7pqe7w", "originalText": "routing on C", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 478, - "versionNonce": 1235221158, + "version": 492, + "versionNonce": 255905070, "isDeleted": false, "id": "F4a-HNfDUp8O41pcv3trV", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -4087,7 +4171,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -4116,8 +4200,8 @@ }, { "type": "rectangle", - "version": 555, - "versionNonce": 477031098, + "version": 568, + "versionNonce": 467690674, "isDeleted": false, "id": "9R0Z6nTrFuTnbu8bnCl12", "fillStyle": "solid", @@ -4146,20 +4230,16 @@ { "id": "qxLbNSc9nH3MjXNRGDNpH", "type": "arrow" - }, - { - "id": "TKQL22FgfD_EaFnrCOSUr", - "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 481, - "versionNonce": 1448457702, + "version": 499, + "versionNonce": 1548416878, "isDeleted": false, "id": "4sB5qUMBsDqFPfGtsMHz0", "fillStyle": "solid", @@ -4168,34 +4248,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1364.1169379747294, - "y": 766.5, + "x": 1364.6952584779642, + "y": 767.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 15.600000381469727, - "height": 25, + "width": 14.443359375, + "height": 23, "seed": 939027038, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "D", "textAlign": "center", "verticalAlign": "middle", "containerId": "9R0Z6nTrFuTnbu8bnCl12", "originalText": "D", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "rectangle", - "version": 599, - "versionNonce": 729121658, + "version": 618, + "versionNonce": 462749298, "isDeleted": false, "id": "kX2Vp26fOxXlTY3cVukwK", "fillStyle": "solid", @@ -4226,18 +4306,38 @@ "type": "arrow" }, { - "id": "TKQL22FgfD_EaFnrCOSUr", + "id": "LK3pGdePKHmYAtL6fLqtH", + "type": "arrow" + }, + { + "id": "S736oegZ-NP_Jn8T2r-5l", + "type": "arrow" + }, + { + "id": "EOpnxfLaitr0veS9fx-7X", + "type": "arrow" + }, + { + "id": "huWlX3Fayf86PF4taHvva", + "type": "arrow" + }, + { + "id": "GAjbUIvQFGDOdQ6xF1wwe", + "type": "arrow" + }, + { + "id": "PRTeWBK6vtriqNmaS6MK6", "type": "arrow" } ], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false }, { "type": "text", - "version": 523, - "versionNonce": 322046246, + "version": 541, + "versionNonce": 1393940910, "isDeleted": false, "id": "-iIGa2IVrKzgz65Cw-4Tx", "fillStyle": "solid", @@ -4246,34 +4346,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1365.176938394346, - "y": 684.5, + "x": 1365.2470162904642, + "y": 685.5, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 13.479999542236328, - "height": 25, + "width": 13.33984375, + "height": 23, "seed": 51112670, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "fontSize": 20, - "fontFamily": 1, + "fontFamily": 2, "text": "E", "textAlign": "center", "verticalAlign": "middle", "containerId": "kX2Vp26fOxXlTY3cVukwK", "originalText": "E", - "lineHeight": 1.25, + "lineHeight": 1.15, "baseline": 18 }, { "type": "arrow", - "version": 672, - "versionNonce": 1872884838, + "version": 705, + "versionNonce": 1964007474, "isDeleted": false, "id": "-QpmtHvCIPviWT2XJZRJV", "fillStyle": "solid", @@ -4282,8 +4382,8 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 1413.9999998420708, - "y": 627.7803268401005, + "x": 1438.9999998420708, + "y": 625.7803268401005, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 0, @@ -4295,18 +4395,18 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { "elementId": "FG0xyEC6bjrVvfXXFkhGr", - "focus": 3.899486604675561e-9, - "gap": 5.2803268401005425 + "focus": -0.6172839467177975, + "gap": 3.2803268401005425 }, "endBinding": { "elementId": "kX2Vp26fOxXlTY3cVukwK", - "focus": 0.3477938981537734, - "gap": 5.2996746628364235 + "focus": 0.5544054684017073, + "gap": 7.2996746628364235 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -4324,13 +4424,13 @@ }, { "type": "arrow", - "version": 895, - "versionNonce": 1702000550, + "version": 909, + "versionNonce": 1791652846, "isDeleted": false, "id": "dtokkLQC1XKcHX81-QdOg", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -4347,7 +4447,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -4376,8 +4476,8 @@ }, { "type": "arrow", - "version": 872, - "versionNonce": 986627514, + "version": 976, + "versionNonce": 1413057010, "isDeleted": false, "id": "FhwMVCyPxe3HDRW1nTF5C", "fillStyle": "solid", @@ -4386,12 +4486,12 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 789.6236215285376, - "y": 753.6359425029369, + "x": 755.4210421860963, + "y": 698.0442609226892, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 28.59168158024795, - "height": 28.591681580247723, + "width": 44.794260922689205, + "height": 0, "seed": 148629990, "groupIds": [], "frameId": null, @@ -4399,19 +4499,15 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { - "elementId": "VNoc_jPVCooBwfSqYSdhh", - "focus": -0.8995805349896169, - "gap": 5.364057497063072 - }, - "endBinding": { "elementId": "DRdIzIA5aSRPeNpP2pex-", - "focus": 0.31507349273609164, - "gap": 7.044260922689205 + "focus": 0.04972671060424786, + "gap": 14.578957813903685 }, + "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", @@ -4421,20 +4517,20 @@ 0 ], [ - 28.59168158024795, - -28.591681580247723 + 44.794260922689205, + 0 ] ] }, { "type": "arrow", - "version": 995, - "versionNonce": 751866598, + "version": 1009, + "versionNonce": 1299301934, "isDeleted": false, "id": "nXrTlzpTYojAwNzux2iX6", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, @@ -4451,7 +4547,7 @@ "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { @@ -4480,41 +4576,185 @@ }, { "type": "arrow", - "version": 944, - "versionNonce": 878900858, + "version": 1014, + "versionNonce": 2079641710, + "isDeleted": false, + "id": "qxLbNSc9nH3MjXNRGDNpH", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1230.1984920751363, + "y": 723.795967694437, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "width": 26.829463375617934, + "height": 26.727165592834353, + "seed": 149713658, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708618435141, + "link": null, + "locked": false, + "startBinding": { + "elementId": "hWRH46gwh1PqF21q9C4zp", + "focus": -0.5137527808081439, + "gap": 4.295967694436968 + }, + "endBinding": { + "elementId": "9R0Z6nTrFuTnbu8bnCl12", + "focus": -0.6031631617717584, + "gap": 6.476866712728679 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 26.829463375617934, + 26.727165592834353 + ] + ] + }, + { + "type": "arrow", + "version": 1024, + "versionNonce": 1914475182, + "isDeleted": false, + "id": "ewxb68pxxmvpvTX8GJKkx", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 996.0022491938748, + "y": 699.5486691975981, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 44.794260922689205, + "height": 0, + "seed": 541981294, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708618435141, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 44.794260922689205, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 1009, + "versionNonce": 967685938, "isDeleted": false, - "id": "5rqqd0bhXqDAnyrpwoGre", + "id": "LK3pGdePKHmYAtL6fLqtH", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1030.5353717302974, - "y": 751.9090757902081, + "x": 1235.0022491938746, + "y": 697.5486691975981, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 44.794260922689205, + "height": 0, + "seed": 534458606, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708618435141, + "link": null, + "locked": false, + "startBinding": { + "elementId": "kX2Vp26fOxXlTY3cVukwK", + "focus": 0.026127104647527977, + "gap": 15.914688971589612 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 44.794260922689205, + 0 + ] + ] + }, + { + "type": "arrow", + "version": 896, + "versionNonce": 1888162482, + "isDeleted": false, + "id": "PRTeWBK6vtriqNmaS6MK6", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1395.192414170092, + "y": 671.8730478428461, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 28.59168158024795, - "height": 28.591681580247723, - "seed": 1446800038, + "width": 0, + "height": 42.919998497063226, + "seed": 513182898, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { - "elementId": "AGWy-WRNKq1GRwlq27Uat", - "focus": -0.9114274197828796, - "gap": 4.090924209791865 + "elementId": "kX2Vp26fOxXlTY3cVukwK", + "focus": 0.19235930582337, + "gap": 4.126952157153937 }, "endBinding": { - "elementId": "hWRH46gwh1PqF21q9C4zp", - "focus": 0.32476161195369313, - "gap": 3.817394209960412 + "elementId": "FG0xyEC6bjrVvfXXFkhGr", + "focus": 0.46438483530637104, + "gap": 6.4530493457828015 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -4525,48 +4765,48 @@ 0 ], [ - 28.59168158024795, - -28.591681580247723 + 0, + -42.919998497063226 ] ] }, { "type": "arrow", - "version": 1000, - "versionNonce": 1883850278, + "version": 611, + "versionNonce": 2137663854, "isDeleted": false, - "id": "qxLbNSc9nH3MjXNRGDNpH", + "id": "f2GbV3PcX4-EuddFNyF8B", "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dotted", + "strokeWidth": 1, + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1230.1984920751363, - "y": 723.795967694437, + "x": 585.4763931790178, + "y": 671.5692639960337, "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 26.829463375617934, - "height": 26.727165592834353, - "seed": 149713658, + "backgroundColor": "#ffec99", + "width": 0, + "height": 43.00000000000002, + "seed": 1969796334, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { - "elementId": "hWRH46gwh1PqF21q9C4zp", - "focus": -0.5137527808081439, - "gap": 4.295967694436968 + "elementId": "IqMFPR6D4iUuFlmFg2h3z", + "focus": -0.0028456892444684526, + "gap": 4.430736003966331 }, "endBinding": { - "elementId": "9R0Z6nTrFuTnbu8bnCl12", - "focus": -0.6031631617717584, - "gap": 6.476866712728679 + "elementId": "OdVAxEmxdP0Y8q-094nMN", + "focus": 0.7332453105971238, + "gap": 3.069263996033669 }, "lastCommittedPoint": null, "startArrowhead": null, @@ -4577,49 +4817,93 @@ 0 ], [ - 26.829463375617934, - 26.727165592834353 + 0, + -43.00000000000002 ] ] }, { "type": "arrow", - "version": 961, - "versionNonce": 141694778, + "version": 684, + "versionNonce": 582610034, "isDeleted": false, - "id": "TKQL22FgfD_EaFnrCOSUr", + "id": "Ehl2ZfbTr49ll8jA6pvS7", "fillStyle": "solid", "strokeWidth": 1, - "strokeStyle": "solid", + "strokeStyle": "dashed", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1269.8469813268543, - "y": 751.9090757902081, + "x": 812.3775164681838, + "y": 673.985380034576, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", - "width": 27.330342883103867, - "height": 26.591681580247723, - "seed": 2006451130, + "width": 0, + "height": 43.00000000000002, + "seed": 1285547822, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, "boundElements": [], - "updated": 1708594325689, + "updated": 1708618435141, "link": null, "locked": false, "startBinding": { - "elementId": "9R0Z6nTrFuTnbu8bnCl12", - "focus": -0.904619473082073, - "gap": 5.090924209791865 + "elementId": "DRdIzIA5aSRPeNpP2pex-", + "focus": -0.6497725911720351, + "gap": 2.0146199654240036 }, "endBinding": { - "elementId": "kX2Vp26fOxXlTY3cVukwK", - "focus": 0.32006279982418195, - "gap": 7.317394209960412 + "elementId": "hw9PKBQkD_MFDyEU0uuka", + "focus": 0.7104485771950103, + "gap": 5.485380034575996 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -43.00000000000002 + ] + ] + }, + { + "type": "arrow", + "version": 1101, + "versionNonce": 1098655218, + "isDeleted": false, + "id": "ZlUIWDp6idXzxgON9_x4C", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 393.4210421860963, + "y": 1042.4486805801255, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1114.4500070296972, + "height": 0, + "seed": 1024031278, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 }, + "boundElements": [], + "updated": 1708618503504, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", @@ -4629,8 +4913,92 @@ 0 ], [ - 27.330342883103867, - -26.591681580247723 + 1114.4500070296972, + 0 + ] + ] + }, + { + "type": "text", + "version": 599, + "versionNonce": 849778610, + "isDeleted": false, + "id": "Z5ItqL1_S5-oF1sZwu3my", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 208.01593017578125, + "y": 1026.75, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 162.3046875, + "height": 23, + "seed": 108397678, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1708618503504, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 2, + "text": "Garbage Collector", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Garbage Collector", + "lineHeight": 1.15, + "baseline": 18 + }, + { + "type": "arrow", + "version": 582, + "versionNonce": 1642554094, + "isDeleted": false, + "id": "OerFprXRPVst8_BgNyFgG", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1372.2508231792017, + "y": 989.1675772818068, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 0, + "height": 46.08242271819347, + "seed": 282180850, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1708618513548, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ia9PMtL1s9vwB3H7pqe7w", + "focus": -0.9789697293152213, + "gap": 3.667577281806757 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + 46.08242271819347 ] ] } diff --git a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg index 36f9fc8ddfe..260d8a450d4 100644 --- a/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg +++ b/src/main/java/org/opentripplanner/updater/images/updater-threads-queues.svg @@ -1,4 +1,4 @@ - + @@ -18,4 +18,4 @@ - Polling Updater 2Threads, Queues, and Buffers Over TimeGraph UpdaterHTTP Handler 1HTTP Handler 2Streaming UpdaterPolling Updater 1rxfetchparsefetchparserxrxfetchparseExecutor Queue12applyapply13Buffer Transit DataLive Transit Data2applyapplyapply211applyABBCCDrouting on Arouting on CDE \ No newline at end of file + Polling Updater 2Threads, Queues, and Buffers Over TimeGraph UpdaterHTTP Handler 1HTTP Handler 2Streaming UpdaterPolling Updater 1rxfetchparsefetchparserxrxfetchparseExecutor Queue12applyapply13Buffer Transit DataLive Transit Data2applyapplyapply211applyABBCCDrouting on Arouting on CDEGarbage Collector \ No newline at end of file From c23038e229e26247846c7762ff23c0d7fe2dc054 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 23 Feb 2024 17:38:50 +0800 Subject: [PATCH 10/24] add data sources and incrementality sections [ci skip] --- .../org/opentripplanner/updater/package.md | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 26120cfea9b..345431300c6 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -1,6 +1,24 @@ # Realtime Updaters -## Realtime Concurrency Overview +## Realtime Data Sources + +Published transit data is broadly divided into two categories, which represent different time scales. On one hand we have scheduled data (also called static or theoretical data), and on the other hand realtime (or dynamic) data. Scheduled data is supplied in GTFS or NeTEx format, with the corresponding realtime data supplied in the [GTFS-RT](https://gtfs.org/realtime/reference/) and [SIRI](https://www.siri-cen.eu/) formats, respectively. This package contains code that retrieves and decodes realtime data, then layers it on top of the static transit data in a live OTP instance while it continues to handle routing requests. + +Different data producers might update their scheduled data every month, week, or day. Realtime data then covers any changes to service at a timescale shorter than that of a given producer's scheduled data. Broadly speaking, realtime data represents short-term unexpected or unplanned changes that modify the planned schedules, and could require changes to journeys that the riders would not expect from the schedule. OpenTripPlanner uses three main categories of realtime data which are summarized in the table below. The SIRI specification includes more types, but OTP handles only these three that correspond to the three GTFS-RT types. + +| GTFS-RT Name | SIRI Name | Description | +|----------------------------------------------------------------------------------|--------------------------|-----------------------------------------------------------------------| +| [Service Alert](https://gtfs.org/realtime/reference/#message-alert) | Situation Exchange (SX) | Text descriptions relevant to riders using a particular route or stop | +| [Trip Update](https://gtfs.org/realtime/reference/#message-tripupdate) | Estimated Timetable (ET) | Observed or expected arrival and departure times for near-term trips | +| [Vehicle Position](https://gtfs.org/realtime/reference/#message-vehicleposition) | Vehicle Monitoring (VM) | Physical location of vehicles currently providing service | + +GTFS-RT takes the form of binary protocol buffer messages that are typically fetched over HTTP. On the other hand, the SIRI specification originally described SOAP remote procedure calls retrieving an XML representation of the messages. Various projects instead adopted simple HTTP GET requests passing parameters in the URL and returning a JSON representation. The latter approach was eventually officially recognized as "SIRI Lite". + +Because OTP handles both GTFS-RT and SIRI data sources, there will often be two equivalent classes for retrieving and interpreting a particular kind of realtime data. For example, there is a SiriAlertsUpdateHandler and an AlertsUpdateHandler. The SIRI variants are typically prefixed with `Siri` while the GTFS-RT ones have no prefix for historical reasons (the GTFS-RT versions were originally the only ones). These should perhaps be renamed with a `GtfsRt` prefix for symmetry. Once the incoming messages have been decoded, they will ideally be mapped into a single internal class that was originally derived from GTFS-RT but has been extended to cover all information afforded by both GTFS and SIRI. For example, both classes mentioned above produce TransitAlert instances. These uniform internal representations can then be applied to the internal transit model using a single mechanism, independent of the message source type. + +In practice, OTP does not yet use a single uniform internal representation for each of the three main message types. Particularly for TripUpdates/SIRI-ET, a lot of custom behavior was introduced for the SIRI case which led to a split between the two implementations. Our aim is to eventually merge the two systems back into one. NOTE: the comments on source code may be deceptive in cases where classes were copied and altered to handle SIRI data. This was sometimes done under time pressure to resolve production bugs. In these situations some comments were inadvertently duplicated without being updated. For example, despite many comments and field names mentioning "trip updates", SIRI estimated timetables are not converted into an internal TripUpdate object. A very notable case is the two implementations of TimetableSnapshotProvider: TimetableSnapshotSource and SiriTimetableSnapshotSource should really be a single implementation operating on internal data types independent of SIRI or GTFS-RT. + +## Realtime Concurrency The following approach to realtime concurrency was devised around 2013 when OTP first started consuming realtime data that affected routing results rather than just displaying messages. At first, the whole realtime system followed this approach. Some aspects of this system were maintained in subsequent work over the years, but because the details and rationale were not fully documented, misinterpretations and subtle inconsistencies were introduced. @@ -8,7 +26,7 @@ On 11 January 2024 a team of OTP developers reviewed this realtime concurrency a The following is a sequence diagram showing how threads are intended to communicate. Unlike some common forms of sequence diagrams, time is on the horizontal axis here. Each horizontal line represents either a thread of execution (handling incoming realtime messages or routing requests) or a queue or buffer data structure. Dotted lines represent object references being handed off, and solid lines represent data being copied. -![Architecture diagram](images/updater-threads-queues.svg) +![Realtime sequence diagram](images/updater-threads-queues.svg) At the top of the diagram are the GraphUpdater implementations. These fall broadly into two categories: polling updaters and streaming updaters. Polling updaters periodically send a request to server (often just a simple HTTP server) which returns a file containing the latest version of the updates. Streaming updaters are generally built around libraries implementing message-oriented protocols such as AMQP or WebSockets, which fire a callback each time a new message is received. Polling updaters tend to return a full dataset describing the entire system state on each polling operation, while streaming updaters tend to receive incremental messages targeting individual transit trips. As such, polling updaters execute relatively infrequently (perhaps every minute or two) and process large responses, while streaming updaters execute very frequently (often many times per second) and operate on small messages in short bursts. Polling updaters are simpler in many ways and make use of common HTTP server components, but they introduce significant latency and redundant communication. Streaming updaters require more purpose-built or custom-configured components including message brokers, but bandwidth consumption and latency are lower, allowing routing results to reflect vehicle delays and positions immediately after they're reported. @@ -20,7 +38,31 @@ This writable buffer of transit data is periodically made immutable and swapped This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where 1) writing operations are simple to reason about and cannot conflict because only one write happens at a time; 2) multiple read operations (including routing requests) can occur concurrently; 3) read operations do not need to pause while writes are happening; 4) read operations see only fully completed write operations, never partial writes; and 5) each read operation sees a consistent, unchanging view of the transit data. -Importantly, no locking is necessary, though some form of synchronization is applied during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. (While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off.) +An important characteristic of this approach is that _no locking is necessary_. However, some form of synchronization is used during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off. Arguably the process of creating an immutable live snapshot (and a corresponding new writable buffer) should be handled by a GraphWriterRunnable on the single graph updater thread. This would serve to defer any queued modifications until the new buffer is in place, without introducing any further locking mechanisms. +## Full Dataset versus Incremental Messages + +The GTFS-RT specification includes an "incrementality" field. The specification says this field is unsupported and its behavior is undefined, but in practice people have been using this field since around 2013 in a fairly standardized way. An effort is underway to document its usage and update the standard (see https://github.com/google/transit/issues/84). + +GTFS-RT messages are most commonly distributed by HTTP GET polling. In this method, the consumer (OTP) has to make a request each time it wants to check for updates, and will receive all messages about all parts of the transit system at once, including messages it's already seen before. The incrementality field allows for some other options. As used in practice, there are three main aspects: + +- **Differential vs. full-dataset:** The incrementality field can take on two values: differential and full_dataset. In full-dataset mode, you'll get one big FeedMessage containing every update for every trip or vehicle. In differential mode, you'll receive updates for each trip or vehicle as they stream in, either individually or in small blocks. This may include a guarantee that an update will be provided on every entity at least once every n minutes, or alternatively the producer sending the full dataset when you first connect, then sending only changes. Once you're in differential mode, this opens up the possibilities below. + + - **Poll vs. push:** In practice differential messages are usually distributed individually or in small blocks via a message queue. This means the notifications are pushed by the message queueing system as soon as they arrive, rather than pulled by the consumer via occasional polling. It would in principle also be possible to provide differential updates by polling, with the producer actively tracking the last time each consumer polled (sessions), or the consumer including the producer-supplied timestamp of its last fetch, but we are not aware of any such implementations. Combining differential+push means that vehicle positions and trip updates can be received immediately after the vehicles report their positions. In some places vehicles provide updates every few seconds, so their position is genuinely known in real time. + +- **Message filtering:** Message queue systems often allow filtering by topic. A continuous stream of immediate small push messages is already an improvement over full dataset fetching, but if you only care about one route (for an arrival time display panel for example) you don't want to continuously receive and parse thousands of messages per second looking for the relevant ones. So rather than a "firehose" of every message, you subscribe to a topic that includes only messages for that one route. You then receive a single message every few seconds with the latest predictions for the routes you care about. + +The latency and bandwidth advantages of differential message passing systems are evident, particularly in large (national/metropolitan) realtime passenger information systems created through an intentional and thorough design process. + +SIRI allows for both polling and pub-sub approaches to message distribution. These correspond to the full dataset and differential modes described for GTFS-RT. + +## SIRI Resources + +- The official SIRI standardization page: https://www.siri-cen.eu/ +- Entur page on SIRI and GTFS-RT: https://developer.entur.org/pages-real-time-intro +- Original proposal and description of SIRI Lite (in French): http://www.normes-donnees-tc.org/wp-content/uploads/2017/01/Proposition-Profil-SIRI-Lite-initial-v1-2.pdf +- UK Government SIRI VM guidance: https://www.gov.uk/government/publications/technical-guidance-publishing-location-data-using-the-bus-open-data-service-siri-vm/technical-guidance-siri-vm +- Wikipedia page: https://en.wikipedia.org/wiki/Service_Interface_for_Real_Time_Information# + From 79de961f916c5ad724c2416b72e0a35199fa10b8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 24 Feb 2024 01:04:17 +0800 Subject: [PATCH 11/24] add design considerations section --- .../org/opentripplanner/updater/package.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 345431300c6..0a1b6974a1e 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -42,6 +42,42 @@ An important characteristic of this approach is that _no locking is necessary_. Arguably the process of creating an immutable live snapshot (and a corresponding new writable buffer) should be handled by a GraphWriterRunnable on the single graph updater thread. This would serve to defer any queued modifications until the new buffer is in place, without introducing any further locking mechanisms. +## Design Considerations + +This section summarizes the rationale behind some of the design decisions. + +### Realtime is Highly Concurrent + +An OTP instance can have multiple sources of realtime data at once. In some cases the transit data includes several feeds of scheduled data from different providers, with one or more types of realtime updates for those different feeds. + +In a large production system, ideally all the scheduled data would be integrated into a single data source with a unified ID namespace, and all the realtime data would also be integrated into a single data source with an exactly matching namespace. This would be the responsibility of a separate piece of software outside (upstream of) OTP. In practice, such an upstream data integration system does not exist in many deployments and OTP must manage several static and realtime data sources at once. Even when data feeds are well-integrated, the different kinds of realtime (arrival time updates, vehicle positions, or text alerts) may be split across multiple feeds as described in the GTFS-RT spec, which implies polling three different files. + +Each OTP instance in such a large configuration is also typically intended to handle several requests concurrently. Each incoming request needs to perform essentially random reads from the same large data structure representing the transit network, so there are efficiency benefits to many concurrent searches happening on the same instance, sharing this one large data structure. In a load-balanced cluster of OTP instances, realtime updates must be received and applied to each copy of the transportation network separately. So sharing each copy of the transportation network between a larger number of concurrent routing requests reduces the number of identical, arguably redundant network update processes going on simultaneously. + +In OTP the combined static and realtime transit data is a relatively large, deeply nested and interconnected data structure. It would take time to copy that structure, and especially to perform a deep copy of every nested object. Within a single instance, making multiple totally independent copies of this structure for different successive snapshots would tend to scatter reads from different routing threads across widely dispersed memory addresses, reducing cache efficiency. It could also lead to high (or highly variable) memory usage. In order to make updates to the transit data available frequently (as often as once every second, or as quickly as possible after each individual message comes in) we do not want to completely replicate the entire transit data structure for each snapshot. This would consume a significant fraction of the instance's available resources and likely degrade the aggregate performance of concurrently handled requests. + +### No Destructive Changes to Scheduled Data + +TripUpdates/SIRI-ET timetables cannot simply replace (overwrite) the original scheduled data. The updated timetables must be stored alongside the original scheduled ones. We need to retain the original data for several reasons. First, some routing requests should not be affected by realtime updates. The most obvious example is searches on future dates, which must use the scheduled trip objects. It is also common to show users delays relative to the originally planned schedule (like `8:30 +5min`). When a realtime disruption is severe enough, users may also expect or need to see the resulting itinerary in comparison with the one they expected to see in the absence of real-time disruptions. + +### Multiple Coexisting Read-only Snapshots + +Routing requests are relatively slow. They may take many seconds to process, and the response time is highly variable. For the duration that OTP is handling a single request, that request should see an effectively immutable, unchanging snapshot of the transit data. Even if new updates are constantly streaming in, each individual request must see a stable and unchanging view that remains internally consistent. Both the incoming realtime updates and the routing requests must behave like transactions that are “serializable” in the database concurrency control sense of the term: from any externally visible perspective, everything appears as if the reads and writes all happened in contiguous blocks in a single non-branching sequence, one after the other, even though much of the work is being done on threads in parallel. + +We take advantage of Java’s garbage collected environment here: once snapshots are no longer in use by any active routing code, they become unreferenced and are candidates for garbage collection. Of course the vast majority of the nested sub-objects used by a snapshot may still be in use, referenced by one or more successive snapshots that did not modify them, and therefore reused indefinitely. As long as each successive snapshot treats the previous ones (and all their nested sub-objects) as immutable, applying a copy-on-write policy to incorporate new information arriving in realtime updates, the garbage collector will weed out and deallocate subtrees as they are invalidated by those incoming realtime updates, once they are no longer needed by any active routing request. + +At any given moment there is a single most recent read-only snapshot of transit data, which is the one that will be handed off to any incoming routing requests. But when that single snapshot is updated, any requests that are currently executing will continue to hold their older snapshots. In this way there is always a single current snapshot, but an unlimited number of concurrently visible snapshots, each of which is being used by an unlimited number of concurrent routing requests. + +### Tolerance for Update Latency + +We don’t need every update for every individual trip to become visible to routing operations and end users independently of all other updates. We can batch updates together to a varying degree, trading off the number of separate snapshots present in memory at a given moment against the rapidity with which updates become visible to routing and to end users. Snapshots could be provided to the routing code on demand: if no requests are coming in, a series of individual updates will apply one after another to the same temporary buffer. As soon as a request comes in and needs a stable snapshot of the transit data to work with, the buffered set of transit data will be handed off to that request and subsequent updates applied to a new buffer in a copy-on-write fashion. However, a typical large system will require several load-balanced OTP instances that are essentially always busy, so such idle periods will rarely or never exist. Instead, we can create the same batching effect even with a continuous stream of incoming requests. For some period of time, typically a few seconds, all incoming requests will continue to be handed the last finalized snapshot and updates will accumulate to the new buffer. A new snapshot is created at regular intervals, independent of how many requests are arriving at the moment. This approach is also better for sparse reqeusts in that any pause or other overhead associated with snapshot creation is not incurred while the client is waiting, but rather proactively on its own thread. Clients always grab the latest available snapshot with no delay at all. + +### Derived Indexes + +In addition to the primary realtime and scheduled data, OTP maintains many derived indexes containing information that is implied by the primary data, but would be too slow to recompute every time it is needed. This includes for example which routes pass through each particular transit stop, or spatial indexes for fast lookups of which entities lie within bounding boxes or within a certain radius of a given point. As realtime data are applied, entities may be added or moved in ways that invalidate the contents of these derived indexes. + +Currently, there are many places in OTP where a single instance-wide index is maintained to be consistent with the latest realtime snapshot. When long-lived routing requests need to make use of these indexes during or at the end of the routing process, the index may have changed with respect to the unchanging snapshots the requests are working with. In fact, the indexes should be maintained using exactly the same strategy as the rest of the realtime transit data. They should first be managed with a copy-on-write strategy in a writable buffer that is only visible to the single-threaded writer actions, then transferred to an immutable snapshot that is handed off to the reading threads. + ## Full Dataset versus Incremental Messages The GTFS-RT specification includes an "incrementality" field. The specification says this field is unsupported and its behavior is undefined, but in practice people have been using this field since around 2013 in a fairly standardized way. An effort is underway to document its usage and update the standard (see https://github.com/google/transit/issues/84). From 94fea1d3e1da8174a8612d72a02ae25c01b536e8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Tue, 12 Mar 2024 22:14:54 +0800 Subject: [PATCH 12/24] More cleanup of all RT comments Uniformly add RT_AB to all TODO and FIXME comments --- .../ext/siri/SiriAlertsUpdateHandler.java | 20 +++--- .../ext/siri/SiriTimetableSnapshotSource.java | 7 +- .../ext/siri/SiriTripPatternCache.java | 64 ++++++++++--------- .../updater/EstimatedTimetableSource.java | 6 +- .../ext/siri/updater/SiriSXUpdater.java | 50 +++++++++------ .../model/TimetableSnapshot.java | 24 +++---- .../routing/alertpatch/TransitAlert.java | 2 +- .../raptoradapter/transit/TransitLayer.java | 12 ++-- .../opentripplanner/routing/graph/Graph.java | 2 +- .../DelegatingTransitAlertServiceImpl.java | 4 +- .../routing/impl/TransitAlertServiceImpl.java | 13 ++-- .../routing/services/TransitAlertService.java | 2 +- .../services/notes/StreetNotesService.java | 8 +-- .../api/OtpServerRequestContext.java | 10 +-- .../framework/json/ParameterBuilder.java | 5 +- .../transit/model/network/TripPattern.java | 5 +- .../transit/service/TransitModel.java | 5 +- .../transit/service/TransitService.java | 2 +- .../updater/spi/GraphUpdater.java | 1 + .../updater/trip/TimetableSnapshotSource.java | 12 ++-- .../updater/trip/TripPatternCache.java | 12 ++-- 21 files changed, 145 insertions(+), 121 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java index 9568684105b..d4df702bc19 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java @@ -36,10 +36,10 @@ /** * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds. - * TODO REALTIME: The name should be clarified, as there is no such thing as "SIRI Alerts", and it + * TODO RT_AB: The name should be clarified, as there is no such thing as "SIRI Alerts", and it * is referencing the internal model concept of "Alerts" which are derived from GTFS terminology. - * NOTE this cannot handle situations where there are multiple feeds with different IDs (for now it - * may only work in single-feed regions). + * NOTE this cannot handle situations where incoming messages are being applied to multiple static + * feeds with different IDs (for now it may only work in single-feed regions). */ public class SiriAlertsUpdateHandler { @@ -48,12 +48,12 @@ public class SiriAlertsUpdateHandler { private final Set alerts = new HashSet<>(); private final TransitAlertService transitAlertService; - /** How long before the posted start of an event it should be displayed to users. */ + /** The alert should be displayed to users this long before the activePeriod begins. */ private final Duration earlyStart; /** * This takes the parts of the SIRI SX message saying which transit entities are affected and - * maps them to multiple OTP internal model entities. + * maps them to the corresponding OTP internal model entity or entities. */ private final AffectsMapper affectsMapper; @@ -129,9 +129,9 @@ public void update(ServiceDelivery delivery) { } /** - * FIXME REALTIME This does not just "handle" an alert, it builds an internal model Alert from + * FIXME RT_AB: This does not just "handle" an alert, it builds an internal model Alert from * an incoming SIRI situation exchange element. It is a mapper or factory. - * It may return null if all of header, description, and detail text are empty or missing in the + * It may return null if the header, description, and detail text are all empty or missing in the * SIRI message. In all other cases it will return a valid TransitAlert instance. */ private TransitAlert handleAlert(PtSituationElement situation) { @@ -213,9 +213,9 @@ private long getEpochSecond(ZonedDateTime startTime) { } /* - * Creates alert from PtSituation with all textual content. - * The feed scoped ID of this alert will be the single feed ID associated with this update handler - * and the situation number provided in the feed. + * Creates a builder for an internal model TransitAlert, including all textual content from the + * supplied SIRI PtSituation. The feed scoped ID of this TransitAlert will be the single feed ID + * associated with this update handler, plus the situation number provided in the SIRI feed. */ private TransitAlertBuilder createAlertWithTexts(PtSituationElement situation) { return TransitAlert diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 08752a50b89..d7b096adabe 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -133,12 +133,13 @@ public TimetableSnapshot getTimetableSnapshot() { /** * Method to apply a trip update list to the most recent version of the timetable snapshot. - * FIXME TripUpdate is the GTFS term, and these SIRI ETs are never converted into that same internal model. + * FIXME RT_AB: TripUpdate is the GTFS term, and these SIRI ETs are never converted into that + * same internal model. * * @param fullDataset true iff the list with updates represent all updates that are active right * now, i.e. all previous updates should be disregarded * @param updates SIRI VehicleMonitoringDeliveries that should be applied atomically - * FIXME aren't these ET deliveries, not VM? + * FIXME RT_AB: aren't these ET deliveries, not VM? */ public UpdateResult applyEstimatedTimetable( @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, @@ -376,7 +377,7 @@ private Result addTripToGraphAndBuffer(TripUpdate tr ); // Add new trip times to the buffer and return result with success or error. The update method - // will perform protective copies as needed whether TripPattern is from realtime data or not. + // will perform protective copies as needed, whether pattern is created by realtime data or not. var result = buffer.update(pattern, tripUpdate.tripTimes(), serviceDate); LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); return result; diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 497aadf0c9a..a803764c2c4 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -26,34 +26,39 @@ * the same sequence of stops, they will all end up on this same TripPattern. *

* Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. - * See additional comments in the Javadoc of the GTFS-RT version of this class. + * See additional comments in the Javadoc of the GTFS-RT version of this class, whose name is + * simply TripPatternCache. + * TODO RT_AB: To the extent that double SIRI/GTFS implementations are kept, prefix all names + * with GTFS or SIRI or NETEX rather than having no prefix on the GTFS versions. */ public class SiriTripPatternCache { private static final Logger LOG = LoggerFactory.getLogger(SiriTripPatternCache.class); - // Seems to be the primary collection of added TripPatterns, with other collections serving as - // indexes. Similar to TripPatternCache.cache but with service date as part of the key. + // TODO RT_AB: Improve documentation. This seems to be the primary collection of added + // TripPatterns, with other collections serving as indexes. Similar to TripPatternCache.cache + // in the GTFS version of this class, but with service date as part of the key. private final Map cache = new HashMap<>(); - // Apparently a SIRI-specific index for use in GraphQL APIs (missing on GTFS-RT version). + // This appears to be a SIRI-specific index for use in GraphQL APIs. + // It is not present on the GTFS-RT version of this class. private final ListMultimap patternsForStop = Multimaps.synchronizedListMultimap( ArrayListMultimap.create() ); - // TODO clarify name and add documentation to this field + // TODO RT_AB: clarify name and add documentation to this field. private final Map updatedTripPatternsForTripCache = new HashMap<>(); - // TODO generalize this so we can generate IDs for SIRI or GTFS-RT sources + // TODO RT_AB: generalize this so we can generate IDs for SIRI or GTFS-RT sources. private final SiriTripPatternIdGenerator tripPatternIdGenerator; - // TODO clarify name and add documentation to this field, and why it's constructor injected + // TODO RT_AB: clarify name and add documentation to this field, and why it's constructor injected private final Function getPatternForTrip; /** * Constructor. - * TODO: clarify why the ID generator and pattern fetching function are injected. Potentially - * make the class usable for GTFS-RT cases by injecting different ID generator etc. + * TODO RT_AB: clarify why the ID generator and pattern fetching function are injected here. + * Potentially make the class usable for GTFS-RT cases by injecting different ID generator etc. */ public SiriTripPatternCache( SiriTripPatternIdGenerator tripPatternIdGenerator, @@ -63,16 +68,17 @@ public SiriTripPatternCache( this.getPatternForTrip = getPatternForTrip; } - // Below was clearly derived from a method from TripPatternCache, down to the obsolete Javadoc - // mentioning transit vertices and edges (which don't exist since raptor was adopted). - // Note that this is the only non-dead-code public method on this class, and mirrors the only - // public method on the GTFS-RT version of TripPatternCache. - // It also explains why this class is called a "cache". It allows reusing the same TripPattern - // instance when many different trips are created or updated with the same pattern. + // /** * Get cached trip pattern or create one if it doesn't exist yet. * + * TODO RT_AB: Improve documentation and/or merge with GTFS version of this class. + * This was clearly derived from a method from TripPatternCache. This is the only non-dead-code + * public method on this class, and mirrors the only public method on the GTFS-RT version of + * TripPatternCache. It also explains why this class is called a "cache". It allows reusing the + * same TripPattern instance when many different trips are created or updated with the same pattern. + * * @param stopPattern stop pattern to retrieve/create trip pattern * @param trip Trip containing route of new trip pattern in case a new trip pattern will be * created @@ -85,9 +91,9 @@ public synchronized TripPattern getOrCreateTripPattern( ) { TripPattern originalTripPattern = getPatternForTrip.apply(trip); - // TODO: verify, this is different than GTFS-RT version - // It can return a TripPattern from the scheduled data, but protective copies are handled - // in TimetableSnapshot.update. Document better this aspect of the contract in this method's Javadoc. + // TODO RT_AB: Verify implementation, which is different than the GTFS-RT version. + // It can return a TripPattern from the scheduled data, but protective copies are handled in + // TimetableSnapshot.update. Better document this aspect of the contract in this method's Javadoc. if (originalTripPattern.getStopPattern().equals(stopPattern)) { return originalTripPattern; } @@ -109,7 +115,7 @@ public synchronized TripPattern getOrCreateTripPattern( .withCreatedByRealtimeUpdater(true) .withOriginalTripPattern(originalTripPattern) .build(); - // TODO - SIRI: Add pattern to transitModel index? + // TODO: Add pattern to transitModel index? // Add pattern to cache cache.put(key, tripPattern); @@ -147,8 +153,8 @@ Therefore, we must clean up the duplicates by deleting the previously added (and outdated) tripPattern for all affected stops. In example above, "StopPattern #rt1" should be removed from all stops. - TODO explore why this particular case is handled in an ad-hoc manner. It seems like all such - indexes should be constantly rebuilt and versioned along with the TimetableSnapshot. + TODO RT_AB: review why this particular case is handled in an ad-hoc manner. It seems like all + such indexes should be constantly rebuilt and versioned along with the TimetableSnapshot. */ TripServiceDateKey tripServiceDateKey = new TripServiceDateKey(trip, serviceDate); if (updatedTripPatternsForTripCache.containsKey(tripServiceDateKey)) { @@ -186,7 +192,7 @@ Therefore, we must clean up the duplicates by deleting the previously added (and /** * Returns any new TripPatterns added by real time information for a given stop. - * TODO: this appears to be currently unused. Perhaps remove it if the API has changed. + * TODO RT_AB: this appears to be currently unused. Perhaps remove it if the API has changed. * * @param stop the stop * @return list of TripPatterns created by real time sources for the stop. @@ -196,15 +202,15 @@ public List getAddedTripPatternsForStop(RegularStop stop) { } } -//// Below here are multiple additional private classes defined in the same top-level class file. -//// TODO: move these private classes inside the above class as private static inner classes. +// TODO RT_AB: move the below classes inside the above class as private static inner classes. +// Defining these additional classes in the same top-level class file is unconventional. /** - * Serves as the key for the collection of realtime-added TripPatterns. + * Serves as the key for the collection of TripPatterns added by realtime messages. * Must define hashcode and equals to confer semantic identity. - * It seems like there's a separate TripPattern instance for each StopPattern and service date, - * rather a single TripPattern instance associated with a separate timetable for each date. - * TODO: clarify why each date has a different TripPattern instead of a different Timetable. + * TODO RT_AB: clarify why each date has a different TripPattern instead of a different Timetable. + * It seems like there's a separate TripPattern instance for each StopPattern and service date, + * rather a single TripPattern instance associated with a separate timetable for each date. */ class StopPatternServiceDateKey { @@ -234,7 +240,7 @@ public boolean equals(Object thatObject) { /** * An alternative key for looking up realtime-added TripPatterns by trip and service date instead * of stop pattern and service date. Must define hashcode and equals to confer semantic identity. - * TODO verify whether one map is considered the definitive collection and the other an index. + * TODO RT_AB: verify whether one map is considered the definitive collection and the other an index. */ class TripServiceDateKey { diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java index 3c5a387ca09..83edebe3911 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java @@ -4,9 +4,9 @@ import uk.org.siri.siri20.Siri; /** - * Interface for a blocking, polling approach - * TODO should the methods return as fast as possible? - * Or do they intentionally wait for refreshed data? + * Interface for a blocking, polling approach to retrieving SIRI realtime timetable updates. + * TODO RT_AB: Clearly document whether the methods should return as fast as possible, or if they + * should intentionally block and wait for refreshed data, and how this fits into the design. */ public interface EstimatedTimetableSource { /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java index 817505d81c0..77ccddfc736 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java @@ -31,7 +31,9 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr private final String url; private final String originalRequestorRef; private final TransitAlertService transitAlertService; - // TODO What is this, why does it exist as a persistent instance? + + // TODO RT_AB: Document why SiriAlertsUpdateHandler is a separate instance that persists across + // many graph update operations. private final SiriAlertsUpdateHandler updateHandler; private WriteToGraphCallback saveResultOnGraph; private ZonedDateTime lastTimestamp = ZonedDateTime.now().minusWeeks(1); @@ -86,7 +88,7 @@ public SiriSXUpdater(SiriSXUpdaterParameters config, TransitModel transitModel) @Override public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - // TODO REALTIME this callback should have a different name, it is currently too verb-like. + // TODO RT_AB: Consider renaming this callback. Its name is currently like an imperative verb. this.saveResultOnGraph = saveResultOnGraph; } @@ -104,7 +106,8 @@ protected void runPolling() throws InterruptedException { } /** - * This part has been factored out to allow repeated retries in case the connection fails etc. + * This part of the update process has been factored out to allow repeated retries of the HTTP + * fetching operation in case the connection fails or some other disruption happens. */ private void updateSiri() { boolean moreData = false; @@ -117,27 +120,36 @@ private void updateSiri() { // primitive, because the object moreData persists across iterations. final boolean markPrimed = !moreData; if (serviceDelivery.getSituationExchangeDeliveries() != null) { - // FIXME REALTIME This is submitting a method on a long-lived instance as a runnable. - // These runnables were intended to be small, disposable self-contained update tasks. + // FIXME RT_AB: This is submitting a reference to a method on a long-lived instance as a + // GraphWriterRunnable. These runnables were originally intended to be small, + // self-contained, throw-away update tasks. // See org/opentripplanner/updater/trip/PollingTripUpdater.java:90 - // Clarify why that is passing in so many other references. It should only contain - // what's needed to operate on the graph. This should be illustrated in documentation - // as a little box labeled "change trip ABC123 by making stop 53 late by 2 minutes." - // Also clarify how this works without even using the supplied graph or TransitModel: - // there are multiple TransitAlertServices and they are not versioned along with the\ - // Graph, they are attached to updaters. + // Clarify why the long-lived instance is capturing and holding so many references. + // The runnable should only contain the minimum needed to operate on the graph. + // Such runnables should be illustrated in documentation as e.g. a little box labeled + // "change trip ABC123 by making stop 53 late by 2 minutes." + // Also clarify how this runnable works without even using the supplied + // (graph, transitModel) parameters. There are multiple TransitAlertServices and they + // are not versioned along with the Graph, they are attached to updaters. + // // This is submitting a runnable to an executor, but that runnable only writes back to - // objects owned by this updater itself with no versioning. Why is this happening? + // objects referenced by updateHandler itself, rather than the graph or transitModel + // supplied for writing, and apparently with no versioning. This seems like a + // misinterpretation of the realtime design. // If this is an intentional choice to live-patch a single server-wide instance of an // alerts service/index while it's already in use by routing, we should be clear about // this and document why it differs from the graph-writer design. Currently the code - // seems to go through the a ritual of following the threadsafe copy-on-write pattern - // without actually doing so. - // It's understandable to defer the list-of-alerts processing to another thread than this - // fetching thread, but I don't think we want that happening on the graph writer thread. - // There seems to be a misunderstanding that the tasks are submitted to get them off the - // updater thread, but the real reason is to ensure consistent transactions in graph - // writing and reading. + // seems to follow some surface conventions of the threadsafe copy-on-write pattern + // without actually providing threadsafe behavior. + // It's a reasonable choice to defer processing the list of alerts to another thread than + // this fetching thread, but we probably don't want to defer any such processing to the + // graph writer thread, as that's explicitly restricted to be one single shared thread for + // the entire application. There seems to be a misunderstanding that the tasks are + // submitted to get them off the updater thread, but the real reason is to ensure + // consistent transactions in graph writing and reading. + // All that said, out of all the update types, Alerts (and SIRI SX) are probably the ones + // that would be most tolerant of non-versioned application-wide storage since they don't + // participate in routing and are tacked on to already-completed routing responses. saveResultOnGraph.execute((graph, transitModel) -> { updateHandler.update(serviceDelivery); if (markPrimed) { diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index dfb1975d818..a176debbe13 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -47,9 +47,9 @@ * departure times of other trips that have not necessarily been boarded. *

* A TimetableSnapshot instance may only be modified by a single thread. This makes it easier to - * reason about how the snapshot is built up and used. Write operation are applied one by one in - * order with no concurrent access, then read operations are allowed concurrently by many threads - * once writing is forbidden. + * reason about how the snapshot is built up and used. Write operations are applied one by one, in + * order, with no concurrent access. Read operations are then allowed concurrently by many threads + * after writing is forbidden. *

* The fact that TripPattern instances carry a reference only to their scheduled Timetable and not * to their realtime timetable is largely due to historical path-dependence in OTP development. @@ -97,22 +97,22 @@ public class TimetableSnapshot { * update, a Map associating the updated trip pattern with a compound key of the feed-scoped * trip ID and the service date. The type of this field is HashMap rather than the more general * Map interface because we need to efficiently clone it whenever we start building up a new - * snapshot. TODO: clarify if this is an index or the original source of truth. + * snapshot. TODO RT_AB: clarify if this is an index or the original source of truth. */ private HashMap realtimeAddedTripPattern = new HashMap<>(); /** - * This is an index of TripPatterns, not the primary collection. - * It tracks which TripPatterns that were updated or newly created by realtime messages contain - * which stops. This allows them to be readily found and included in API responses containing - * stop times at a specific stop. This is a SetMultimap, so that each pattern is only retained - * once per stop even if it's added more than once. - * TODO: Better, more general handling of all realtime indexes outside primary data structures. + * This is an index of TripPatterns, not the primary collection. It tracks which TripPatterns + * that were updated or newly created by realtime messages contain which stops. This allows them + * to be readily found and included in API responses containing stop times at a specific stop. + * This is a SetMultimap, so that each pattern is only retained once per stop even if it's added + * more than once. + * TODO RT_AB: More general handling of all realtime indexes outside primary data structures. */ private SetMultimap patternsForStop = HashMultimap.create(); /** - * This is an as-yet unused alternative to the current boolean fields readOnly and dirty, as well + * This is an AS YET UNUSED alternative to the current boolean fields readOnly and dirty, as well * as setting dirtyTimetables to null. A given instance of TimetableSnapshot should progress * through all these states in order, and cannot return to a previous state. */ @@ -129,7 +129,7 @@ private enum TimetableSnapshotState { * other hand, reading is expected to be highly concurrent and happens during core routing * processes. Therefore, any assertions about state should be concentrated in the writing methods. */ - private TimetableSnapshotState state; + // private TimetableSnapshotState state; /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java index 0d188df5ef4..4a7ef91a8ea 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java @@ -21,7 +21,7 @@ * of the transit system which will be displayed to users as text. * Although they have flags describing the effect of the problem described in the text, convention * is that these messages do not modify routing behavior on their own. They must be accompanied by - * other messages such as + * messages of other types to actually impact routing. */ public class TransitAlert extends AbstractTransitEntity { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index 4188d5e569b..6757dbccae0 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -19,13 +19,13 @@ /** * This is a replica of public transportation data already present in TransitModel, but rearranged - * and indexed differently for efficient use by the Raptor router. - * Patterns and trips are split out by days, retaining only the services actually running on any - * particular day. + * and indexed differently for efficient use by the Raptor router. Patterns and trips are split out + * by days, retaining only the services actually running on any particular day. * - * TODO rename - this name appears to be modeled after R5, where the TransportNetwork is split into - * two layers: one for the streets and one for the public transit data. But here, this seems to be - * an indexed and rearranged copy of the main transit data collected under a TransitModel instance. + * TODO RT_AB: this name is apparently modeled after R5, where the TransportNetwork is split into + * two layers (one for the streets and one for the public transit data). Here the situation is + * different: this seems to be an indexed and rearranged copy of the main transit data (a + * TransitModel instance). */ public class TransitLayer { diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 42909952178..3e47046aab9 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -59,7 +59,7 @@ * In some sense the Graph is just some indexes into a set of vertices. The Graph used to hold lists * of edges for each vertex, but those lists are now attached to the vertices themselves. *

- * TODO rename to StreetGraph to emphasize what it represents? + * TODO RT_AB: rename to StreetGraph to emphasize what it represents? */ public class Graph implements Serializable { diff --git a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java index cfa92ffaff3..8da6fc21aa7 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java @@ -20,8 +20,8 @@ * all alerts. * * Concretely: every realtime updater receiving GTFS Alerts or SIRI Situation Exchange (SX) - * messages currently maintains its own private index of alerts seperate from all other updaters. - * To make the set of all alerts from all updaters available in a single operaion and associate it + * messages currently maintains its own private index of alerts separately from all other updaters. + * To make the set of all alerts from all updaters available in a single operation and associate it * with the graph as a whole, the various indexes are merged in such a way as to have the same * index as each individual index. */ diff --git a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java index b63041d66e7..0cc24c691e9 100644 --- a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java @@ -20,7 +20,7 @@ * of TransitAlerts and indexes them for fast lookup by which transit entity is affected. * The only other implementation exists just to combine several instances of this primary * implementation into one. - * TODO REALTIME investigate why each updater has its own service instead of taking turns + * TODO RT_AB: investigate why each updater has its own service instead of taking turns * sequentially writing to a single service. Original design was for all data and indexes to be * associated with the Graph or transit model (i.e. the object graph of instances of the transit * model) and for updaters to submit write tasks that would patch the current version in a @@ -42,18 +42,17 @@ public TransitAlertServiceImpl(TransitModel transitModel) { @Override public void setAlerts(Collection alerts) { - // NOTE this is being patched live by updaters while in use (being read) by other threads - // performing trip planning. The single-action assignment helps a bit, but the map can be - // swapped out while the delegating service is in the middle of multiple calls that read from it. - // The consistent approach would be to duplicate the entire service, update it copy-on write, - // and swap in the entire service after the update. + // FIXME RT_AB: this is patched live by updaters while in use (being read) by other threads + // performing trip planning. The single-action assignment helps a bit, but the map can be + // swapped out while the delegating service is in the middle of multiple calls that read from + // it. The consistent approach would be to duplicate the entire service, update it + // copy-on-write, and swap in the entire service after the update. Multimap newAlerts = HashMultimap.create(); for (TransitAlert alert : alerts) { for (EntitySelector entity : alert.entities()) { newAlerts.put(entity.key(), alert); } } - this.alerts = newAlerts; } diff --git a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java index 6dd1423ea8c..7b037192c77 100644 --- a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java +++ b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java @@ -9,7 +9,7 @@ import org.opentripplanner.transit.model.timetable.Direction; /** - * The TransitAlertService stores a set of alerts (passenger-facing textual information associated + * A TransitAlertService stores a set of alerts (passenger-facing textual information associated * with transit entities such as stops or routes) which are currently active and should be provided * to end users when their itineraries include the relevant stop, route, etc. * diff --git a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java index 92d73574530..966ac72c7d8 100644 --- a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java +++ b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java @@ -16,10 +16,10 @@ /** * This service manages street edge notes. An edge note is a free-format alert (text) attached to an - * edge, which is returned along with any itinerary where this edge is used, and which does not have any - * impact on routing. Notes cannot affect routing because edges do not know which notes are - * attached to them. This avoids storing references to notes on the edge, which would probably not - * be worth the memory consumption as only few edges have notes. + * edge, which is returned along with any itinerary where this edge is used, and which does not + * have any impact on routing. Notes cannot affect routing because edges do not know which notes + * are attached to them. This avoids storing references to notes on the edge, which would probably + * not be worth the memory consumption as only a few edges have notes. *

* The service owns a list of StreetNotesSource, with a single static one used for graph building. * "Dynamic" notes can be returned by classes implementing StreetNoteSource, added to this service diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 98377eb285d..d1d565eeb0d 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -31,15 +31,15 @@ /** * The purpose of this class is to allow APIs (HTTP Resources) to access the OTP Server Context. - * FIXME circular definition (OtpServerRequestContext provides access to OTP Server Context). - * What exactly does "Server Context" mean here? What does "access" mean? Does this mean it - * "allows individual requests to call a limited number of methods on the components of the server - * without direct access to their internals?" + * FIXME RT_AB: Expand and clarify descriptions, which are circular (OtpServerRequestContext + * provides access to OTP Server Context). What exactly does "Server Context" mean here? + * What does "access" mean? Does this mean it "allows individual requests to call a limited + * number of methods on the components of the server without direct access to their internals?" * By using an interface, and not injecting each service class we avoid giving the resources access * to the server implementation. The context is injected by Jersey. An alternative to injecting this * interface is to inject each individual component in the context - hence reducing the dependencies * further. - * TODO clarify how injecting more individual components would "reduce dependencies further". + * TODO RT_AB: clarify how injecting more individual components would "reduce dependencies further". * But there is not a "real" need for this. For example, we do not have unit tests on the * Resources. If we in the future would decide to write unit tests for the APIs, then we could * eliminate this interface and just inject the components. See the bind method in OTPServer. diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index cdf386b8cbb..ee68159cdff 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -43,8 +43,9 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; /** - * TODO clarify whether this is building a declarative representation of the parameter, or building - * a concrete key-value pair for a parameter in a config file being read at server startup, or both. + * TODO RT_AB: add Javadoc to clarify whether this is building a declarative representation of the + * parameter, or building a concrete key-value pair for a parameter in a config file being read + * at server startup, or both. */ public class ParameterBuilder { diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 13b600a26eb..2d64160de2d 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -80,7 +80,8 @@ public final class TripPattern // This TransitMode is a redundant replication/memoization of information on the Route. // It appears that in the TripPatternBuilder it is only ever set from a Trip which is itself set - // from a Route. TODO confirm whether there is any reason this doesn't just read through to Route. + // from a Route. + // TODO RT_AB: confirm whether there is any reason this doesn't just read through to Route. private final TransitMode mode; private final SubMode netexSubMode; @@ -96,7 +97,7 @@ public final class TripPattern * Currently this seems to only be set (via TripPatternBuilder) from TripPatternCache and * SiriTripPatternCache. * - * FIXME this is only used rarely, make that obvious from comments. + * FIXME RT_AB: Revise comments to make it clear how this is used (it is only used rarely). */ private final TripPattern originalTripPattern; diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index 1f90764dda5..3d2f2d5424e 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -72,8 +72,9 @@ * TransitLayer rather than the TransitModel it's derived from. Both are often accessed via the * TransitService rather than directly reading the fields of TransitModel or TransitLayer. * - * TODO rename, as this is not really the model, but a top-level object grouping together instances - * of model classes with things that operate on and map those instances. + * TODO RT_AB: consider renaming. By some definitions this is not really the model, but a top-level + * object grouping together instances of model classes with things that operate on and map those + * instances. */ public class TransitModel implements Serializable { diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index b3b80cf6742..83b65c44d12 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -50,7 +50,7 @@ * fetching tables of specific information like the routes passing through a particular stop, or for * gaining access to the entirety of the data to perform routing. *

- * TODO this interface seems to provide direct access to the TransitLayer but not the TransitModel. + * TODO RT_AB: this interface seems to provide direct access to TransitLayer but not TransitModel. * Is this intentional, because TransitLayer is meant to be read-only and TransitModel is not? * Should this be renamed TransitDataService since it seems to provide access to the data but * not to transit routing functionality (which is provided by the RoutingService)? diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index d8edc651e7e..e0eed54a6de 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -55,6 +55,7 @@ default boolean isPrimed() { * A GraphUpdater implementation uses this method to report its corresponding value of the "type" * field in the configuration file. This value should ONLY be used when providing human-friendly * messages while logging and debugging. Association of configuration to particular types is + * performed by the UpdatersConfig.Type constructor calling factory methods. */ String getConfigRef(); } diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 2a95fb47e56..9cc0cab52a9 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -89,10 +89,12 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { * The working copy of the timetable snapshot. Should not be visible to routing threads. Should * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that * might modify this buffer will correctly acquire the lock. By design, only one thread should - * ever be writing to this buffer. But we need to suspend writes while we're indexing and swapping - * out the buffer (Or do we? Can't we just make a new copy of the buffer first?) - * TODO: research why this lock is needed since only one thread should ever be writing to this buffer. - * Instead we should throw an exception if a writing section is entered by more than one thread. + * ever be writing to this buffer. + * TODO RT_AB: research and document why this lock is needed since only one thread should ever be + * writing to this buffer. One possible reason may be a need to suspend writes while indexing + * and swapping out the buffer. But the original idea was to make a new copy of the buffer + * before re-indexing it. While refactoring or rewriting parts of this system, we could throw + * an exception if a writing section is entered by more than one thread. */ private final TimetableSnapshot buffer = new TimetableSnapshot(); @@ -121,7 +123,7 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { /** * Should expired real-time data be purged from the graph. - * TODO clarify exactly what this means? In what circumstances would you want to turn it off? + * TODO RT_AB: Clarify exactly what "purge" means and in what circumstances would one turn it off. */ private final boolean purgeExpiredData; diff --git a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java index f43bf5e3b99..bf8e75418ae 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java +++ b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java @@ -20,11 +20,11 @@ * the same sequence of stops, they will all end up on this same TripPattern. *

* Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. - * TODO: consolidate TripPatternCache and SiriTripPatternCache. They seem to only be separate - * because SIRI- or GTFS-specific indexes of the added TripPatterns seem to have been added to - * this primary collection. - * FIXME: the name does not make it clear that this has anything to do with elements that are only - * added due to realtime updates, and it is only loosely a cache. RealtimeAddedTripPatterns? + * TODO RT_AB: consolidate TripPatternCache and SiriTripPatternCache. They seem to only be separate + * because SIRI- or GTFS-specific indexes of the added TripPatterns seem to have been added to + * this primary collection. + * FIXME RT_AB: the name does not make it clear that this has anything to do with elements that are + * only added due to realtime updates, and it is only loosely a cache. RealtimeAddedTripPatterns? */ public class TripPatternCache { @@ -86,7 +86,7 @@ public synchronized TripPattern getOrCreateTripPattern( * implementation of TripPatternIdGenerator. * This method is not static because it references a monotonically increasing integer counter. * But like in SiriTripPatternIdGenerator, this could be encapsulated outside the cache object. - * TODO: create GtfsRtTripPatternIdGenerator as part of merging the two TripPatternCaches + * TODO RT_AB: create GtfsRtTripPatternIdGenerator as part of merging the two TripPatternCaches. */ private FeedScopedId generateUniqueTripPatternCode(Trip trip) { FeedScopedId routeId = trip.getRoute().getId(); From a92917bbcd9c795784ddfca2d305b670bf898ef3 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Thu, 14 Mar 2024 20:17:38 +0800 Subject: [PATCH 13/24] remove draft code --- .../model/TimetableSnapshot.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index a176debbe13..cf254472a43 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -111,26 +111,6 @@ public class TimetableSnapshot { */ private SetMultimap patternsForStop = HashMultimap.create(); - /** - * This is an AS YET UNUSED alternative to the current boolean fields readOnly and dirty, as well - * as setting dirtyTimetables to null. A given instance of TimetableSnapshot should progress - * through all these states in order, and cannot return to a previous state. - */ - private enum TimetableSnapshotState { - WRITABLE_CLEAN, - WRITBLE_DIRTY, - INDEXING, - READ_ONLY, - } - - /** - * Which stage of existence this TimetableSnapshot is in, which determines whether it's read-only. - * Writing to TimetableSnapshots is not concurrent and does not happen in hot methods. On the - * other hand, reading is expected to be highly concurrent and happens during core routing - * processes. Therefore, any assertions about state should be concentrated in the writing methods. - */ - // private TimetableSnapshotState state; - /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. From d5dbf7949e8c0e73021faae9d16d2d36770bfacb Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sun, 31 Mar 2024 14:39:39 +0900 Subject: [PATCH 14/24] Update src/main/java/org/opentripplanner/routing/graph/Graph.java Co-authored-by: Leonard Ehrenfried --- src/main/java/org/opentripplanner/routing/graph/Graph.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 3e47046aab9..ba83ed6886d 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -54,7 +54,7 @@ * Other data structures related to street routing, such as elevation data and vehicle parking * information, are also collected here as fields of the Graph. For historical reasons the Graph * sometimes serves as a catch-all, as it used to be the root of the object tree representing the - * whole transportation network. + * whole transportation network. This use of the Graph object is being phased out and discouraged. *

* In some sense the Graph is just some indexes into a set of vertices. The Graph used to hold lists * of edges for each vertex, but those lists are now attached to the vertices themselves. From 50bd2520292a396d1165ee9aec4726b6d74b80d0 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sun, 31 Mar 2024 15:05:13 +0900 Subject: [PATCH 15/24] update docs in response to PR comments --- .../opentripplanner/ext/siri/SiriTripPatternCache.java | 9 ++++++--- .../opentripplanner/routing/alertpatch/TransitAlert.java | 7 ++++--- .../transit/model/network/StopPattern.java | 3 ++- .../opentripplanner/transit/service/TransitModel.java | 4 +++- src/main/java/org/opentripplanner/updater/package.md | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index a803764c2c4..146fc2ade1e 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -52,13 +52,16 @@ public class SiriTripPatternCache { // TODO RT_AB: generalize this so we can generate IDs for SIRI or GTFS-RT sources. private final SiriTripPatternIdGenerator tripPatternIdGenerator; - // TODO RT_AB: clarify name and add documentation to this field, and why it's constructor injected + /** + * SiriTripPatternCache needs only this one feature of TransitService, so we retain only this + * function reference to effectively narrow the interface. This should also facilitate testing. + */ private final Function getPatternForTrip; /** * Constructor. - * TODO RT_AB: clarify why the ID generator and pattern fetching function are injected here. - * Potentially make the class usable for GTFS-RT cases by injecting different ID generator etc. + * TODO RT_AB: This class could potentially be reused for both SIRI and GTFS-RT, which may + * involve injecting a different ID generator and pattern fetching method. */ public SiriTripPatternCache( SiriTripPatternIdGenerator tripPatternIdGenerator, diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java index 4a7ef91a8ea..59ad16cda6a 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java @@ -19,9 +19,10 @@ * Internal representation of a GTFS-RT Service Alert or SIRI Situation Exchange (SX) message. * These are text descriptions of problems affecting specific stops, routes, or other components * of the transit system which will be displayed to users as text. - * Although they have flags describing the effect of the problem described in the text, convention - * is that these messages do not modify routing behavior on their own. They must be accompanied by - * messages of other types to actually impact routing. + * Although they have flags describing the effect of the problem described in the text, these + * messages do not currently modify routing behavior on their own. They must be accompanied by + * messages of other types to actually impact routing. However, there is ongoing discussion about + * allowing Alerts to affect routing, especially for cases such as stop closure messages. */ public class TransitAlert extends AbstractTransitEntity { diff --git a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index 6aad00b659b..21b911f500a 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -94,7 +94,8 @@ StopPatternBuilder mutate() { return new StopPatternBuilder(this, null); } - // TODO RT_AB: documentation or naming - this does not mutate the object in place, it makes a copy + // TODO RT_AB: Rename and add documentation. This method does not mutate the object in place, it + // makes a copy. Confirmed in discussion that this should have a different name like "copy". StopPatternBuilder mutate(StopPattern realTime) { return new StopPatternBuilder(this, realTime); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index 3d2f2d5424e..da257821174 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -58,7 +58,9 @@ * The TransitModel groups together all instances making up OTP's primary internal representation * of the public transportation network. Although the names of many entities are derived from * GTFS concepts, these are actually independent of the data source from which they are loaded. - * Both GTFS and NeTEx entities are mapped to these same internal OTP entities. + * Both GTFS and NeTEx entities are mapped to these same internal OTP entities. If a concept exists + * in both GTFS and NeTEx, the GTFS name is used in the internal model. For concepts that exist + * only in NeTEx, the NeTEx name is used in the internal model. * * A TransitModel instance also includes references to some transient indexes of its contents, to * the TransitLayer derived from it, and to some other services and utilities that operate upon diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 0a1b6974a1e..1a7c79f52b6 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -28,7 +28,7 @@ The following is a sequence diagram showing how threads are intended to communic ![Realtime sequence diagram](images/updater-threads-queues.svg) -At the top of the diagram are the GraphUpdater implementations. These fall broadly into two categories: polling updaters and streaming updaters. Polling updaters periodically send a request to server (often just a simple HTTP server) which returns a file containing the latest version of the updates. Streaming updaters are generally built around libraries implementing message-oriented protocols such as AMQP or WebSockets, which fire a callback each time a new message is received. Polling updaters tend to return a full dataset describing the entire system state on each polling operation, while streaming updaters tend to receive incremental messages targeting individual transit trips. As such, polling updaters execute relatively infrequently (perhaps every minute or two) and process large responses, while streaming updaters execute very frequently (often many times per second) and operate on small messages in short bursts. Polling updaters are simpler in many ways and make use of common HTTP server components, but they introduce significant latency and redundant communication. Streaming updaters require more purpose-built or custom-configured components including message brokers, but bandwidth consumption and latency are lower, allowing routing results to reflect vehicle delays and positions immediately after they're reported. +At the top of the diagram are the GraphUpdater implementations. These fall broadly into two categories: polling updaters and streaming updaters. Polling updaters periodically send a request to server (often just a simple HTTP server) which returns a file containing the latest version of the updates. Streaming updaters are generally built around libraries implementing message-oriented protocols such as MQTT or AMQP, which fire a callback each time a new message is received. Polling updaters tend to return a full dataset describing the entire system state on each polling operation, while streaming updaters tend to receive incremental messages targeting individual transit trips. As such, polling updaters execute relatively infrequently (perhaps every minute or two) and process large responses, while streaming updaters execute very frequently (often many times per second) and operate on small messages in short bursts. Polling updaters are simpler in many ways and make use of common HTTP server components, but they introduce significant latency and redundant communication. Streaming updaters require more purpose-built or custom-configured components including message brokers, but bandwidth consumption and latency are lower, allowing routing results to reflect vehicle delays and positions immediately after they're reported. The GraphUpdaterManager coordinates all these updaters, and each runs freely in its own thread, receiving, deserializing, and validating data on its own schedule. Importantly, the GraphUpdaters are _not allowed to directly modify the transit data (Graph)_. Instead, they submit instances of GraphWriterRunnable which are queued up using the WriteToGraphCallback interface. These instances are essentially deferred code snippets that _are allowed_ to write to the Graph, but in a very controlled way. In short, there is exactly one thread that is allowed to make changes to the transit data, and those changes are queued up and executed in sequence, one at a time. From 74430b0ef4c7e593bc32f62838478561b2640f1d Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 3 May 2024 18:35:16 +0800 Subject: [PATCH 16/24] Apply suggestions from code review Co-authored-by: Thomas Gran --- .../opentripplanner/ext/siri/SiriTripPatternCache.java | 8 ++++++-- .../ext/siri/SiriTripPatternIdGenerator.java | 2 ++ .../transit/model/network/TripPattern.java | 8 ++++++-- .../org/opentripplanner/updater/spi/GraphUpdater.java | 9 ++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 146fc2ade1e..695db21f467 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -30,6 +30,12 @@ * simply TripPatternCache. * TODO RT_AB: To the extent that double SIRI/GTFS implementations are kept, prefix all names * with GTFS or SIRI or NETEX rather than having no prefix on the GTFS versions. + * TODO RT_TG: There is no clear strategy for what should be in the cache and the transit model and the flow + * between them. The NeTEx and a GTFS version of this should be merged. Having NeTex and GTFS + * specific indexes inside is ok. With the increased usage of DatedServiceJourneys, this should probably + * be part of the main model - not a separate cashe. It is possible that this class works when it comes to + * the thread-safety, but just by looking at a few lines of code I see problems - a strategy needs to be + * analysed, designed and documented. */ public class SiriTripPatternCache { @@ -71,8 +77,6 @@ public SiriTripPatternCache( this.getPatternForTrip = getPatternForTrip; } - // - /** * Get cached trip pattern or create one if it doesn't exist yet. * diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java index e0882bd6215..d5a98088746 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java @@ -12,6 +12,8 @@ * from the SIRI updaters. It is important to create only one instance of this class, and inject * that single instance wherever it is needed. The ID generation is threadsafe, even if that is * probably not needed. + * TODO RT: To make this simpler to use we could make it a "Singelton" (static getInstance() method) - that would + * enforce one instance only, and simplify injection (use getInstance() where needed). */ class SiriTripPatternIdGenerator { diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 2d64160de2d..a4a6330e895 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -58,7 +58,7 @@ public final class TripPattern private final Route route; /** - * The Route and StopPattern together form the primary key of the TripPattern. They are the shared + * The Route and StopPattern together form the natural key of the TripPattern. They are the shared * set of characteristics that group many trips together into one TripPattern. This grouping saves * memory by not replicating any details shared across all trips in the TripPattern, but it is * also essential to some optimizations in routing algorithms like Raptor. @@ -88,7 +88,11 @@ public final class TripPattern private final boolean containsMultipleModes; private String name; - /** Geometries of each inter-stop segment of the tripPattern. */ + /** + * Geometries of each inter-stop segment of the tripPattern. + * Not used in routing, only for API listing. + * TODO: Encapsulate the byte arrays in a class. + */ private final byte[][] hopGeometries; /** diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index e0eed54a6de..9682d560334 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -3,13 +3,16 @@ /** * Interface for classes that fetch or receive information while the OTP instance is running and * make changes to the Graph and associated transit data to reflect the current situation. This is - * typically information about disruptions to service, bicycle or parking availability, etc.

+ * typically information about disruptions to service, bicycle or parking availability, etc. + *

* Each GraphUpdater implementation will be run in a separate thread, allowing it to make blocking * calls to fetch data or even sleep between periodic polling operations without affecting the rest - * of the OTP instance.

+ * of the OTP instance. + *

* GraphUpdater implementations are instantiated by UpdaterConfigurator. Each updater configuration * item in the router-config for a ThingUpdater is mapped to a corresponding configuration class - * ThingUpdaterParameters, which is passed to the ThingUpdater constructor.

+ * ThingUpdaterParameters, which is passed to the ThingUpdater constructor. + *

* GraphUpdater implementations are only allowed to make changes to the Graph and related structures * by submitting instances implementing GraphWriterRunnable (often anonymous functions) to the * Graph writing callback function supplied to them by the GraphUpdaterManager after they're From 31dc11041b941ca9f8feb94cf4cce2cad55daae3 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 4 May 2024 01:08:05 +0800 Subject: [PATCH 17/24] updates in response to PR review --- .../ext/siri/SiriAlertsUpdateHandler.java | 34 ++++++++++++------- .../ext/siri/SiriTimetableSnapshotSource.java | 7 ++-- .../ext/siri/SiriTripPatternCache.java | 5 +-- .../updater/SiriETGooglePubsubUpdater.java | 4 +-- .../ext/siri/updater/SiriETUpdater.java | 4 +-- .../ext/siri/updater/SiriSXUpdater.java | 9 +++-- .../azure/AbstractAzureSiriUpdater.java | 4 +-- .../model/TimetableSnapshot.java | 7 ++-- .../raptoradapter/transit/TransitLayer.java | 9 ++--- .../opentripplanner/routing/graph/Graph.java | 2 +- .../api/OtpServerRequestContext.java | 16 ++++----- .../transit/model/network/StopPattern.java | 6 ++-- .../transit/model/network/TripPattern.java | 4 +-- .../updater/GraphUpdaterManager.java | 2 +- .../alert/GtfsRealtimeAlertsUpdater.java | 4 +-- .../updater/alert/TransitAlertProvider.java | 3 ++ .../updater/spi/GraphUpdater.java | 2 +- .../updater/trip/MqttGtfsRealtimeUpdater.java | 4 +-- .../updater/trip/PollingTripUpdater.java | 4 +-- .../VehicleParkingUpdater.java | 4 +-- .../PollingVehiclePositionUpdater.java | 4 +-- .../vehicle_rental/VehicleRentalUpdater.java | 4 +-- .../model/network/StopPatternTest.java | 2 +- 23 files changed, 74 insertions(+), 70 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java index d4df702bc19..8a747e765da 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java @@ -35,11 +35,18 @@ import uk.org.siri.siri20.WorkflowStatusEnumeration; /** - * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds. + * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange (SX) feeds. + * As the incoming SIRI SX messages are mapped to internal TransitAlerts, their FeedScopedIds will + * be the single feed ID associated with this update handler, plus the situation number provided in + * the SIRI SX message. + * This class cannot handle situations where incoming messages are being applied to multiple static + * feeds with different IDs. For now it may only work in single-feed regions. A possible workaround + * is to assign the same feed ID to multiple static feeds where it is known that their entity IDs + * are all drawn from the same namespace (i.e. they are functionally fragments of the same feed). + * TODO RT_AB: Internal FeedScopedId creation strategy should probably be pluggable or configurable. + * TG has indicated this is a necessary condition for moving this updater out of sandbox. * TODO RT_AB: The name should be clarified, as there is no such thing as "SIRI Alerts", and it * is referencing the internal model concept of "Alerts" which are derived from GTFS terminology. - * NOTE this cannot handle situations where incoming messages are being applied to multiple static - * feeds with different IDs (for now it may only work in single-feed regions). */ public class SiriAlertsUpdateHandler { @@ -47,8 +54,6 @@ public class SiriAlertsUpdateHandler { private final String feedId; private final Set alerts = new HashSet<>(); private final TransitAlertService transitAlertService; - - /** The alert should be displayed to users this long before the activePeriod begins. */ private final Duration earlyStart; /** @@ -57,6 +62,9 @@ public class SiriAlertsUpdateHandler { */ private final AffectsMapper affectsMapper; + /** + * @param earlyStart display the alerts to users this long before their activePeriod begins + */ public SiriAlertsUpdateHandler( String feedId, TransitModel transitModel, @@ -98,7 +106,7 @@ public void update(ServiceDelivery delivery) { } else { TransitAlert alert = null; try { - alert = handleAlert(sxElement); + alert = mapSituationToAlert(sxElement); addedCounter++; } catch (Exception e) { LOG.info( @@ -129,12 +137,11 @@ public void update(ServiceDelivery delivery) { } /** - * FIXME RT_AB: This does not just "handle" an alert, it builds an internal model Alert from - * an incoming SIRI situation exchange element. It is a mapper or factory. - * It may return null if the header, description, and detail text are all empty or missing in the + * Build an internal model Alert from an incoming SIRI situation exchange element. + * May return null if the header, description, and detail text are all empty or missing in the * SIRI message. In all other cases it will return a valid TransitAlert instance. */ - private TransitAlert handleAlert(PtSituationElement situation) { + private TransitAlert mapSituationToAlert(PtSituationElement situation) { TransitAlertBuilder alert = createAlertWithTexts(situation); if ( @@ -213,9 +220,10 @@ private long getEpochSecond(ZonedDateTime startTime) { } /* - * Creates a builder for an internal model TransitAlert, including all textual content from the - * supplied SIRI PtSituation. The feed scoped ID of this TransitAlert will be the single feed ID - * associated with this update handler, plus the situation number provided in the SIRI feed. + * Creates a builder for an internal model TransitAlert. The builder is pre-filled with all + * textual content from the supplied SIRI PtSituation. The builder also has the feed scoped ID + * pre-set to the single feed ID associated with this update handler, plus the situation number + * provided in the SIRI PtSituation. */ private TransitAlertBuilder createAlertWithTexts(PtSituationElement situation) { return TransitAlert diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index d7b096adabe..080a1535284 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -138,8 +138,7 @@ public TimetableSnapshot getTimetableSnapshot() { * * @param fullDataset true iff the list with updates represent all updates that are active right * now, i.e. all previous updates should be disregarded - * @param updates SIRI VehicleMonitoringDeliveries that should be applied atomically - * FIXME RT_AB: aren't these ET deliveries, not VM? + * @param updates SIRI EstimatedTimetable deliveries that should be applied atomically. */ public UpdateResult applyEstimatedTimetable( @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, @@ -375,9 +374,7 @@ private Result addTripToGraphAndBuffer(TripUpdate tr trip, serviceDate ); - - // Add new trip times to the buffer and return result with success or error. The update method - // will perform protective copies as needed, whether pattern is created by realtime data or not. + // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. var result = buffer.update(pattern, tripUpdate.tripTimes(), serviceDate); LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); return result; diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 695db21f467..5cce9031e1b 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -46,8 +46,9 @@ public class SiriTripPatternCache { // in the GTFS version of this class, but with service date as part of the key. private final Map cache = new HashMap<>(); - // This appears to be a SIRI-specific index for use in GraphQL APIs. - // It is not present on the GTFS-RT version of this class. + // TODO RT_AB: Improve documentation. This field appears to be an index that exists only in the + // SIRI version of this class (i.e. this version and not the older TripPatternCache that + // handles GTFS-RT). This index appears to be tailored for use by the Transmodel GraphQL APIs. private final ListMultimap patternsForStop = Multimaps.synchronizedListMultimap( ArrayListMultimap.create() ); diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java index 4b5797f03de..1e06d334e74 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java @@ -166,8 +166,8 @@ public SiriETGooglePubsubUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java index c8ccd2c533b..66007f4aed8 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java @@ -77,8 +77,8 @@ public SiriETUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java index 77ccddfc736..5ededbb3bf0 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java @@ -35,7 +35,7 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr // TODO RT_AB: Document why SiriAlertsUpdateHandler is a separate instance that persists across // many graph update operations. private final SiriAlertsUpdateHandler updateHandler; - private WriteToGraphCallback saveResultOnGraph; + private WriteToGraphCallback writeToGraphCallback; private ZonedDateTime lastTimestamp = ZonedDateTime.now().minusWeeks(1); private String requestorRef; /** @@ -87,9 +87,8 @@ public SiriSXUpdater(SiriSXUpdaterParameters config, TransitModel transitModel) } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - // TODO RT_AB: Consider renaming this callback. Its name is currently like an imperative verb. - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.writeToGraphCallback = writeToGraphCallback; } public TransitAlertService getTransitAlertService() { @@ -150,7 +149,7 @@ private void updateSiri() { // All that said, out of all the update types, Alerts (and SIRI SX) are probably the ones // that would be most tolerant of non-versioned application-wide storage since they don't // participate in routing and are tacked on to already-completed routing responses. - saveResultOnGraph.execute((graph, transitModel) -> { + writeToGraphCallback.execute((graph, transitModel) -> { updateHandler.update(serviceDelivery); if (markPrimed) { primed = true; diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java index f96941c7370..3023ff8359b 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java @@ -92,8 +92,8 @@ public AbstractAzureSiriUpdater(SiriAzureUpdaterParameters config, TransitModel protected abstract void errorConsumer(ServiceBusErrorContext errorContext); @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index cf254472a43..136d70fd7d9 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -196,11 +196,10 @@ public boolean hasRealtimeAddedTripPatterns() { /** * 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. + * 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. * - * @param pattern trip pattern - * @param updatedTripTimes updated trip times - * @param serviceDate service day for which this update is valid + * @param serviceDate service day for which this update is valid * @return whether the update was actually applied */ public Result update( diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index 6757dbccae0..e8dacee87a8 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -22,10 +22,11 @@ * and indexed differently for efficient use by the Raptor router. Patterns and trips are split out * by days, retaining only the services actually running on any particular day. * - * TODO RT_AB: this name is apparently modeled after R5, where the TransportNetwork is split into - * two layers (one for the streets and one for the public transit data). Here the situation is - * different: this seems to be an indexed and rearranged copy of the main transit data (a - * TransitModel instance). + * TODO RT_AB: this name may reflect usage in R5, where the TransportNetwork encompasses two + * sub-aggregates (one for the streets and one for the public transit data). Here, the TransitLayer + * seems to just be an indexed and rearranged copy of the main TransitModel instance. TG has + * indicated that "layer" should be restricted in its standard OO meaning, and this class should + * really be merged into TransitModel. */ public class TransitLayer { diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index ba83ed6886d..4b6c9938317 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -59,7 +59,7 @@ * In some sense the Graph is just some indexes into a set of vertices. The Graph used to hold lists * of edges for each vertex, but those lists are now attached to the vertices themselves. *

- * TODO RT_AB: rename to StreetGraph to emphasize what it represents? + * TODO RT_AB: I favor renaming to StreetGraph to emphasize what it represents. TG agreed in review. */ public class Graph implements Serializable { diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index d1d565eeb0d..b632ea6104b 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -30,16 +30,14 @@ import org.opentripplanner.transit.service.TransitService; /** - * The purpose of this class is to allow APIs (HTTP Resources) to access the OTP Server Context. - * FIXME RT_AB: Expand and clarify descriptions, which are circular (OtpServerRequestContext - * provides access to OTP Server Context). What exactly does "Server Context" mean here? - * What does "access" mean? Does this mean it "allows individual requests to call a limited - * number of methods on the components of the server without direct access to their internals?" + * The purpose of this class is to give APIs (HTTP Resources) read-only access to the OTP internal + * transit model. It allows individual API requests to use a limited number of methods and data + * structures without direct access to the internals of the server components. + * * By using an interface, and not injecting each service class we avoid giving the resources access - * to the server implementation. The context is injected by Jersey. An alternative to injecting this - * interface is to inject each individual component in the context - hence reducing the dependencies - * further. - * TODO RT_AB: clarify how injecting more individual components would "reduce dependencies further". + * to the server implementation. The context is injected by Jersey. Instead of injecting this + * context interface, it is conceivable to inject each of the individual items within this context. + * * But there is not a "real" need for this. For example, we do not have unit tests on the * Resources. If we in the future would decide to write unit tests for the APIs, then we could * eliminate this interface and just inject the components. See the bind method in OTPServer. diff --git a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index 21b911f500a..3fc51cea0c6 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -90,13 +90,11 @@ public static StopPatternBuilder create(int length) { * This has package local access since a StopPattern is a part of a TripPattern. To change it * use the {@link TripPattern#copyPlannedStopPattern()} method. */ - StopPatternBuilder mutate() { + StopPatternBuilder copyOf() { return new StopPatternBuilder(this, null); } - // TODO RT_AB: Rename and add documentation. This method does not mutate the object in place, it - // makes a copy. Confirmed in discussion that this should have a different name like "copy". - StopPatternBuilder mutate(StopPattern realTime) { + StopPatternBuilder copyOf(StopPattern realTime) { return new StopPatternBuilder(this, realTime); } diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index a4a6330e895..8e0ec4c6129 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -200,8 +200,8 @@ public StopPattern getStopPattern() { */ public StopPattern.StopPatternBuilder copyPlannedStopPattern() { return isModified() - ? originalTripPattern.stopPattern.mutate(stopPattern) - : stopPattern.mutate(); + ? originalTripPattern.stopPattern.copyOf(stopPattern) + : stopPattern.copyOf(); } public LineString getGeometry() { diff --git a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java index 09b7966908e..0dad35bfcd8 100644 --- a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java +++ b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java @@ -90,7 +90,7 @@ public GraphUpdaterManager(Graph graph, TransitModel transitModel, List Date: Sat, 4 May 2024 01:32:22 +0800 Subject: [PATCH 18/24] Update javadoc in response to PR review --- .../transit/model/network/TripPattern.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 8e0ec4c6129..ae7a605d5c1 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -42,6 +42,11 @@ * stop). Trips are assumed to be non-overtaking, so that an earlier trip never arrives after a * later trip. *

+ * The Route and StopPattern together form the natural key of the TripPattern. They are the shared + * set of characteristics that group many trips together into one TripPattern. This grouping saves + * memory by not replicating any details shared across all trips in the TripPattern, but it is + * also essential to some optimizations in routing algorithms like Raptor. + *

* This is called a JOURNEY_PATTERN in the Transmodel vocabulary. However, GTFS calls a Transmodel * JOURNEY a "trip", thus TripPattern. *

@@ -58,11 +63,6 @@ public final class TripPattern private final Route route; /** - * The Route and StopPattern together form the natural key of the TripPattern. They are the shared - * set of characteristics that group many trips together into one TripPattern. This grouping saves - * memory by not replicating any details shared across all trips in the TripPattern, but it is - * also essential to some optimizations in routing algorithms like Raptor. - *

* This field should not be accessed outside this class. All access to the StopPattern is * performed through method delegation, like the {@link #numberOfStops()} and * {@link #canBoard(int)} methods. @@ -73,15 +73,18 @@ public final class TripPattern * TripPatterns hold a reference to a Timetable (i.e. TripTimes for all Trips in the pattern) for * only scheduled trips from the GTFS or NeTEx data. If any trips were later updated in real time, * there will be another Timetable holding those updates and reading through to the scheduled one. - * This realtime Timetable is retrieved from a TimetableSnapshot. - * Also see end of Javadoc on TimetableSnapshot for more details. + * That other realtime Timetable is retrieved from a TimetableSnapshot (see end of Javadoc on + * TimetableSnapshot for more details). + * TODO RT_AB: The above system should be changed to integrate realtime and scheduled data more + * closely. The Timetable may become obsolete or change significantly when they are integrated. */ private final Timetable scheduledTimetable; - // This TransitMode is a redundant replication/memoization of information on the Route. + // This TransitMode is arguably a redundant replication/memoization of information on the Route. // It appears that in the TripPatternBuilder it is only ever set from a Trip which is itself set - // from a Route. - // TODO RT_AB: confirm whether there is any reason this doesn't just read through to Route. + // from a Route. This does not just read through to Route because in Netex trips may override + // the mode of their route. But we need to establish with more clarity whether our internal model + // TripPatterns allow trips of mixed modes, or rather if a single mode is part of their unique key. private final TransitMode mode; private final SubMode netexSubMode; From e0f74f197283e15e028f91a126d593144c889387 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 4 May 2024 01:35:21 +0800 Subject: [PATCH 19/24] formatting (mvn prettier:write) --- .../opentripplanner/ext/siri/SiriTripPatternCache.java | 6 +++--- .../transit/model/network/TripPattern.java | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 5cce9031e1b..4411e096ebc 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -31,11 +31,11 @@ * TODO RT_AB: To the extent that double SIRI/GTFS implementations are kept, prefix all names * with GTFS or SIRI or NETEX rather than having no prefix on the GTFS versions. * TODO RT_TG: There is no clear strategy for what should be in the cache and the transit model and the flow - * between them. The NeTEx and a GTFS version of this should be merged. Having NeTex and GTFS - * specific indexes inside is ok. With the increased usage of DatedServiceJourneys, this should probably + * between them. The NeTEx and a GTFS version of this should be merged. Having NeTex and GTFS + * specific indexes inside is ok. With the increased usage of DatedServiceJourneys, this should probably * be part of the main model - not a separate cashe. It is possible that this class works when it comes to * the thread-safety, but just by looking at a few lines of code I see problems - a strategy needs to be - * analysed, designed and documented. + * analysed, designed and documented. */ public class SiriTripPatternCache { diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index ae7a605d5c1..e59d9f5125c 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -91,11 +91,11 @@ public final class TripPattern private final boolean containsMultipleModes; private String name; - /** - * Geometries of each inter-stop segment of the tripPattern. - * Not used in routing, only for API listing. - * TODO: Encapsulate the byte arrays in a class. - */ + /** + * Geometries of each inter-stop segment of the tripPattern. + * Not used in routing, only for API listing. + * TODO: Encapsulate the byte arrays in a class. + */ private final byte[][] hopGeometries; /** From 07642672c80d1ca46795da81f8077e69cd1132e8 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 4 May 2024 01:40:19 +0800 Subject: [PATCH 20/24] Update updater/package.md Co-authored-by: Thomas Gran --- src/main/java/org/opentripplanner/updater/package.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 1a7c79f52b6..e6dc485d4df 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -2,7 +2,7 @@ ## Realtime Data Sources -Published transit data is broadly divided into two categories, which represent different time scales. On one hand we have scheduled data (also called static or theoretical data), and on the other hand realtime (or dynamic) data. Scheduled data is supplied in GTFS or NeTEx format, with the corresponding realtime data supplied in the [GTFS-RT](https://gtfs.org/realtime/reference/) and [SIRI](https://www.siri-cen.eu/) formats, respectively. This package contains code that retrieves and decodes realtime data, then layers it on top of the static transit data in a live OTP instance while it continues to handle routing requests. +Published transit data is broadly divided into two categories, which represent different time scales. On one hand we have scheduled data (also called planned or static data), and on the other hand realtime data. Scheduled data is supplied in GTFS or NeTEx format, with the corresponding realtime data supplied in the [GTFS-RT](https://gtfs.org/realtime/reference/) and [SIRI](https://www.siri-cen.eu/) formats, respectively. This package contains code that retrieves and decodes realtime data, then layers it on top of the static transit data in a live OTP instance while it continues to handle routing requests. Different data producers might update their scheduled data every month, week, or day. Realtime data then covers any changes to service at a timescale shorter than that of a given producer's scheduled data. Broadly speaking, realtime data represents short-term unexpected or unplanned changes that modify the planned schedules, and could require changes to journeys that the riders would not expect from the schedule. OpenTripPlanner uses three main categories of realtime data which are summarized in the table below. The SIRI specification includes more types, but OTP handles only these three that correspond to the three GTFS-RT types. From 97d99b09a6f3dde455d9a9d5aadbe5395bde4f79 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sat, 4 May 2024 23:44:42 +0800 Subject: [PATCH 21/24] Update src/main/java/org/opentripplanner/updater/package.md Co-authored-by: Thomas Gran --- src/main/java/org/opentripplanner/updater/package.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index e6dc485d4df..4855a6a5e51 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -36,7 +36,14 @@ As mentioned above, these GraphWriterRunnable instances must write to the transi This writable buffer of transit data is periodically made immutable and swapped into the role of a live snapshot, which is ready to be handed off to any incoming routing requests. Each time an immutable snapshot is created, a new writable buffer is created by making a shallow copy of the root instance in the transit data aggreagate. This functions like a double-buffering system, except that any number of snapshots can exist at once, and large subsets of the data can be shared across snapshots. As older snapshots (and their component parts) fall out of use, they are dereferenced and become eligible for garbage collection. Although the buffer swap could in principle occur after every write operation, it can incur significant copying and indexing overhead. When incremental message-oriented updaters are present this overhead would be incurred more often than necesary. Snapshots can be throttled to occur at most every few seconds, thereby reducing the total overhead at no perceptible cost to realtime visibility latency. -This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where 1) writing operations are simple to reason about and cannot conflict because only one write happens at a time; 2) multiple read operations (including routing requests) can occur concurrently; 3) read operations do not need to pause while writes are happening; 4) read operations see only fully completed write operations, never partial writes; and 5) each read operation sees a consistent, unchanging view of the transit data. +This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where: +

    +
  1. writing operations are simple to reason about and cannot conflict because only one write happens at a time.
  2. +
  3. multiple read operations (including routing requests) can occur concurrently.
  4. +
  5. read operations do not need to pause while writes are happening.
  6. +
  7. read operations see only fully completed write operations, never partial writes.
  8. +
  9. each read operation sees a consistent, unchanging view of the transit data.
  10. +
      An important characteristic of this approach is that _no locking is necessary_. However, some form of synchronization is used during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off. From f4a23d837138d50885b2b30eac0e6dffe702fbbf Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Sun, 5 May 2024 00:26:58 +0800 Subject: [PATCH 22/24] update package.md in response to PR review --- .../org/opentripplanner/updater/package.md | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 4855a6a5e51..134416ac826 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -4,7 +4,7 @@ Published transit data is broadly divided into two categories, which represent different time scales. On one hand we have scheduled data (also called planned or static data), and on the other hand realtime data. Scheduled data is supplied in GTFS or NeTEx format, with the corresponding realtime data supplied in the [GTFS-RT](https://gtfs.org/realtime/reference/) and [SIRI](https://www.siri-cen.eu/) formats, respectively. This package contains code that retrieves and decodes realtime data, then layers it on top of the static transit data in a live OTP instance while it continues to handle routing requests. -Different data producers might update their scheduled data every month, week, or day. Realtime data then covers any changes to service at a timescale shorter than that of a given producer's scheduled data. Broadly speaking, realtime data represents short-term unexpected or unplanned changes that modify the planned schedules, and could require changes to journeys that the riders would not expect from the schedule. OpenTripPlanner uses three main categories of realtime data which are summarized in the table below. The SIRI specification includes more types, but OTP handles only these three that correspond to the three GTFS-RT types. +Different data producers might update their scheduled data every month, week, or day; some even update it multiple times per day. Realtime data then covers any changes to service at a timescale shorter than that of a given producer's scheduled data. Broadly speaking, realtime data represents short-term unexpected or unplanned changes that modify the planned schedules, and could require changes to journeys that the riders would not expect from the schedule. OpenTripPlanner uses three main categories of realtime data which are summarized in the table below. The SIRI specification includes more types, but OTP handles only these three that correspond to the three GTFS-RT types. | GTFS-RT Name | SIRI Name | Description | |----------------------------------------------------------------------------------|--------------------------|-----------------------------------------------------------------------| @@ -12,7 +12,7 @@ Different data producers might update their scheduled data every month, week, or | [Trip Update](https://gtfs.org/realtime/reference/#message-tripupdate) | Estimated Timetable (ET) | Observed or expected arrival and departure times for near-term trips | | [Vehicle Position](https://gtfs.org/realtime/reference/#message-vehicleposition) | Vehicle Monitoring (VM) | Physical location of vehicles currently providing service | -GTFS-RT takes the form of binary protocol buffer messages that are typically fetched over HTTP. On the other hand, the SIRI specification originally described SOAP remote procedure calls retrieving an XML representation of the messages. Various projects instead adopted simple HTTP GET requests passing parameters in the URL and returning a JSON representation. The latter approach was eventually officially recognized as "SIRI Lite". +GTFS-RT takes the form of binary protocol buffer messages that are typically fetched over HTTP. On the other hand, the SIRI specification originally described SOAP remote procedure calls retrieving an XML representation of the messages. Various projects instead adopted simple HTTP GET requests passing parameters in the URL and optionally returning a JSON representation instead of XML. This latter approach was officially recognized as "SIRI Lite", has become quite common, and is the approach supported by OTP. Because OTP handles both GTFS-RT and SIRI data sources, there will often be two equivalent classes for retrieving and interpreting a particular kind of realtime data. For example, there is a SiriAlertsUpdateHandler and an AlertsUpdateHandler. The SIRI variants are typically prefixed with `Siri` while the GTFS-RT ones have no prefix for historical reasons (the GTFS-RT versions were originally the only ones). These should perhaps be renamed with a `GtfsRt` prefix for symmetry. Once the incoming messages have been decoded, they will ideally be mapped into a single internal class that was originally derived from GTFS-RT but has been extended to cover all information afforded by both GTFS and SIRI. For example, both classes mentioned above produce TransitAlert instances. These uniform internal representations can then be applied to the internal transit model using a single mechanism, independent of the message source type. @@ -24,6 +24,8 @@ The following approach to realtime concurrency was devised around 2013 when OTP On 11 January 2024 a team of OTP developers reviewed this realtime concurrency approach together. The conclusion was that this approach remains sound, and that any consistency problems were not due to the approach itself, but rather due to its partial or erroneous implementation in realtime updater classes. Therefore, we decided to continue applying this approach in any new work on the realtime subsystem, at least until we encounter some situation that does not fit within this model. All existing realtime code that is not consistent with this approach should progressively be brought in line with it. +In OTP's internal transit model, realtime data is currently stored separately from the scheduled data. This is only because realtime was originally introduced as an optional extra feature. Now that realtime is very commonly used, we intend to create a single unified transit model that will nonetheless continue to apply the same concurrency approach. + The following is a sequence diagram showing how threads are intended to communicate. Unlike some common forms of sequence diagrams, time is on the horizontal axis here. Each horizontal line represents either a thread of execution (handling incoming realtime messages or routing requests) or a queue or buffer data structure. Dotted lines represent object references being handed off, and solid lines represent data being copied. ![Realtime sequence diagram](images/updater-threads-queues.svg) @@ -37,13 +39,14 @@ As mentioned above, these GraphWriterRunnable instances must write to the transi This writable buffer of transit data is periodically made immutable and swapped into the role of a live snapshot, which is ready to be handed off to any incoming routing requests. Each time an immutable snapshot is created, a new writable buffer is created by making a shallow copy of the root instance in the transit data aggreagate. This functions like a double-buffering system, except that any number of snapshots can exist at once, and large subsets of the data can be shared across snapshots. As older snapshots (and their component parts) fall out of use, they are dereferenced and become eligible for garbage collection. Although the buffer swap could in principle occur after every write operation, it can incur significant copying and indexing overhead. When incremental message-oriented updaters are present this overhead would be incurred more often than necesary. Snapshots can be throttled to occur at most every few seconds, thereby reducing the total overhead at no perceptible cost to realtime visibility latency. This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where: -
        -
      1. writing operations are simple to reason about and cannot conflict because only one write happens at a time.
      2. -
      3. multiple read operations (including routing requests) can occur concurrently.
      4. -
      5. read operations do not need to pause while writes are happening.
      6. -
      7. read operations see only fully completed write operations, never partial writes.
      8. -
      9. each read operation sees a consistent, unchanging view of the transit data.
      10. -
          + +1. Writing operations are simple to reason about and cannot conflict because only one write happens at a time. +1. Multiple read operations (including routing requests) can occur concurrently. +1. Read operations do not need to pause while writes are happening. +1. Read operations see only fully completed write operations, never partial writes. +1. Each read operation sees a consistent, unchanging view of the transit data. +1. Each external API request sees a consistent data set, meaning all services that the + query directly or indirectly uses are operating on the same version of the data. An important characteristic of this approach is that _no locking is necessary_. However, some form of synchronization is used during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off. From ce31c8e5b0bcf52bf76c113e89247a356f891449 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 17 May 2024 17:05:52 +0800 Subject: [PATCH 23/24] Apply suggestions from code review Co-authored-by: Thomas Gran --- .../opentripplanner/ext/siri/SiriTripPatternCache.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 4411e096ebc..8b5896a1bf6 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -59,16 +59,15 @@ public class SiriTripPatternCache { // TODO RT_AB: generalize this so we can generate IDs for SIRI or GTFS-RT sources. private final SiriTripPatternIdGenerator tripPatternIdGenerator; - /** - * SiriTripPatternCache needs only this one feature of TransitService, so we retain only this - * function reference to effectively narrow the interface. This should also facilitate testing. - */ private final Function getPatternForTrip; /** - * Constructor. * TODO RT_AB: This class could potentially be reused for both SIRI and GTFS-RT, which may * involve injecting a different ID generator and pattern fetching method. + * + * @param getPatternForTrip SiriTripPatternCache needs only this one feature of TransitService, so we retain + * only this function reference to effectively narrow the interface. This should also facilitate + * testing. */ public SiriTripPatternCache( SiriTripPatternIdGenerator tripPatternIdGenerator, From c66d3011f02e225b9b780b56646a1d3e3f74fa27 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Fri, 17 May 2024 18:16:06 +0800 Subject: [PATCH 24/24] update comments in response to review --- .../ext/siri/SiriTripPatternIdGenerator.java | 9 ++++----- .../transit/model/network/TripPattern.java | 12 ++++++++---- src/main/java/org/opentripplanner/updater/package.md | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java index d5a98088746..1b56938d712 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java @@ -9,11 +9,10 @@ /** * This class generates new unique IDs for TripPatterns created in response to real-time updates - * from the SIRI updaters. It is important to create only one instance of this class, and inject - * that single instance wherever it is needed. The ID generation is threadsafe, even if that is - * probably not needed. - * TODO RT: To make this simpler to use we could make it a "Singelton" (static getInstance() method) - that would - * enforce one instance only, and simplify injection (use getInstance() where needed). + * from the SIRI updaters. In non-test usage it is important to create only one instance of this + * class, and inject that single instance wherever it is needed. However, this single-instance + * usage pattern is not enforced due to differing needs in tests. + * The ID generation is threadsafe, even if that is probably not needed. */ class SiriTripPatternIdGenerator { diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index e59d9f5125c..f9734c1cb1a 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -42,10 +42,14 @@ * stop). Trips are assumed to be non-overtaking, so that an earlier trip never arrives after a * later trip. *

          - * The Route and StopPattern together form the natural key of the TripPattern. They are the shared - * set of characteristics that group many trips together into one TripPattern. This grouping saves - * memory by not replicating any details shared across all trips in the TripPattern, but it is - * also essential to some optimizations in routing algorithms like Raptor. + * The key of the TripPattern includes the Route, StopPattern, TransitMode, and SubMode. All trips + * grouped under a TripPattern should have the same values for these characteristics (with possible + * exceptions for TransitMode and SubMode). + * TODO RT_AB: We need to clarify exactly which characteristics are identical across the trips. + * Grouping into patterns serves more than one purpose: it conserves memory by not replicating + * details shared across all trips in the TripPattern; it reflects business practices outside + * routing; it is essential to optimizations in routing algorithms like Raptor. We may be + * conflating a domain model grouping with an internal routing grouping. *

          * This is called a JOURNEY_PATTERN in the Transmodel vocabulary. However, GTFS calls a Transmodel * JOURNEY a "trip", thus TripPattern. diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md index 134416ac826..50c425bb6eb 100644 --- a/src/main/java/org/opentripplanner/updater/package.md +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -60,7 +60,7 @@ This section summarizes the rationale behind some of the design decisions. An OTP instance can have multiple sources of realtime data at once. In some cases the transit data includes several feeds of scheduled data from different providers, with one or more types of realtime updates for those different feeds. -In a large production system, ideally all the scheduled data would be integrated into a single data source with a unified ID namespace, and all the realtime data would also be integrated into a single data source with an exactly matching namespace. This would be the responsibility of a separate piece of software outside (upstream of) OTP. In practice, such an upstream data integration system does not exist in many deployments and OTP must manage several static and realtime data sources at once. Even when data feeds are well-integrated, the different kinds of realtime (arrival time updates, vehicle positions, or text alerts) may be split across multiple feeds as described in the GTFS-RT spec, which implies polling three different files. +In a large production OTP deployment, input data may be integrated into a single data source by an upstream system, or left as multiple data sources with guarantees about the uniqueness of identifiers. In either case, the single unified ID namespace allows realtime data to be easily associated with transit model entities. In practice, many OTP deployments do not have upstream data integration pipelines. In these cases OTP must manage several independent static and realtime data sources at once; feed IDs are used to keep namespaces separate, and to associate realtime data with the right subset of entities. Even when data feeds are well-integrated, the different kinds of realtime (arrival time updates, vehicle positions, or text alerts) may be split across multiple feeds as described in the GTFS-RT spec, which implies polling three different files. To handle these cases, it must be possible for more than one data source to specify the same feed ID. Eventually we want to make these feed IDs optional to simplify single-namespace OTP deployments. Each OTP instance in such a large configuration is also typically intended to handle several requests concurrently. Each incoming request needs to perform essentially random reads from the same large data structure representing the transit network, so there are efficiency benefits to many concurrent searches happening on the same instance, sharing this one large data structure. In a load-balanced cluster of OTP instances, realtime updates must be received and applied to each copy of the transportation network separately. So sharing each copy of the transportation network between a larger number of concurrent routing requests reduces the number of identical, arguably redundant network update processes going on simultaneously.