diff --git a/README.md b/README.md index ad479d7b5..a605a6943 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ The special E2E client settings should be defined in `env.yml`: | CONNECTED_DATA_PLATFORM_TRIP_HISTORY_UPLOAD_JOB_FREQUENCY_IN_MINUTES | integer | Optional | 5 | CDP trip history upload frequency. | | CONNECTED_DATA_PLATFORM_UPLOAD_BLANK_FILES | boolean | Optional | true | Whether to upload files where no records have been written. Defaults to true. | | DEFAULT_USAGE_PLAN_ID | string | Required | 123e45 | AWS API gateway default usage plan used when creating API keys for API users. | +| MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS | integer | Optional | 3 | The maximum number of attempts to obtain a monitored trip itinerary. | | MAXIMUM_PERMITTED_MONITORED_TRIPS | integer | Optional | 5 | The maximum number of saved monitored trips. | | MONGO_DB_NAME | string | Required | otp_middleware | The name of the OTP Middleware Mongo DB. | | MONGO_HOST | string | Optional | localhost:27017 | Mongo host address. | diff --git a/configurations/default/env.yml.tmp b/configurations/default/env.yml.tmp index 746e021b7..93730bf49 100644 --- a/configurations/default/env.yml.tmp +++ b/configurations/default/env.yml.tmp @@ -104,5 +104,8 @@ US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL: https://bus.notifier.example.com US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY: your-key US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES: agency_id:route_id +MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS: 3 + # The location for an OTP plan query request. -PLAN_QUERY_RESOURCE_URI: https://plan.resource.com \ No newline at end of file +PLAN_QUERY_RESOURCE_URI: https://plan.resource.com + diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/MonitoredTripController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/MonitoredTripController.java index f22f60bbb..476e99b11 100644 --- a/src/main/java/org/opentripplanner/middleware/controllers/api/MonitoredTripController.java +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/MonitoredTripController.java @@ -104,7 +104,7 @@ MonitoredTrip postCreateHook(MonitoredTrip monitoredTrip, Request req) { * monitored trip job, so return the trip as found in the database after the job completes. */ private MonitoredTrip runCheckMonitoredTrip(MonitoredTrip monitoredTrip) throws Exception { - new CheckMonitoredTrip(monitoredTrip).run(); + new CheckMonitoredTrip(monitoredTrip, false).run(); return Persistence.monitoredTrips.getById(monitoredTrip.id); } diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 382937cdb..6d0e25ef9 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -173,6 +173,11 @@ public class MonitoredTrip extends Model { */ public boolean notifyAtLeadingInterval = true; + /** + * The number of attempts made to obtain a trip's itinerary from OTP which matches this trip. + */ + public int attemptsToGetMatchingItinerary; + public MonitoredTrip() { } diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java index 3e0fdd1f7..081bbbc66 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java @@ -54,6 +54,9 @@ public class CheckMonitoredTrip implements Runnable { private final String OTP_UI_NAME = ConfigUtils.getConfigPropertyAsText("OTP_UI_NAME"); + public static final int MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS = + ConfigUtils.getConfigPropertyAsInt("MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS", 3); + private final String ACCOUNT_PATH = "/#/account"; private final String TRIPS_PATH = ACCOUNT_PATH + "/trips"; @@ -113,8 +116,15 @@ public class CheckMonitoredTrip implements Runnable { /** The OTP Response provider */ private Supplier otpResponseProvider; + private final boolean hasTolerantItineraryCheck; + public CheckMonitoredTrip(MonitoredTrip trip) throws CloneNotSupportedException { + this(trip, true); + } + + public CheckMonitoredTrip(MonitoredTrip trip, boolean hasTolerantItineraryCheck) throws CloneNotSupportedException { this.trip = trip; + this.hasTolerantItineraryCheck = hasTolerantItineraryCheck; previousJourneyState = trip.journeyState; journeyState = previousJourneyState.clone(); previousMatchingItinerary = trip.journeyState.matchingItinerary; @@ -122,7 +132,16 @@ public CheckMonitoredTrip(MonitoredTrip trip) throws CloneNotSupportedException } public CheckMonitoredTrip(MonitoredTrip trip, Supplier otpResponseProvider) throws CloneNotSupportedException { - this(trip); + this(trip, false); + this.otpResponseProvider = otpResponseProvider; + } + + public CheckMonitoredTrip( + MonitoredTrip trip, + Supplier otpResponseProvider, + boolean hasTolerantItineraryCheck + ) throws CloneNotSupportedException { + this(trip, hasTolerantItineraryCheck); this.otpResponseProvider = otpResponseProvider; } @@ -246,6 +265,7 @@ private boolean makeOTPRequestAndUpdateMatchingItineraryInternal() { if (ItineraryUtils.itinerariesMatch(trip.itinerary, candidateItinerary)) { // matching itinerary found! LOG.info("Found matching itinerary!"); + trip.attemptsToGetMatchingItinerary = 0; // Set the matching itinerary. matchingItinerary = candidateItinerary; @@ -283,40 +303,68 @@ private boolean makeOTPRequestAndUpdateMatchingItineraryInternal() { // If this point is reached, a matching itinerary was not found LOG.warn("No comparison itinerary found in otp response for trip"); - // Check whether this trip should no longer ever be checked due to not having matching itineraries on any - // monitored day of the week. For trips that are only monitored on one day of the week, they could have been not - // possible for just that day, but could again be possible the next week. Therefore, this checks if the trip - // was not possible on all monitored days of the previous week and if so, it updates the journeyState to say - // that the trip is no longer possible. - boolean noMatchingItineraryFoundOnPreviousChecks = - !trip.itineraryExistence.isPossibleOnAtLeastOneMonitoredDayOfTheWeek(trip); - journeyState.tripStatus = noMatchingItineraryFoundOnPreviousChecks - ? TripStatus.NO_LONGER_POSSIBLE - : TripStatus.NEXT_TRIP_NOT_POSSIBLE; + if (hasReachedMaxItineraryChecks()) { + // Check whether this trip should no longer ever be checked due to not having matching itineraries on any + // monitored day of the week. For trips that are only monitored on one day of the week, they could have been not + // possible for just that day, but could again be possible the next week. Therefore, this checks if the trip + // was not possible on all monitored days of the previous week and if so, it updates the journeyState to say + // that the trip is no longer possible. + boolean noMatchingItineraryFoundOnPreviousChecks = + !trip.itineraryExistence.isPossibleOnAtLeastOneMonitoredDayOfTheWeek(trip); + journeyState.tripStatus = noMatchingItineraryFoundOnPreviousChecks + ? TripStatus.NO_LONGER_POSSIBLE + : TripStatus.NEXT_TRIP_NOT_POSSIBLE; - LOG.info( - noMatchingItineraryFoundOnPreviousChecks - ? "Trip checking has no more possible days to check, TRIP NO LONGER POSSIBLE!" - : "Trip is not possible today, will check again next week." - ); + LOG.info( + noMatchingItineraryFoundOnPreviousChecks + ? "Trip checking has no more possible days to check, TRIP NO LONGER POSSIBLE!" + : "Trip is not possible today, will check again next week." + ); - // update trip itinerary existence to reflect that trip was not possible on this day of the week - trip.itineraryExistence - .getResultForDayOfWeek(targetZonedDateTime.getDayOfWeek()) - .handleInvalidDate(targetZonedDateTime); - updateMonitoredTrip(); + // update trip itinerary existence to reflect that trip was not possible on this day of the week + trip.itineraryExistence + .getResultForDayOfWeek(targetZonedDateTime.getDayOfWeek()) + .handleInvalidDate(targetZonedDateTime); + updateMonitoredTrip(); - // send an appropriate notification if the trip is still possible on another day of the week, or if it is now - // not possible on any day of the week that the trip should be monitored - enqueueNotification( - TripMonitorNotification.createItineraryNotFoundNotification( - !noMatchingItineraryFoundOnPreviousChecks, - getOtpUserLocale() - ) - ); + // send an appropriate notification if the trip is still possible on another day of the week, or if it is now + // not possible on any day of the week that the trip should be monitored + enqueueNotification( + TripMonitorNotification.createItineraryNotFoundNotification( + !noMatchingItineraryFoundOnPreviousChecks, + getOtpUserLocale() + ) + ); + } return false; } + /** + * If the OTP response does not contain the expected itinerary, check the number of attempts made and decide whether + * to try again or stop checking. + */ + private boolean hasReachedMaxItineraryChecks() { + if (!hasTolerantItineraryCheck) { + LOG.info("Tolerant itinerary check disabled."); + return true; + } + trip.attemptsToGetMatchingItinerary++; + LOG.info( + "Attempt {} of {} to get matching itinerary.", + trip.attemptsToGetMatchingItinerary, + MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS + ); + if (trip.attemptsToGetMatchingItinerary < MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS) { + if (Persistence.monitoredTrips.getById(trip.id) == null) { + // Trip has been deleted. Continue as if maximum itinerary checks have been reached. + return true; + } + Persistence.monitoredTrips.replace(trip.id, trip); + return false; + } + return true; + } + /** Default implementation for OtpResponse provider that actually invokes the OTP server. */ private OtpResponse getOtpResponse() { return OtpDispatcher.sendOtpRequestWithErrorHandling(getQueryParamsForTargetZonedDateTime()); diff --git a/src/main/resources/env.schema.json b/src/main/resources/env.schema.json index 715cef056..fcc9f79f8 100644 --- a/src/main/resources/env.schema.json +++ b/src/main/resources/env.schema.json @@ -128,6 +128,11 @@ "examples": ["123e45"], "description": "AWS API gateway default usage plan used when creating API keys for API users." }, + "MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS": { + "type": "integer", + "examples": ["3"], + "description": "The maximum number of attempts to obtain a monitored trip itinerary." + }, "MAXIMUM_PERMITTED_MONITORED_TRIPS": { "type": "integer", "examples": ["5"], diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 009779b5c..0e0f1348c 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -56,10 +56,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -90,10 +90,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -123,10 +123,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/admin/user: get: tags: @@ -155,10 +155,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/admin/user" @@ -178,10 +178,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -220,10 +220,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -269,10 +269,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -309,10 +309,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -356,10 +356,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -402,10 +402,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -447,10 +447,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TokenHolder" schema: $ref: "#/definitions/TokenHolder" + responseSchema: + $ref: "#/definitions/TokenHolder" /api/secure/application/fromtoken: get: tags: @@ -464,10 +464,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -498,10 +498,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -531,10 +531,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/application: get: tags: @@ -563,10 +563,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/application" @@ -586,10 +586,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -628,10 +628,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -677,10 +677,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -717,10 +717,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -754,10 +754,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -788,10 +788,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -821,10 +821,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/cdp: get: tags: @@ -853,10 +853,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/cdp" @@ -876,10 +876,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -918,10 +918,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -967,10 +967,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1007,10 +1007,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1050,10 +1050,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ItineraryExistence" schema: $ref: "#/definitions/ItineraryExistence" + responseSchema: + $ref: "#/definitions/ItineraryExistence" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1102,10 +1102,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredtrip" @@ -1125,10 +1125,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1167,10 +1167,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1216,10 +1216,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1257,10 +1257,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1298,10 +1298,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/updatetracking: post: tags: @@ -1319,10 +1319,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/track: post: tags: @@ -1340,10 +1340,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/endtracking: post: tags: @@ -1361,10 +1361,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" schema: $ref: "#/definitions/EndTrackingResponse" + responseSchema: + $ref: "#/definitions/EndTrackingResponse" /api/secure/monitoredtrip/forciblyendtracking: post: tags: @@ -1382,10 +1382,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" schema: $ref: "#/definitions/EndTrackingResponse" + responseSchema: + $ref: "#/definitions/EndTrackingResponse" /api/secure/triprequests: get: tags: @@ -1430,10 +1430,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TripRequest" schema: $ref: "#/definitions/TripRequest" + responseSchema: + $ref: "#/definitions/TripRequest" /api/secure/monitoredcomponent: get: tags: @@ -1462,10 +1462,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredcomponent" @@ -1485,10 +1485,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1527,10 +1527,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1576,10 +1576,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1617,10 +1617,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1660,10 +1660,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/VerificationResult" schema: $ref: "#/definitions/VerificationResult" + responseSchema: + $ref: "#/definitions/VerificationResult" /api/secure/user/{id}/verify_sms/{code}: post: tags: @@ -1683,10 +1683,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/VerificationResult" schema: $ref: "#/definitions/VerificationResult" + responseSchema: + $ref: "#/definitions/VerificationResult" /api/secure/user/fromtoken: get: tags: @@ -1700,10 +1700,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1734,10 +1734,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1767,10 +1767,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/user: get: tags: @@ -1799,10 +1799,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/user" @@ -1822,10 +1822,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1864,10 +1864,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1913,10 +1913,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1953,10 +1953,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -2010,11 +2010,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/ApiUsageResult" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/ApiUsageResult" @@ -2041,11 +2041,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/BugsnagEvent" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/BugsnagEvent" @@ -2072,11 +2072,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/CDPFile" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/CDPFile" @@ -2097,11 +2097,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/URL" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/URL" @@ -2566,6 +2566,9 @@ definitions: $ref: "#/definitions/JourneyState" notifyAtLeadingInterval: type: "boolean" + attemptsToGetMatchingItinerary: + type: "integer" + format: "int32" StopTime: type: "object" properties: diff --git a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java index 5986a74d6..9bc9a3eb6 100644 --- a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java +++ b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java @@ -47,6 +47,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.middleware.tripmonitor.TripStatus.NEXT_TRIP_NOT_POSSIBLE; +import static org.opentripplanner.middleware.tripmonitor.TripStatus.TRIP_ACTIVE; import static org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTripBasicTest.makeMonitoredTripFromNow; import static org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTripBasicTest.setRecurringTodayAndTomorrow; @@ -408,7 +410,7 @@ void canMakeOTPRequestAndUpdateMatchingItineraryForPreviouslyUnmatchedItinerary( // verify that status is active assertEquals( - TripStatus.TRIP_ACTIVE, + TRIP_ACTIVE, updatedTrip.journeyState.tripStatus, "updated trips status should be active" ); @@ -473,7 +475,7 @@ void canMakeOTPRequestAndResolveUnmatchedItinerary() throws Exception { // verify that status is active assertEquals( - TripStatus.NEXT_TRIP_NOT_POSSIBLE, + NEXT_TRIP_NOT_POSSIBLE, updatedTrip.journeyState.tripStatus, "updated trips status should indicate trip is not possible this day" ); @@ -550,7 +552,7 @@ void canMakeOTPRequestAndResolveNoLongerPossibleTrip() throws Exception { // fetch updated trip from persistence MonitoredTrip updatedTrip = Persistence.monitoredTrips.getById(mockTrip.id); - // verify that status is active + // verify that trip status is no longer possible assertEquals( TripStatus.NO_LONGER_POSSIBLE, updatedTrip.journeyState.tripStatus, @@ -580,7 +582,7 @@ void canMakeOTPRequestAndResolveNoLongerPossibleTrip() throws Exception { void shouldReportOneTimeTripInPastAsCompleted() throws CloneNotSupportedException { MonitoredTrip trip = makeMonitoredTripFromNow(-900, -300); trip.journeyState.matchingItinerary = trip.itinerary; - trip.journeyState.tripStatus = TripStatus.TRIP_ACTIVE; + trip.journeyState.tripStatus = TRIP_ACTIVE; new CheckMonitoredTrip(trip, this::mockOtpPlanResponse).checkOtpAndUpdateTripStatus(); assertEquals(TripStatus.PAST_TRIP, trip.journeyState.tripStatus); @@ -591,7 +593,7 @@ void shouldReportOneTimeTripInPastWithTrackingAsActive() throws CloneNotSupporte MonitoredTrip trip = createPastActiveTripWithTrackedJourney(); new CheckMonitoredTrip(trip, this::mockOtpPlanResponse).checkOtpAndUpdateTripStatus(); - assertEquals(TripStatus.TRIP_ACTIVE, trip.journeyState.tripStatus); + assertEquals(TRIP_ACTIVE, trip.journeyState.tripStatus); } private static MonitoredTrip createPastActiveTripWithTrackedJourney() { @@ -614,7 +616,7 @@ private static MonitoredTrip createPastActiveTrip() { trip.userId = user.id; trip.journeyState.matchingItinerary = trip.itinerary; trip.journeyState.targetDate = todayFormatted; - trip.journeyState.tripStatus = TripStatus.TRIP_ACTIVE; + trip.journeyState.tripStatus = TRIP_ACTIVE; Persistence.monitoredTrips.create(trip); return trip; } @@ -647,7 +649,7 @@ void shouldReportRecurringTripInstanceInPastWithTrackingAsActive() throws Except check.shouldSkipMonitoredTripCheck(false); check.checkOtpAndUpdateTripStatus(); // Trip should remain active, and the target date should still be "today". - assertEquals(TripStatus.TRIP_ACTIVE, trip.journeyState.tripStatus); + assertEquals(TRIP_ACTIVE, trip.journeyState.tripStatus); assertEquals(todayFormatted, trip.journeyState.targetDate); } @@ -656,4 +658,92 @@ static String getShiftedDay(Date startTime, int dayShift) { Date nextDayStart = Date.from(startInstant.plus(dayShift, ChronoUnit.DAYS)); return DateTimeUtils.makeOtpZonedDateTime(nextDayStart).format(DateTimeFormatter.ISO_LOCAL_DATE); } + + @Test + void canBeTolerantWithItineraryChecks() throws Exception { + MonitoredTrip monitoredTrip = PersistenceTestUtils.createMonitoredTrip( + user.id, + OtpTestUtils.OTP2_DISPATCHER_PLAN_RESPONSE.clone(), + false, + OtpTestUtils.createDefaultJourneyState() + ); + monitoredTrip.itineraryExistence.monday = new ItineraryExistence.ItineraryExistenceResult(); + Persistence.monitoredTrips.create(monitoredTrip); + + OtpResponse expectedResponse = getMockOtpResponse(); + OtpResponse unexpectedResponse = getMockOtpResponse(); + // Remove the final itinerary leg so that the matching itineraries check fails. + unexpectedResponse.plan.itineraries.get(0).legs.remove(2); + + // Mock the current time to be 8:45am on Monday, June 15, 2020. + DateTimeUtils.useFixedClockAt( + noonMonday8June2020 + .withDayOfMonth(15) + .withHour(8) + .withMinute(45) + ); + + // Match on first attempts. + assertCheckMonitoredTrip(monitoredTrip, expectedResponse, true, 0, TRIP_ACTIVE); + assertCheckMonitoredTrip(monitoredTrip, expectedResponse, false, 0, TRIP_ACTIVE); + + // Fail on first attempt. + assertCheckMonitoredTrip(monitoredTrip, unexpectedResponse, false, 0, NEXT_TRIP_NOT_POSSIBLE); + + // Reactivate trip. + monitoredTrip.journeyState.tripStatus = TRIP_ACTIVE; + Persistence.monitoredTrips.replace(monitoredTrip.id, monitoredTrip); + + // Match on second attempt. + assertCheckMonitoredTrip(monitoredTrip, unexpectedResponse, true, 1, TRIP_ACTIVE); + assertCheckMonitoredTrip(monitoredTrip, expectedResponse, true, 0, TRIP_ACTIVE); + + // Match on third attempt. + for (int i = 1; i <= 3; i++) { + int expectedAttempts = (i < 3) ? i : 0; + OtpResponse response = (i < 3) ? unexpectedResponse : expectedResponse; + assertCheckMonitoredTrip(monitoredTrip, response, true, expectedAttempts, TRIP_ACTIVE); + } + + int maxChecks = CheckMonitoredTrip.MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS; + + // Fail after maximum checks have been reached. + for (int i = 1; i <= maxChecks; i++) { + TripStatus tripStatus = (i < maxChecks) ? TRIP_ACTIVE : NEXT_TRIP_NOT_POSSIBLE; + assertCheckMonitoredTrip(monitoredTrip, unexpectedResponse, true, i, tripStatus); + } + + // Clear the created trip. + PersistenceTestUtils.deleteMonitoredTrip(monitoredTrip); + } + + /** + * Create mock OTP response and set the base times of the first itinerary to Monday, June 15, 2020. + */ + private OtpResponse getMockOtpResponse() { + OtpResponse mockResponse = mockOtpPlanResponse(); + Itinerary mockMondayJune15Itinerary = mockResponse.plan.itineraries.get(0); + OtpTestUtils.updateBaseItineraryTime( + mockMondayJune15Itinerary, + DateTimeUtils.makeOtpZonedDateTime(mockMondayJune15Itinerary.startTime) + .withDayOfMonth(15) + ); + return mockResponse; + } + + /** + * Run check on monitored trip and confirm expected state. + */ + private void assertCheckMonitoredTrip( + MonitoredTrip monitoredTrip, + OtpResponse mockResponse, + boolean hasTolerantItineraryCheck, + int expectedAttempts, + TripStatus expectedTripStatus + ) throws CloneNotSupportedException { + CheckMonitoredTrip checkMonitoredTrip = new CheckMonitoredTrip(monitoredTrip, () -> mockResponse, hasTolerantItineraryCheck); + checkMonitoredTrip.run(); + assertEquals(expectedAttempts, monitoredTrip.attemptsToGetMatchingItinerary); + assertEquals(expectedTripStatus, monitoredTrip.journeyState.tripStatus); + } }