From e5075f744858a4cd1e8e964a9ae1af9f6558d8d9 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 18 Sep 2020 16:01:40 +0100 Subject: [PATCH 1/3] provides documentation for key modules, switch off documentation and hexdocs listing for non essential modules --- lib/origin_simulator.ex | 7 ++++ lib/origin_simulator/admin_router.ex | 30 ++++++++++++++ lib/origin_simulator/body.ex | 39 +++++++++++++++++++ lib/origin_simulator/counter.ex | 2 + lib/origin_simulator/duration.ex | 1 + lib/origin_simulator/http/client.ex | 1 + lib/origin_simulator/http/mock_client.ex | 1 + lib/origin_simulator/plug/response_counter.ex | 2 + lib/origin_simulator/size.ex | 1 + 9 files changed, 84 insertions(+) diff --git a/lib/origin_simulator.ex b/lib/origin_simulator.ex index 5281689..3680172 100644 --- a/lib/origin_simulator.ex +++ b/lib/origin_simulator.ex @@ -1,4 +1,8 @@ defmodule OriginSimulator do + @moduledoc """ + Main router for handling and responding to OriginSimulator requests. + """ + use Plug.Router alias OriginSimulator.{Payload, Simulation, Plug.ResponseCounter} @@ -20,6 +24,7 @@ defmodule OriginSimulator do send_resp(conn, 404, "not found") end + @doc false def admin_domain(), do: Application.get_env(:origin_simulator, :admin_domain) defp serve_payload(conn, route) do @@ -52,9 +57,11 @@ defmodule OriginSimulator do defp sleep(%Range{} = time), do: :timer.sleep(Enum.random(time)) defp sleep(duration), do: :timer.sleep(duration) + @doc false def recipe_not_set(), do: "Recipe not set, please POST a recipe to /#{admin_domain()}/add_recipe" + @doc false def recipe_not_set(path) do "Recipe not set at #{path}, please POST a recipe for this route to /#{admin_domain()}/add_recipe" end diff --git a/lib/origin_simulator/admin_router.ex b/lib/origin_simulator/admin_router.ex index 9fb95cf..120b0da 100644 --- a/lib/origin_simulator/admin_router.ex +++ b/lib/origin_simulator/admin_router.ex @@ -1,4 +1,34 @@ defmodule OriginSimulator.AdminRouter do + @moduledoc """ + Router for handling and responding to admin requests. + + #### Admin routes + + * /_admin/status + + Check if the simulator is running, return `ok!` + + * /_admin/add_recipe + + Post (POST) recipe: update or create new origins + + * /_admin/current_recipe + + List existing recipe for all origins and routes + + * /_admin/restart + + Reset the simulator: remove all recipes + + * /_admin/routes + + List all origins and routes + + * /_admin/routes_status + + List all origin and routes with the corresponding current status and latency values + """ + use Plug.Router alias OriginSimulator.{Recipe, Simulation, Counter} diff --git a/lib/origin_simulator/body.ex b/lib/origin_simulator/body.ex index 38a016b..a8b3b45 100644 --- a/lib/origin_simulator/body.ex +++ b/lib/origin_simulator/body.ex @@ -1,8 +1,35 @@ defmodule OriginSimulator.Body do + @moduledoc """ + Utilities for generating OriginSimulator recipe response payloads (body). + """ @regex ~r"<<(.+?)>>" alias OriginSimulator.Size + @doc """ + Parse a string value to include generated and compressed random payload if required. + + OriginSimulator response payload can be defined in recipes. The payload + can also include random content specified with tags, e.g. <<10kb>>. + + ``` + iex> OriginSimulator.Body.parse("{\"payload\":\"test\"}") + "{\"payload\":\"test\"}" + + # generate random content with <<100b>> + iex> OriginSimulator.Body.parse("{\"payload\":\"<<100b>>\"}") + "{\"payload\":\"iDS0MMNKT3QMmEiOPvjeIsEXB7cjGlGktCLCMta3D8ZleSHbcbUn1mNa470POxzDJAhJvP4L3cDhDvwBP5eQC8fGMo3DCgewZzBv\"}" + ``` + + Payload can be compressed with a `%{"content-encoding" => "gzip"}` header. + ``` + iex> OriginSimulator.Body.parse("{\"payload\":\"<<100b>>\"}", %{"content-encoding" => "gzip"}) + <<31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 171, 86, 42, 72, 172, 204, 201, 79, 76, 81, + 178, 82, 50, 8, 115, 214, 15, 169, 114, 245, 74, 244, 204, 205, 44, 54, 52, + 174, 242, 170, 204, 244, 11, 171, 172, 50, 205, 201, 43, 54, ...>> + ``` + """ + @spec parse(binary(), map()) :: binary() def parse(str, headers \\ %{}) def parse(str, %{"content-encoding" => "gzip"}) do @@ -14,6 +41,18 @@ defmodule OriginSimulator.Body do Regex.replace(@regex, str, fn _whole, tag -> randomise(tag) end) end + @doc """ + Generate and compress random payload. + + ``` + # generate 100kb random payload in gzip format + iex(1)> OriginSimulator.Body.randomise("100kb", %{"content-encoding" => "gzip"}) + <<31, 139, 8, 0, 0, 0, 0, 0, 0, 19, 20, 154, 69, 178, 227, 64, 16, 5, 15, 228, + 133, 152, 150, 98, 102, 214, 78, 204, 204, 58, 253, 252, 89, 59, 28, 182, 186, + 171, 222, 203, 116, 88, 158, 88, 201, 207, 21, 14, 52, 48, ...>> + ``` + """ + @spec randomise(binary(), map()) :: binary() def randomise(tag, headers \\ %{}) def randomise(tag, %{"content-encoding" => "gzip"}), do: randomise(tag, %{}) |> :zlib.gzip() diff --git a/lib/origin_simulator/counter.ex b/lib/origin_simulator/counter.ex index b644697..8c7eabc 100644 --- a/lib/origin_simulator/counter.ex +++ b/lib/origin_simulator/counter.ex @@ -1,4 +1,6 @@ defmodule OriginSimulator.Counter do + @moduledoc false + use Agent @initial_state %{total_requests: 0} diff --git a/lib/origin_simulator/duration.ex b/lib/origin_simulator/duration.ex index 463fe73..7e6b5cb 100644 --- a/lib/origin_simulator/duration.ex +++ b/lib/origin_simulator/duration.ex @@ -1,4 +1,5 @@ defmodule OriginSimulator.Duration do + @moduledoc false def parse(0), do: 0 def parse(time) when is_integer(time) do diff --git a/lib/origin_simulator/http/client.ex b/lib/origin_simulator/http/client.ex index a215fa8..68359b9 100644 --- a/lib/origin_simulator/http/client.ex +++ b/lib/origin_simulator/http/client.ex @@ -1,4 +1,5 @@ defmodule OriginSimulator.HTTP.Client do + @moduledoc false def get(endpoint, headers \\ %{}) def get(endpoint, %{"content-encoding" => "gzip"} = headers) do diff --git a/lib/origin_simulator/http/mock_client.ex b/lib/origin_simulator/http/mock_client.ex index 76251a0..ca3a707 100644 --- a/lib/origin_simulator/http/mock_client.ex +++ b/lib/origin_simulator/http/mock_client.ex @@ -1,4 +1,5 @@ defmodule OriginSimulator.HTTP.MockClient do + @moduledoc false def get(_endpoint, headers \\ %{}) def get(_endpoint, %{"content-type" => "application/json"}), do: {:ok, %HTTPoison.Response{body: "{\"hello\":\"world\"}"}} def get(_endpoint, %{"content-encoding" => "gzip"}), do: {:ok, %HTTPoison.Response{body: :zlib.gzip("some content from origin")}} diff --git a/lib/origin_simulator/plug/response_counter.ex b/lib/origin_simulator/plug/response_counter.ex index c52e691..ca656bf 100644 --- a/lib/origin_simulator/plug/response_counter.ex +++ b/lib/origin_simulator/plug/response_counter.ex @@ -1,4 +1,6 @@ defmodule OriginSimulator.Plug.ResponseCounter do + @moduledoc false + @behaviour Plug import Plug.Conn, only: [register_before_send: 2] diff --git a/lib/origin_simulator/size.ex b/lib/origin_simulator/size.ex index d32cabd..02e680a 100644 --- a/lib/origin_simulator/size.ex +++ b/lib/origin_simulator/size.ex @@ -1,4 +1,5 @@ defmodule OriginSimulator.Size do + @moduledoc false def parse(size) when is_binary(size) do Integer.parse(size) |> parse() end From 12acef8083724593bed80e465510bd0dc4eadad3 Mon Sep 17 00:00:00 2001 From: Boon Low Date: Mon, 21 Sep 2020 11:37:10 +0100 Subject: [PATCH 2/3] provide documentation and typespec for Payload, Recipe, Simulation modules, recipe mix task --- lib/mix/upload_recipe.ex | 13 ++++++ lib/origin_simulator/payload.ex | 25 ++++++++++- lib/origin_simulator/recipe.ex | 13 ++++++ lib/origin_simulator/simulation.ex | 66 ++++++++++++++++++++++++++++++ lib/origin_simulator/supervisor.ex | 4 ++ 5 files changed, 120 insertions(+), 1 deletion(-) diff --git a/lib/mix/upload_recipe.ex b/lib/mix/upload_recipe.ex index 0b552e1..e0e2d4c 100644 --- a/lib/mix/upload_recipe.ex +++ b/lib/mix/upload_recipe.ex @@ -1,6 +1,19 @@ defmodule Mix.Tasks.UploadRecipe do + @moduledoc """ + A mix task for uploading JSON recipes in the `examples` directory to OriginSimulator. + + ``` + # upload `examples/demo.json` to OriginSimulator running locally (http://localhost:8080). + mix upload_recipe demo + + # upload `examples/demo.json to OriginSimulator on a specific host. + mix upload_recipe "http://origin-simulator.com" demo + ``` + """ + use Mix.Task + @spec run(list()) :: {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} | {:error, HTTPoison.Error.t()} def run([host, recipe]) do {:ok, _started} = Application.ensure_all_started(:httpoison) diff --git a/lib/origin_simulator/payload.ex b/lib/origin_simulator/payload.ex index b7dbc58..d3170b8 100644 --- a/lib/origin_simulator/payload.ex +++ b/lib/origin_simulator/payload.ex @@ -1,16 +1,35 @@ defmodule OriginSimulator.Payload do - use GenServer + @moduledoc """ + Server for fetching payload from origin, storing and serving payloads. + + Recipe payload is pre-created and stored in memory + ([Erlang ETS](https://erlang.org/doc/man/ets.html)) when the + recipe is upload to OriginSimulator. This module provides API to + fetch and store payload from origins specified in recipe so that + payload can be served repeatedly during simulation without hitting + the simulated origins. It also deals with body / random content payloads + if these are specified in recipe. + """ + use GenServer alias OriginSimulator.{Body, Recipe} @http_client Application.get_env(:origin_simulator, :http_client) + @type server :: pid() | :atom + @type recipe :: OriginSimulator.Recipe.t() + ## Client API + @doc false def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: :payload) end + @doc """ + Fetch (from origin) or generate payload specified in recipe for in-memory storage. + """ + @spec fetch(server, recipe) :: :ok def fetch(server, %Recipe{origin: value, route: route} = recipe) when is_binary(value) do GenServer.call(server, {:fetch, recipe, route}) end @@ -24,6 +43,10 @@ defmodule OriginSimulator.Payload do GenServer.call(server, {:generate, recipe, route}) end + @doc """ + Retrieve a payload from server for a given path and matching route. + """ + @spec body(server, integer(), binary(), binary()) :: {:ok, term()} | {:error, binary()} def body(_server, status, path \\ Recipe.default_route(), route \\ Recipe.default_route()) do case {status, path} do {200, _} -> cache_lookup(route) diff --git a/lib/origin_simulator/recipe.ex b/lib/origin_simulator/recipe.ex index 43ba8d6..a55545d 100644 --- a/lib/origin_simulator/recipe.ex +++ b/lib/origin_simulator/recipe.ex @@ -1,4 +1,13 @@ defmodule OriginSimulator.Recipe do + @moduledoc """ + Data struct and functions underpinning OriginSimulator recipes. + + A recipe defines the different stages of a [simulation scenario](readme.html#scenarios). + It is a JSON that can be uploaded to OriginSimulator via HTTP POST. The recipe is + represented internally as a struct. This module also provides a function to parse + JSON recipe into struct. + """ + defstruct origin: nil, body: nil, random_content: nil, headers: %{}, stages: [], route: "/*" @type t :: %__MODULE__{ @@ -9,11 +18,15 @@ defmodule OriginSimulator.Recipe do route: String.t() } + @doc """ + Parse a JSON recipe into `t:OriginSimulator.Recipe.t/0` data struct. + """ # TODO: parameters don't make sense, need fixing @spec parse({:ok, binary(), any()}) :: binary() def parse({:ok, "[" <> body, _conn}), do: Poison.decode!("[" <> body, as: [%__MODULE__{}]) def parse({:ok, body, _conn}), do: Poison.decode!(body, as: %__MODULE__{}) + @doc false @spec default_route() :: binary() def default_route(), do: %__MODULE__{}.route end diff --git a/lib/origin_simulator/simulation.ex b/lib/origin_simulator/simulation.ex index 1e22d5b..3469ffd 100644 --- a/lib/origin_simulator/simulation.ex +++ b/lib/origin_simulator/simulation.ex @@ -1,38 +1,100 @@ defmodule OriginSimulator.Simulation do + @moduledoc """ + Server facilitating simulation recipe usage before and during load tests. + """ + use GenServer alias OriginSimulator.{Recipe, Payload, Duration} + @type latency :: integer() + @type recipe :: OriginSimulator.Recipe.t() + @type status :: integer() + + @type route :: binary() + @type server :: :simulation | module() + @type simulation_state :: %{required(:latency) => latency, required(:recipe) => recipe, required(:status) => status} + ## Client API + @doc false def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: :simulation) end + @doc """ + Retrieve the simulation state for all routes. + """ + @spec state(server) :: %{required(route) => simulation_state} def state(server) do GenServer.call(server, :state) end + @doc """ + Retrieve the latency and status data for a specific route. + + ``` + iex> OriginSimulator.Simulation.state(:simulation, "/news") + {200, 100} + ``` + """ + @spec state(server, route) :: {status, latency} def state(server, route) do GenServer.call(server, {:state, route}) end + @doc """ + Retrieve all recipes uploaded to OriginSimulator. + """ + @spec recipe(server) :: list(recipe) def recipe(server) do GenServer.call(server, :recipe) end + @doc """ + Retrieve the recipe for a specific route. + """ + @spec recipe(server, route) :: recipe def recipe(server, route) do GenServer.call(server, {:recipe, route}) end + @doc """ + Find a matching recipe route pattern for a given path. + + OriginSimulator is capable of serving multiple simulation recipes + on multiple routes which could also be wildcard routes. For example: + + ``` + iex> OriginSimulator.Simulation.route(:simulation, "/news/weather") + "/news*" + ``` + + The matching route is used for retrieving simulation state (latency, status) + in `state/2`. + """ + @spec route(server, route) :: route def route(server, route) do GenServer.call(server, {:route, route}) end + @doc """ + Retrieve a list of simulated routes. + + ``` + iex> OriginSimulator.Simulation.route(:simulation) + ["/*", "/example/endpoint", "/news"] + ``` + """ + @spec route(server, route) :: list(route) def route(server) do GenServer.call(server, :route) end + @doc """ + Add a recipe or a list of recipes to the server. + """ + @spec add_recipe(server, recipe | list(recipe)) :: :ok | :error def add_recipe(server, recipes) when is_list(recipes) do resp = for recipe <- recipes do @@ -46,6 +108,10 @@ defmodule OriginSimulator.Simulation do GenServer.call(server, {:add_recipe, new_recipe}) end + @doc """ + Deletes simulation state including recipes and restart server. + """ + @spec restart() :: :ok def restart do GenServer.stop(:simulation) end diff --git a/lib/origin_simulator/supervisor.ex b/lib/origin_simulator/supervisor.ex index 007eb0e..b43c1aa 100644 --- a/lib/origin_simulator/supervisor.ex +++ b/lib/origin_simulator/supervisor.ex @@ -1,4 +1,8 @@ defmodule OriginSimulator.Supervisor do + @moduledoc """ + `Supervisor` of OriginSimulator server processes. + """ + use Supervisor def start_link(init_arg) do From 98d04f0d3b65b27a496afadcc60fc64dc028d58a Mon Sep 17 00:00:00 2001 From: Boon Low Date: Fri, 2 Oct 2020 08:58:15 +0100 Subject: [PATCH 3/3] add CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03de335 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v1.0.0 (2020-10-02) + +* Initial release