Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

random content range recipe #30

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions examples/permanent_200_random_content_range_gzip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"stages": [
{
"status": 200,
"latency": 0,
"at": 0
}
],
"route": "/*",
"random_content": "300kb..400kb",
"headers": {
"content-encoding": "gzip"
}
}
]
4 changes: 2 additions & 2 deletions lib/origin_simulator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
86 changes: 86 additions & 0 deletions lib/origin_simulator/flakiness.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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: %__MODULE__{}
def new(payload_series, route), do: %__MODULE__{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
schedule_flakiness()
{: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()}}
)

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
42 changes: 40 additions & 2 deletions lib/origin_simulator/payload.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ defmodule OriginSimulator.Payload do

@http_client Application.get_env(:origin_simulator, :http_client)

@random_payload_step_size 5
@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
Expand All @@ -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

Expand All @@ -34,6 +47,17 @@ defmodule OriginSimulator.Payload do
end
end

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
[{^route, body}] -> {:ok, body}
Expand All @@ -53,6 +77,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

Expand All @@ -63,6 +88,19 @@ defmodule OriginSimulator.Payload do
{:reply, :ok, state}
end

@impl true
def handle_call({:generate, %{random_content: [min, max]} = recipe, route}, _from, state) do
:ets.insert(:payload, {route, Body.randomise(max, recipe.headers)})

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)})
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)})
Expand Down
1 change: 1 addition & 0 deletions lib/origin_simulator/recipe.ex
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
50 changes: 40 additions & 10 deletions lib/origin_simulator/simulation.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
defmodule OriginSimulator.Simulation do
use GenServer

alias OriginSimulator.{Recipe, Payload, Duration}
alias OriginSimulator.{Recipe, Payload, Duration, Simulation, Flakiness}

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

def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: :simulation)
end

def new(), do: %Simulation{}

def state(server) do
GenServer.call(server, :state)
end
Expand Down Expand Up @@ -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
Expand All @@ -64,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}
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
Expand All @@ -94,11 +115,13 @@ 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)

if auto_flakiness?(new_recipe), do: Flakiness.start(new_recipe, route)

{:reply, :ok, Map.put(state, route, %{simulation | recipe: new_recipe})}
end

Expand All @@ -111,14 +134,21 @@ 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: default_simulation()
defp get(current_state), do: current_state
@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 default_simulation(), do: %{recipe: nil, status: 406, latency: 0}
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

defp match_route(state, nil, route) do
Map.keys(state)
Expand Down
3 changes: 2 additions & 1 deletion lib/origin_simulator/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ defmodule OriginSimulator.Supervisor do
children = [
OriginSimulator.Simulation,
OriginSimulator.Payload,
OriginSimulator.Counter
OriginSimulator.Counter,
OriginSimulator.Flakiness
]

opts = [
Expand Down
5 changes: 3 additions & 2 deletions test/fixtures/recipes.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading