diff --git a/load_testing/locustfile.py b/load_testing/locustfile.py index 504d688..6c45368 100644 --- a/load_testing/locustfile.py +++ b/load_testing/locustfile.py @@ -1,5 +1,6 @@ import datetime import random +from hashlib import sha256 from zoneinfo import ZoneInfo import requests @@ -7,42 +8,61 @@ from phoenix_channel import PhoenixChannel, PhoenixChannelUser -all_stop_ids: list[str] = list(map(lambda stop: stop["id"],requests.get( +all_station_ids: list[str] = list(map(lambda stop: stop["id"], requests.get( "https://api-v3.mbta.com/stops", - {"fields[stop]": "latitude,longitude", "filter[location_type]": "0,1"}, + {"fields[stop]": "id", "filter[location_type]": "1"}, ).json()["data"])) -rail_stop_ids: list[str] = list(map(lambda stop: stop["id"], requests.get( +standalone_bus_stop_ids: list[str] = list(map(lambda stop: stop["id"], +filter(lambda stop: stop["relationships"]["parent_station"]["data"] is None, requests.get( "https://api-v3.mbta.com/stops", - {"fields[stop]": "id", "filter[location_type]": "0", "filter[route_type]": "0,1"}, -).json()["data"])) + {"fields[stop]": "id", "filter[location_type]": "0", "filter[route_type]": "3"}, +).json()["data"]))) -cr_stop_ids: list[str] = list(map(lambda stop: stop["id"], requests.get( - "https://api-v3.mbta.com/stops", - {"fields[stop]": "id", "filter[location_type]": "0", "filter[route_type]": "2"}, -).json()["data"])) +all_stations_and_bus = all_station_ids + standalone_bus_stop_ids -bus_stop_ids: list[str] = list(map(lambda stop: stop["id"], requests.get( - "https://api-v3.mbta.com/stops", - {"fields[stop]": "id", "filter[location_type]": "0", "filter[route_type]": "3"}, -).json()["data"])) all_routes: list[dict] = requests.get( "https://api-v3.mbta.com/routes", {}, ).json()["data"] +initial_global_headers = {} +initial_rail_headers = {} + + +@events.test_start.add_listener +def on_init(environment, **_kwargs): + # Assume some % of users have already loaded global data before. + # Fetch global + rail data once from target host to use as baseline etag headers for newly spawned users + host = environment.host + global initial_global_headers + global initial_rail_headers + + initial_global_response = requests.get(f"{host}/api/global") + initial_global_headers = {} + if initial_global_response.status_code == 200: + initial_global_headers = {"if-none-match": sha256(initial_global_response.text.encode()).hexdigest()} + + initial_rail_response = requests.get(f"{host}/api/shapes/map-friendly/rail") + initial_rail_headers = {} + if initial_rail_response.status_code == 200: + initial_rail_headers = {"if-none-match": sha256(initial_rail_response.text.encode()).hexdigest()} + + @events.init_command_line_parser.add_listener def _(parser): parser.add_argument("--api-key", type=str, env_var="V3_API_KEY", default="", help="API Key for the V3 API. Set to avoid rate limiting.") class MobileAppUser(HttpUser, PhoenixChannelUser): - wait_time = between(5, 20) + wait_time = between(5, 60) socket_path = "/socket" prob_reset_initial_load = 0.02 prob_reset_nearby_stops = 0.3 prob_filtered_stop_details = 0.76 + prob_already_loaded_global = 0.8 + prob_station = 0.6 location: dict | None = None stop_id: str | None = None @@ -50,18 +70,31 @@ class MobileAppUser(HttpUser, PhoenixChannelUser): alerts_channel: PhoenixChannel | None = None predictions_channel: PhoenixChannel | None = None vehicles_channel: PhoenixChannel | None = None - did_initial_load = False + global_headers: dict = {} + rail_headers: dict = {} + v3_api_headers: dict = {} + + - v3_api_headers: dict = {} def on_start(self): self.v3_api_headers = {"x-api-key" : self.environment.parsed_options.api_key} + + if random.random() < self.prob_already_loaded_global: + self.global_headers = initial_global_headers + self.rail_headers = initial_rail_headers + self.app_reload() @task(1) def app_reload(self): - self.client.get("/api/global") - self.client.get("/api/shapes/map-friendly/rail") + global_response = self.client.get("/api/global", headers=self.global_headers) + if global_response.status_code == 200: + self.global_headers = {"if-none-match": sha256(global_response.text.encode()).hexdigest()} + + rail_response = self.client.get("/api/shapes/map-friendly/rail", headers=self.rail_headers) + if rail_response.status_code == 200: + self.rail_headers = {"if-none-match": sha256(rail_response.text.encode()).hexdigest()} if self.alerts_channel is not None: self.alerts_channel.leave() @@ -69,17 +102,17 @@ def app_reload(self): self.alerts_channel = self.socket.channel("alerts") self.alerts_channel.join() - - self.did_initial_load = True + + def fetch_schedules_for_stops(self, stop_ids): + self.client.get(f'/api/schedules?stop_ids={stop_ids}&date_time={datetime.datetime.now().astimezone(ZoneInfo("America/New_York")).replace(microsecond=0).isoformat()}' , name="/api/schedules",) @task(10) def nearby_transit(self): - nearby_rail_ids = random.sample(rail_stop_ids, random.randint(2,8)) - nearby_cr_ids = random.sample(cr_stop_ids, random.randint(0,14)) - nearby_bus_ids = random.sample(bus_stop_ids, random.randint(0,14)) + nearby_station_ids = random.sample(all_station_ids, random.randint(2,5)) + nearby_bus_ids = random.sample(standalone_bus_stop_ids, random.randint(0,10)) - self.nearby_stop_ids = nearby_rail_ids + nearby_cr_ids + nearby_bus_ids + self.nearby_stop_ids = nearby_station_ids + nearby_bus_ids if ( self.predictions_channel is not None and random.random() < self.prob_reset_nearby_stops @@ -92,12 +125,19 @@ def nearby_transit(self): f'predictions:stops:v2:{nearby_stops_concat}' ) self.predictions_channel.join() + + self.fetch_schedules_for_stops(self.nearby_stop_ids) + @task(5) def stop_details(self): - self.stop_id = random.choice(all_stop_ids) - self.client.get(f'/api/schedules?stop_ids={self.stop_id}&date_time={datetime.datetime.now().astimezone(ZoneInfo("America/New_York")).replace(microsecond=0).isoformat()}' , name="/api/schedules",) + if random.random() < self.prob_station: + self.stop_id = random.choice(all_station_ids) + else: + self.stop_id = random.choice(standalone_bus_stop_ids) + + self.fetch_schedules_for_stops([self.stop_id]) self.client.get(f'/api/stop/map?stop_id={self.stop_id}', name = "/api/stop/map") if ( @@ -126,7 +166,7 @@ def stop_details(self): @task(5) def trip_details(self): if self.stop_id is None: - self.stop_id = random.choice(all_stop_ids) + self.stop_id = random.choice(all_stations_and_bus) predictions_for_stop = requests.get( "https://api-v3.mbta.com/predictions", params={"stop": self.stop_id}, headers=self.v3_api_headers).json()["data"] diff --git a/mix.lock b/mix.lock index fdc560b..5298c08 100644 --- a/mix.lock +++ b/mix.lock @@ -52,7 +52,7 @@ "polyline": {:hex, :polyline, "1.4.0", "36666a3d010692d91d89501e13d385b6b136ef446f05814fb2e90149349d5a14", [:mix], [{:vector, "~> 1.0", [hex: :vector, repo: "hexpm", optional: false]}], "hexpm", "0e1e57497ba05f0355e23d722b03d5dc9a68d5d0c17c9f2dd5efefaa96f8960d"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "req": {:hex, :req, "0.5.7", "b722680e03d531a2947282adff474362a48a02aa54b131196fbf7acaff5e4cee", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c6035374615120a8923e8089d0c21a3496cf9eda2d287b806081b8f323ceee29"}, - "sentry": {:hex, :sentry, "10.7.1", "33392222d80ccff99c503f972998d2858b4c1e5aca2219a34269b68dacba8e7d", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "56291312397bf2b6afab6cf4f7aa1f27413b0eb2ceeb63b8aab2d7658aaea882"}, + "sentry": {:hex, :sentry, "10.8.0", "1e8cc0ef21401e5914e6fc2f37489d6c685d31a0556dbd8ab4709cc1587a7232", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "92549e7ba776b7ccfed4e74d58987272d37d99606b130e4141bc015a1a8e4235"}, "server_sent_event_stage": {:hex, :server_sent_event_stage, "1.2.1", "ede8c63496e19f039972503aa242cb4c16a301d495be7d9fdda2f952298cbf0c", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.22", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.1", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "5b84b2c886bfa3ab38143d424eff6ec9d224e65ca576652aab762ed9d11847c3"}, "slipstream": {:hex, :slipstream, "1.1.1", "7e56f62f1a9ee81351e3c36f57b9b187e00dc2f470e70ba46ea7ad16e80b061f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c20e420cde1654329d38ec3aa1c0e4debbd4c91ca421491e7984ad4644e638a6"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},