From 2694cf1c59ebd6b0cf50ec6871de77b5c3347e75 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 10 Jul 2020 16:05:20 +0100 Subject: [PATCH 1/8] generate and store random KB-size payloads within a specified range --- lib/origin_simulator/payload.ex | 36 ++++++++++++++++++++++++-- lib/origin_simulator/recipe.ex | 1 + test/origin_simulator/payload_test.exs | 14 +++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/origin_simulator/payload.ex b/lib/origin_simulator/payload.ex index b7dbc58..404e355 100644 --- a/lib/origin_simulator/payload.ex +++ b/lib/origin_simulator/payload.ex @@ -5,12 +5,19 @@ defmodule OriginSimulator.Payload do @http_client Application.get_env(:origin_simulator, :http_client) + @range_step_size 20 + @unit "kb" + @unit_regex ~r/kb/ + ## Client API def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: :payload) end + # TODO: rename `fetch` to `create` or `generate` to better reflect that OS actually create + # in-memory payload via various mechanisms, i.e. fetch from origin, + # provided in recipe or random content def fetch(server, %Recipe{origin: value, route: route} = recipe) when is_binary(value) do GenServer.call(server, {:fetch, recipe, route}) end @@ -19,8 +26,14 @@ defmodule OriginSimulator.Payload do GenServer.call(server, {:parse, recipe, route}) end - def fetch(server, %Recipe{random_content: value, route: route} = recipe) - when is_binary(value) do + def fetch(server, %Recipe{random_content: value, route: route} = recipe) when is_binary(value) do + case String.contains?(value, "..") do + true -> fetch(server, %{recipe | random_content: String.split(value, "..")}) + false -> GenServer.call(server, {:generate, recipe, route}) + end + end + + def fetch(server, %Recipe{random_content: [_min, _max], route: route} = recipe) do GenServer.call(server, {:generate, recipe, route}) end @@ -34,6 +47,8 @@ defmodule OriginSimulator.Payload do end end + def range_step_size, do: @range_step_size + defp cache_lookup(route) do case :ets.lookup(:payload, route) do [{^route, body}] -> {:ok, body} @@ -53,6 +68,7 @@ defmodule OriginSimulator.Payload do def handle_call({:fetch, recipe, route}, _from, state) do {:ok, %HTTPoison.Response{body: body}} = @http_client.get(recipe.origin, recipe.headers) :ets.insert(:payload, {route, body}) + {:reply, :ok, state} end @@ -63,6 +79,22 @@ defmodule OriginSimulator.Payload do {:reply, :ok, state} end + @impl true + def handle_call({:generate, %{random_content: [min, max]} = recipe, route}, _from, state) do + min_integer = Regex.replace(@unit_regex, min, "") |> String.to_integer() + max_integer = Regex.replace(@unit_regex, max, "") |> String.to_integer() + + min_integer..max_integer + |> Enum.take_every(@range_step_size) + |> Enum.filter(&(&1 != 0)) + |> Enum.each(fn size -> + size_kb = Integer.to_string(size) <> @unit + :ets.insert(:payload, {{route, size}, Body.randomise(size_kb, recipe.headers)}) + end) + + {:reply, :ok, state} + end + @impl true def handle_call({:generate, recipe, route}, _from, state) do :ets.insert(:payload, {route, Body.randomise(recipe.random_content, recipe.headers)}) diff --git a/lib/origin_simulator/recipe.ex b/lib/origin_simulator/recipe.ex index 43ba8d6..067755a 100644 --- a/lib/origin_simulator/recipe.ex +++ b/lib/origin_simulator/recipe.ex @@ -1,6 +1,7 @@ defmodule OriginSimulator.Recipe do defstruct origin: nil, body: nil, random_content: nil, headers: %{}, stages: [], route: "/*" + # TODO: missing stage stages, type spec for 'stage' @type t :: %__MODULE__{ origin: String.t(), body: String.t(), diff --git a/test/origin_simulator/payload_test.exs b/test/origin_simulator/payload_test.exs index 0b4f69e..0aec33f 100644 --- a/test/origin_simulator/payload_test.exs +++ b/test/origin_simulator/payload_test.exs @@ -5,6 +5,8 @@ defmodule OriginSimulator.PayloadTest do alias OriginSimulator.Payload + @range_step_size OriginSimulator.Payload.range_step_size() + # TODO: additional tests for fetching and storing multi-origin / source content in ETS describe "with origin" do setup do @@ -61,11 +63,21 @@ defmodule OriginSimulator.PayloadTest do assert Payload.body(:payload, 200) == {:ok, :zlib.gzip("{\"hello\":\"world\"}")} end - test "returns gzip random content" do + test "returns gzip random payload of a specified size" do Payload.fetch(:payload, random_content_recipe("10kb", %{"content-encoding" => "gzip"})) {:ok, gzip_content} = Payload.body(:payload, 200) assert gzip_content |> :zlib.gunzip() |> String.length() == 10 * 1024 end + + test "returns gzip random payloads within a specified range" do + Payload.fetch(:payload, random_content_recipe("0kb..100kb", %{"content-encoding" => "gzip"})) + + # currently with fixed 20kb step sizes + for size <- Enum.take_every(20..100, @range_step_size) do + {:ok, gzip_content} = Payload.body(:payload, 200, "/*", {"/*", size}) + assert gzip_content |> :zlib.gunzip() |> String.length() == size * 1024 + end + end end end From ce9d02e998e1f5c4408b0d1f8fe616bd983f679e Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 24 Jul 2020 13:04:30 +0100 Subject: [PATCH 2/8] create a Simulation struct with payload_id (ETS entry) --- lib/origin_simulator/payload.ex | 2 ++ lib/origin_simulator/simulation.ex | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/origin_simulator/payload.ex b/lib/origin_simulator/payload.ex index 404e355..c9f6d44 100644 --- a/lib/origin_simulator/payload.ex +++ b/lib/origin_simulator/payload.ex @@ -84,6 +84,8 @@ defmodule OriginSimulator.Payload do min_integer = Regex.replace(@unit_regex, min, "") |> String.to_integer() max_integer = Regex.replace(@unit_regex, max, "") |> String.to_integer() + :ets.insert(:payload, {route, Body.randomise(max, recipe.headers)}) + min_integer..max_integer |> Enum.take_every(@range_step_size) |> Enum.filter(&(&1 != 0)) diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index 1e22d5b..63b76ef 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -1,7 +1,17 @@ defmodule OriginSimulator.Simulation do use GenServer - alias OriginSimulator.{Recipe, Payload, Duration} + alias OriginSimulator.{Recipe, Payload, Duration, Simulation} + + defstruct latency: 0, payload_id: nil, recipe: nil, status: 406 + + @type recipe :: OriginSimulator.Recipe.t() + @type t :: %__MODULE__{ + latency: integer(), + payload_id: binary(), + recipe: recipe, + status: integer() + } ## Client API @@ -9,6 +19,8 @@ defmodule OriginSimulator.Simulation do GenServer.start_link(__MODULE__, opts, name: :simulation) end + def new(), do: %Simulation{} + def state(server) do GenServer.call(server, :state) end @@ -54,7 +66,7 @@ defmodule OriginSimulator.Simulation do @impl true def init(_) do - {:ok, %{Recipe.default_route() => default_simulation()}} + {:ok, %{Recipe.default_route() => new()}} end @impl true @@ -115,11 +127,9 @@ defmodule OriginSimulator.Simulation do {:noreply, Map.put(state, route, %{state[route] | status: status, latency: latency})} end - defp get(nil), do: default_simulation() + defp get(nil), do: new() defp get(current_state), do: current_state - defp default_simulation(), do: %{recipe: nil, status: 406, latency: 0} - defp match_route(state, nil, route) do Map.keys(state) |> Enum.filter(&String.ends_with?(&1, "*")) From 76f0f05501cd28cf9980fe37ca07a63393e04d60 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 24 Jul 2020 13:44:05 +0100 Subject: [PATCH 3/8] incorporate payload_id state for Simulation --- lib/origin_simulator.ex | 4 ++-- lib/origin_simulator/simulation.ex | 8 ++++---- test/origin_simulator/simulation_test.exs | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/origin_simulator.ex b/lib/origin_simulator.ex index 170b869..46a0e6c 100644 --- a/lib/origin_simulator.ex +++ b/lib/origin_simulator.ex @@ -23,11 +23,11 @@ defmodule OriginSimulator do def admin_domain(), do: Application.get_env(:origin_simulator, :admin_domain) defp serve_payload(conn, route) do - {status, latency} = Simulation.state(:simulation, route) + {status, latency, payload_id} = Simulation.state(:simulation, route) sleep(latency) - {:ok, body} = Payload.body(:payload, status, conn.request_path, route) + {:ok, body} = Payload.body(:payload, status, conn.request_path, payload_id) recipe = Simulation.recipe(:simulation, route) diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index 63b76ef..03b07db 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -76,7 +76,7 @@ defmodule OriginSimulator.Simulation do @impl true def handle_call({:state, route}, _from, state) do - {:reply, {state[route].status, state[route].latency}, state} + {:reply, {state[route].status, state[route].latency, state[route].payload_id}, state} end @impl true @@ -106,7 +106,7 @@ defmodule OriginSimulator.Simulation do Enum.map(new_recipe.stages, fn item -> Process.send_after( self(), - {:update, route, item["status"], Duration.parse(item["latency"])}, + {:update, route, item["status"], Duration.parse(item["latency"]), route}, Duration.parse(item["at"]) ) end) @@ -123,8 +123,8 @@ defmodule OriginSimulator.Simulation do def handle_call(:route, _from, state), do: {:reply, state |> Map.keys(), state} @impl true - def handle_info({:update, route, status, latency}, state) do - {:noreply, Map.put(state, route, %{state[route] | status: status, latency: latency})} + def handle_info({:update, route, status, latency, payload_id}, state) do + {:noreply, Map.put(state, route, %{state[route] | status: status, latency: latency, payload_id: payload_id})} end defp get(nil), do: new() diff --git a/test/origin_simulator/simulation_test.exs b/test/origin_simulator/simulation_test.exs index a6fd02e..69c461b 100644 --- a/test/origin_simulator/simulation_test.exs +++ b/test/origin_simulator/simulation_test.exs @@ -16,8 +16,8 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status and latency in ms for a route", %{route: route} do - assert Simulation.state(:simulation, route) == {200, 1000} + test "state() returns a tuple with http status, latency, payload_id for a route", %{route: route} do + assert Simulation.state(:simulation, route) == {200, 1000, route} end test "recipe() returns the loaded recipe for a route", %{recipe: recipe, route: route} do @@ -73,8 +73,8 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status and latency in ms", %{route: route} do - assert Simulation.state(:simulation, route) == {200, 1000..1200} + test "state() returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do + assert Simulation.state(:simulation, route) == {200, 1000..1200, route} end test "recipe() returns the loaded recipe", %{recipe: recipe, route: route} do @@ -96,10 +96,10 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status and latency in ms", %{route: route} do - assert Simulation.state(:simulation, route) == {200, 0} + test "state() returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do + assert Simulation.state(:simulation, route) == {200, 0, route} Process.sleep(80) - assert Simulation.state(:simulation, route) == {503, 1000} + assert Simulation.state(:simulation, route) == {503, 1000, route} end test "recipe() returns the loaded recipe", %{recipe: recipe, route: route} do @@ -114,7 +114,7 @@ defmodule OriginSimulator.SimulationTest do end test "state() returns a tuple with default values" do - assert Simulation.state(:simulation, Recipe.default_route()) == {406, 0} + assert Simulation.state(:simulation, Recipe.default_route()) == {406, 0, nil} end test "recipe() returns an empty list" do From f4b2390536c54eb79ffd272e684994b105ab25af Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 24 Jul 2020 14:13:03 +0100 Subject: [PATCH 4/8] fix state/recipe calls for non-existing routes so these do not cause simulation GenServer crash --- lib/origin_simulator/simulation.ex | 13 ++++++++++-- test/origin_simulator/simulation_test.exs | 26 +++++++++++++++-------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index 03b07db..acfd8d0 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -76,12 +76,21 @@ defmodule OriginSimulator.Simulation do @impl true def handle_call({:state, route}, _from, state) do - {:reply, {state[route].status, state[route].latency, state[route].payload_id}, state} + case state[route] do + %{status: status, latency: latency, payload_id: payload_id} -> + {:reply, {status, latency, payload_id}, state} + + nil -> + {:reply, {406, 0, nil}, state} + end end @impl true def handle_call({:recipe, route}, _from, state) do - {:reply, state[route].recipe, state} + case state[route] do + %{recipe: recipe} -> {:reply, recipe, state} + nil -> {:reply, nil, state} + end end # retrieve all recipes diff --git a/test/origin_simulator/simulation_test.exs b/test/origin_simulator/simulation_test.exs index 69c461b..cf3eac9 100644 --- a/test/origin_simulator/simulation_test.exs +++ b/test/origin_simulator/simulation_test.exs @@ -16,14 +16,22 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status, latency, payload_id for a route", %{route: route} do + test "state/2 returns a tuple with http status, latency, payload_id for a route", %{route: route} do assert Simulation.state(:simulation, route) == {200, 1000, route} end - test "recipe() returns the loaded recipe for a route", %{recipe: recipe, route: route} do + test "state/2 does not crash GenServer and returns default tuple for non existing routes" do + assert Simulation.state(:simulation, "/non_existing") == {406, 0, nil} + end + + test "recipe/2 returns the loaded recipe for a route", %{recipe: recipe, route: route} do assert Simulation.recipe(:simulation, route) == recipe end + test "recipe/2 does not crash GenServer and returns nil for non existing routes" do + assert Simulation.recipe(:simulation, "/non_existing") == nil + end + test "route/2 returns matching route", %{recipe: recipe, route: route} do assert Simulation.route(:simulation, route) == recipe |> Map.get(:route) end @@ -73,11 +81,11 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do + test "state/2 returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do assert Simulation.state(:simulation, route) == {200, 1000..1200, route} end - test "recipe() returns the loaded recipe", %{recipe: recipe, route: route} do + test "recipe/2 returns the loaded recipe", %{recipe: recipe, route: route} do assert Simulation.recipe(:simulation, route) == recipe end end @@ -96,13 +104,13 @@ defmodule OriginSimulator.SimulationTest do {:ok, recipe: recipe, route: recipe.route} end - test "state() returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do + test "state/2 returns a tuple with http status, latency in ms, payload id (route)", %{route: route} do assert Simulation.state(:simulation, route) == {200, 0, route} Process.sleep(80) assert Simulation.state(:simulation, route) == {503, 1000, route} end - test "recipe() returns the loaded recipe", %{recipe: recipe, route: route} do + test "recipe/2 returns the loaded recipe", %{recipe: recipe, route: route} do assert Simulation.recipe(:simulation, route) == recipe end end @@ -113,15 +121,15 @@ defmodule OriginSimulator.SimulationTest do Process.sleep(5) end - test "state() returns a tuple with default values" do + test "state/2 returns a tuple with default values" do assert Simulation.state(:simulation, Recipe.default_route()) == {406, 0, nil} end - test "recipe() returns an empty list" do + test "recipe/1 returns an empty list" do assert Simulation.recipe(:simulation) == [] end - test "route() returns default route" do + test "route/2 returns default route" do assert Simulation.route(:simulation, "/random_path") == "/*" end end From 33ce80df83709a92216af5289221c99d8f260600 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 7 Aug 2020 10:28:20 +0100 Subject: [PATCH 5/8] refactor random payloads auto-generation with a function returning a series of created payloads --- lib/origin_simulator/payload.ex | 20 ++++++++++++-------- test/origin_simulator/payload_test.exs | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/origin_simulator/payload.ex b/lib/origin_simulator/payload.ex index c9f6d44..689a4e7 100644 --- a/lib/origin_simulator/payload.ex +++ b/lib/origin_simulator/payload.ex @@ -5,7 +5,7 @@ defmodule OriginSimulator.Payload do @http_client Application.get_env(:origin_simulator, :http_client) - @range_step_size 20 + @random_payload_step_size 5 @unit "kb" @unit_regex ~r/kb/ @@ -47,7 +47,16 @@ defmodule OriginSimulator.Payload do end end - def range_step_size, do: @range_step_size + def random_payload_step_size, do: @random_payload_step_size + + def random_payload_series([min, max]) do + min_integer = Regex.replace(@unit_regex, min, "") |> String.to_integer() + max_integer = Regex.replace(@unit_regex, max, "") |> String.to_integer() + + min_integer..max_integer + |> Enum.take_every(@random_payload_step_size) + |> Enum.filter(&(&1 != 0)) + end defp cache_lookup(route) do case :ets.lookup(:payload, route) do @@ -81,14 +90,9 @@ defmodule OriginSimulator.Payload do @impl true def handle_call({:generate, %{random_content: [min, max]} = recipe, route}, _from, state) do - min_integer = Regex.replace(@unit_regex, min, "") |> String.to_integer() - max_integer = Regex.replace(@unit_regex, max, "") |> String.to_integer() - :ets.insert(:payload, {route, Body.randomise(max, recipe.headers)}) - min_integer..max_integer - |> Enum.take_every(@range_step_size) - |> Enum.filter(&(&1 != 0)) + random_payload_series([min, max]) |> Enum.each(fn size -> size_kb = Integer.to_string(size) <> @unit :ets.insert(:payload, {{route, size}, Body.randomise(size_kb, recipe.headers)}) diff --git a/test/origin_simulator/payload_test.exs b/test/origin_simulator/payload_test.exs index 0aec33f..78a5952 100644 --- a/test/origin_simulator/payload_test.exs +++ b/test/origin_simulator/payload_test.exs @@ -5,7 +5,7 @@ defmodule OriginSimulator.PayloadTest do alias OriginSimulator.Payload - @range_step_size OriginSimulator.Payload.range_step_size() + @random_payload_step_size OriginSimulator.Payload.random_payload_step_size() # TODO: additional tests for fetching and storing multi-origin / source content in ETS describe "with origin" do @@ -74,7 +74,7 @@ defmodule OriginSimulator.PayloadTest do Payload.fetch(:payload, random_content_recipe("0kb..100kb", %{"content-encoding" => "gzip"})) # currently with fixed 20kb step sizes - for size <- Enum.take_every(20..100, @range_step_size) do + for size <- Enum.take_every(20..100, @random_payload_step_size) do {:ok, gzip_content} = Payload.body(:payload, 200, "/*", {"/*", size}) assert gzip_content |> :zlib.gunzip() |> String.length() == size * 1024 end From e842935fac8f52a637f62ad6f0109d5e5e87a6a8 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 7 Aug 2020 15:05:00 +0100 Subject: [PATCH 6/8] introduce a flakiness GenServer that updates payload id for random payload (range) recipe --- lib/origin_simulator/flakiness.ex | 77 ++++++++++++++++++++++++ lib/origin_simulator/simulation.ex | 13 +++- lib/origin_simulator/supervisor.ex | 3 +- test/fixtures/recipes.exs | 5 +- test/origin_simulator/flakiness_test.exs | 49 +++++++++++++++ 5 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 lib/origin_simulator/flakiness.ex create mode 100644 test/origin_simulator/flakiness_test.exs diff --git a/lib/origin_simulator/flakiness.ex b/lib/origin_simulator/flakiness.ex new file mode 100644 index 0000000..b95d58e --- /dev/null +++ b/lib/origin_simulator/flakiness.ex @@ -0,0 +1,77 @@ +defmodule OriginSimulator.Flakiness do + @moduledoc """ + A server that introduces latency, payload and status flakiness + during simulation. + """ + use GenServer + alias OriginSimulator.{Flakiness, Payload} + + @default_interval 1000 + @simulation_server :simulation + + defstruct payload: [], status: [], route: "/*", interval: nil + + @type t :: %__MODULE__{ + payload: [binary()], + status: [integer()], + route: binary, + interval: integer() + } + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: :flakiness) + end + + def new(), do: %Flakiness{} + def new(payload_series, route), do: %Flakiness{payload: payload_series, route: route} + + def start(%{random_content: value}, route) do + String.split(value, "..") + |> Payload.random_payload_series() + |> Flakiness.new(route) + |> Map.put(:interval, @default_interval) + |> set() + + start() + end + + def set(flakiness), do: GenServer.call(:flakiness, {:set, flakiness}) + def state(), do: GenServer.call(:flakiness, :state) + def start(), do: GenServer.call(:flakiness, :start) + + # TODO + # def stop() + + # Callbacks + + @impl true + def init(_) do + {:ok, new()} + end + + @impl true + def handle_call(:state, _from, flakiness) do + {:reply, flakiness, flakiness} + end + + @impl true + def handle_call(:start, _from, flakiness) do + send(self(), :flaky) + {:reply, :ok, flakiness} + end + + @impl true + def handle_call({:set, new_flakiness}, _from, _flakiness) do + {:reply, :ok, new_flakiness} + end + + @impl true + def handle_info(:flaky, flakiness) do + send( + @simulation_server, + {:update, {flakiness.route, flakiness.payload |> Enum.random()}} + ) + + {:noreply, flakiness} + end +end diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index acfd8d0..ab2b445 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -1,7 +1,7 @@ defmodule OriginSimulator.Simulation do use GenServer - alias OriginSimulator.{Recipe, Payload, Duration, Simulation} + alias OriginSimulator.{Recipe, Payload, Duration, Simulation, Flakiness} defstruct latency: 0, payload_id: nil, recipe: nil, status: 406 @@ -112,6 +112,8 @@ defmodule OriginSimulator.Simulation do route = new_recipe.route simulation = get(state[route]) + if auto_flakiness?(new_recipe), do: Flakiness.start(new_recipe, route) + Enum.map(new_recipe.stages, fn item -> Process.send_after( self(), @@ -136,6 +138,15 @@ defmodule OriginSimulator.Simulation do {:noreply, Map.put(state, route, %{state[route] | status: status, latency: latency, payload_id: payload_id})} end + @impl true + def handle_info({:update, {route, payload_id}}, state) do + {:noreply, Map.put(state, route, %{state[route] | payload_id: {route, payload_id}})} + end + + defp auto_flakiness?(%{random_content: nil}), do: false + defp auto_flakiness?(%{random_content: value}), do: String.contains?(value, "..") + defp auto_flakiness?(_other_recipe_type), do: false + defp get(nil), do: new() defp get(current_state), do: current_state diff --git a/lib/origin_simulator/supervisor.ex b/lib/origin_simulator/supervisor.ex index 007eb0e..65a7227 100644 --- a/lib/origin_simulator/supervisor.ex +++ b/lib/origin_simulator/supervisor.ex @@ -10,7 +10,8 @@ defmodule OriginSimulator.Supervisor do children = [ OriginSimulator.Simulation, OriginSimulator.Payload, - OriginSimulator.Counter + OriginSimulator.Counter, + OriginSimulator.Flakiness ] opts = [ diff --git a/test/fixtures/recipes.exs b/test/fixtures/recipes.exs index be5ef85..a6faa79 100644 --- a/test/fixtures/recipes.exs +++ b/test/fixtures/recipes.exs @@ -27,11 +27,12 @@ defmodule Fixtures.Recipes do } end - def random_content_recipe(size \\ "50kb", headers \\ %{}) do + def random_content_recipe(size \\ "50kb", headers \\ %{}, route \\ "/*") do %Recipe{ random_content: size, stages: [%{"at" => 0, "status" => 200, "latency" => 0}], - headers: headers + headers: headers, + route: route } end diff --git a/test/origin_simulator/flakiness_test.exs b/test/origin_simulator/flakiness_test.exs new file mode 100644 index 0000000..5011710 --- /dev/null +++ b/test/origin_simulator/flakiness_test.exs @@ -0,0 +1,49 @@ +defmodule OriginSimulator.FlakinessTest do + use ExUnit.Case, async: true + import Fixtures.Recipes + alias OriginSimulator.{Flakiness, Simulation} + + @default_interval 1000 + @simulation_server :simulation + + test "new/0 returns a new Flakiness struct" do + assert Flakiness.new() == %Flakiness{interval: nil, payload: [], route: "/*", status: []} + end + + test "new/2 returns Flakiness struct with random payload series and route" do + flakiness = Flakiness.new([150, 155, 160], "/an_origin_route") + + assert flakiness.payload == [150, 155, 160] + assert flakiness.route == "/an_origin_route" + end + + test "state/0 returns the current flakiness state" do + Flakiness.new([150, 155, 160], "/an_origin_route") + |> Flakiness.set() + + assert Flakiness.state() == %Flakiness{payload: [150, 155, 160], route: "/an_origin_route"} + end + + test "set/1 flakiness" do + previous_flakiness = Flakiness.state() + new_flakiness = Flakiness.new([500, 505, 510], "/route_a") + + Flakiness.set(new_flakiness) + + refute Flakiness.state() == previous_flakiness + assert Flakiness.state() == %Flakiness{payload: [500, 505, 510], route: "/route_a"} + end + + test "start/2 flakiness for a route" do + recipe = random_content_recipe("10kb..50kb", %{"content-encoding" => "gzip"}, "/route_for_random_payloads") + + Simulation.add_recipe(@simulation_server, recipe) + Process.sleep(5) + + assert Flakiness.state() == %Flakiness{ + interval: @default_interval, + payload: [10, 15, 20, 25, 30, 35, 40, 45, 50], + route: "/route_for_random_payloads" + } + end +end From c4b07619285ed095ced2774b53734451d0c4e36e Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 7 Aug 2020 15:45:43 +0100 Subject: [PATCH 7/8] create a loop in flakiness server that schedules payload update at fixed intervals random content (range) recipe --- lib/origin_simulator/flakiness.ex | 15 ++++++++++++--- lib/origin_simulator/simulation.ex | 4 ++-- test/origin_simulator/flakiness_test.exs | 11 +++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/origin_simulator/flakiness.ex b/lib/origin_simulator/flakiness.ex index b95d58e..19b799e 100644 --- a/lib/origin_simulator/flakiness.ex +++ b/lib/origin_simulator/flakiness.ex @@ -22,8 +22,8 @@ defmodule OriginSimulator.Flakiness do GenServer.start_link(__MODULE__, opts, name: :flakiness) end - def new(), do: %Flakiness{} - def new(payload_series, route), do: %Flakiness{payload: payload_series, route: route} + def new(), do: %__MODULE__{} + def new(payload_series, route), do: %__MODULE__{payload: payload_series, route: route} def start(%{random_content: value}, route) do String.split(value, "..") @@ -56,7 +56,7 @@ defmodule OriginSimulator.Flakiness do @impl true def handle_call(:start, _from, flakiness) do - send(self(), :flaky) + schedule_flakiness() {:reply, :ok, flakiness} end @@ -72,6 +72,15 @@ defmodule OriginSimulator.Flakiness do {:update, {flakiness.route, flakiness.payload |> Enum.random()}} ) + schedule_flakiness(flakiness.interval) + {:noreply, flakiness} end + + defp schedule_flakiness(interval \\ 0) + defp schedule_flakiness(nil), do: :ok + + defp schedule_flakiness(interval) do + Process.send_after(self(), :flaky, interval) + end end diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index ab2b445..197c998 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -112,8 +112,6 @@ defmodule OriginSimulator.Simulation do route = new_recipe.route simulation = get(state[route]) - if auto_flakiness?(new_recipe), do: Flakiness.start(new_recipe, route) - Enum.map(new_recipe.stages, fn item -> Process.send_after( self(), @@ -122,6 +120,8 @@ defmodule OriginSimulator.Simulation do ) end) + if auto_flakiness?(new_recipe), do: Flakiness.start(new_recipe, route) + {:reply, :ok, Map.put(state, route, %{simulation | recipe: new_recipe})} end diff --git a/test/origin_simulator/flakiness_test.exs b/test/origin_simulator/flakiness_test.exs index 5011710..fa1f7a5 100644 --- a/test/origin_simulator/flakiness_test.exs +++ b/test/origin_simulator/flakiness_test.exs @@ -46,4 +46,15 @@ defmodule OriginSimulator.FlakinessTest do route: "/route_for_random_payloads" } end + + test "current flakiness payload is within the intended random range" do + recipe = random_content_recipe("50kb..100kb", %{"content-encoding" => "gzip"}, "/route_for_random_payloads") + + Simulation.add_recipe(@simulation_server, recipe) + Process.sleep(50) + + {_status, _latency, {_route, payload}} = Simulation.state(@simulation_server, "/route_for_random_payloads") + + assert payload in Flakiness.state().payload + end end From 242b9fcf3ef687b4ceb20fdf148b434724341146 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 7 Aug 2020 15:56:43 +0100 Subject: [PATCH 8/8] random content (range) example recipe and README --- README.md | 17 +++++++++++++++++ ...permanent_200_random_content_range_gzip.json | 16 ++++++++++++++++ test/origin_simulator/flakiness_test.exs | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 examples/permanent_200_random_content_range_gzip.json diff --git a/README.md b/README.md index 69d627b..73970ff 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,24 @@ In this example we are requiring a continuous successful response with no simula ```json { + "route": "/*", "random_content": "428kb", + "stages": [ + { + "at": 0, + "latency": 0, + "status": 200 + } + ] +} +``` + +To simulate serving payloads of different sizes, a random content range recipe can be used. For example, the example below enables OriginSimulator to serve payloads of random and varying sizes within a range of 300kb and 400kb. + +```json +{ + "route": "/*", + "random_content": "300kb..400kb", "stages": [ { "at": 0, diff --git a/examples/permanent_200_random_content_range_gzip.json b/examples/permanent_200_random_content_range_gzip.json new file mode 100644 index 0000000..7319e9a --- /dev/null +++ b/examples/permanent_200_random_content_range_gzip.json @@ -0,0 +1,16 @@ +[ + { + "stages": [ + { + "status": 200, + "latency": 0, + "at": 0 + } + ], + "route": "/*", + "random_content": "300kb..400kb", + "headers": { + "content-encoding": "gzip" + } + } +] \ No newline at end of file diff --git a/test/origin_simulator/flakiness_test.exs b/test/origin_simulator/flakiness_test.exs index fa1f7a5..489c9b8 100644 --- a/test/origin_simulator/flakiness_test.exs +++ b/test/origin_simulator/flakiness_test.exs @@ -10,7 +10,7 @@ defmodule OriginSimulator.FlakinessTest do assert Flakiness.new() == %Flakiness{interval: nil, payload: [], route: "/*", status: []} end - test "new/2 returns Flakiness struct with random payload series and route" do + test "new/2 returns a Flakiness struct for random payload series and route" do flakiness = Flakiness.new([150, 155, 160], "/an_origin_route") assert flakiness.payload == [150, 155, 160]