From 1248eeb88d90f0ece972299d40b7406ef0a17cf5 Mon Sep 17 00:00:00 2001 From: kballou Date: Thu, 28 Jul 2016 16:44:32 -0600 Subject: Add HTTP "Ping" --- config/test.exs | 7 ++++++ lib/exping.ex | 20 +++++++++++++++++ lib/exping/http.ex | 45 +++++++++++++++++++++++++++++++++++++ lib/exping/http/task.ex | 45 +++++++++++++++++++++++++++++++++++++ lib/exping/supervisor.ex | 1 + test/http_test.exs | 36 ++++++++++++++++++++++++++++++ test/support/test_http_client.ex | 48 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+) create mode 100644 lib/exping/http.ex create mode 100644 lib/exping/http/task.ex create mode 100644 test/http_test.exs create mode 100644 test/support/test_http_client.ex diff --git a/config/test.exs b/config/test.exs index d2d855e..2f71b61 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1 +1,8 @@ use Mix.Config + +config :exping, http_client: ExPing.Test.HTTPClient + +config :exping, :http, + timeout: 10 + +config :logger, level: :warn diff --git a/lib/exping.ex b/lib/exping.ex index 056bfda..c63c37a 100644 --- a/lib/exping.ex +++ b/lib/exping.ex @@ -1,5 +1,25 @@ defmodule ExPing do + require Logger @moduledoc """ Public API for ExPing """ + + @pings [&ExPing.HTTP.get/1, &ExPing.HTTP.head/1] + + @spec ping(URI.t) :: boolean + def ping(address) do + :ok = Logger.info("Attempting to ping #{inspect(address)}") + @pings + |> Enum.map(&spawn_ping(&1, address)) + |> Enum.all? + end + + @spec spawn_ping(fun, URI.t) :: boolean + defp spawn_ping(ping, address) do + case ping.(address) do + {:ok, _} -> true + {:error, _} -> false + end + end + end diff --git a/lib/exping/http.ex b/lib/exping/http.ex new file mode 100644 index 0000000..576562f --- /dev/null +++ b/lib/exping/http.ex @@ -0,0 +1,45 @@ +defmodule ExPing.HTTP do + require Logger + @moduledoc """ + Provides basic HTTP client for pinging endpoints + """ + + @timeout Application.get_env(:exping, :http)[:timeout] || 5000 + + @spec head(URI.t) :: {:ok, {integer, binary}} | {:error, term} + def head(endpoint) do + ref = make_ref() + :ok = Logger.info("Sending HEAD request to #{inspect(endpoint)}") + {:ok, _} = spawn_http_task(:head, [endpoint, ref, self()]) + + receive do + {:http_task_resp, ^ref, {:ok, {_, _}} = resp} -> resp + {:http_task_resp, ^ref, {:error, _} = error} -> error + after @timeout -> + {:error, :timeout} + end + end + + @spec get(URI.t) :: {:ok, {integer, binary}} | {:error, term} + def get(endpoint) do + ref = make_ref() + :ok = Logger.info("Sending GET request to #{inspect(endpoint)}") + {:ok, _} = spawn_http_task(:get, [endpoint, ref, self()]) + + receive do + {:http_task_resp, ^ref, {:ok, _} = resp} -> resp + {:http_task_resp, ^ref, {:error, _} = error} -> error + after @timeout -> + {:error, :timeout} + end + end + + defp spawn_http_task(method, args) do + Task.Supervisor.start_child( + ExPing.Supervisor.Task, + ExPing.HTTP.Task, + method, + args) + end + +end diff --git a/lib/exping/http/task.ex b/lib/exping/http/task.ex new file mode 100644 index 0000000..f9d39f5 --- /dev/null +++ b/lib/exping/http/task.ex @@ -0,0 +1,45 @@ +defmodule ExPing.HTTP.Task do + require Logger + @moduledoc """ + Task module for performing HTTP requests + """ + + @user_agent {'user-agent', 'exping'} + + @http Application.get_env(:exping, :http_client) || :httpc + + @type response :: {integer, binary} + + @spec head(URI.t, reference, pid) :: {:ok, response} | {:error, term} + def head(%URI{} = uri, ref, owner) do + :head + |> @http.request({endpoint(uri), headers()}, [], []) + |> process_response(ref, owner) + end + + @spec get(URI.t, reference, pid) :: {:ok, response} | {:error, term} + def get(%URI{} = uri, ref, owner) do + :get + |> @http.request({endpoint(uri), headers()}, [], []) + |> process_response(ref, owner) + end + + defp process_response({:ok, {{_, code, _}, _, body}}, ref, owner) do + :ok = Logger.info("HTTP request returned: #{code}") + send(owner, {:http_task_resp, ref, {:ok, {code, body}}}) + end + + defp process_response({:error, _} = error, ref, owner) do + :ok = Logger.warn("HTTP request returned error: #{inspect(error)}") + send(owner, {:http_task_resp, ref, error}) + end + + defp endpoint(%URI{} = uri) do + uri |> to_string() |> String.to_charlist + end + + defp headers do + [@user_agent] + end + +end diff --git a/lib/exping/supervisor.ex b/lib/exping/supervisor.ex index 403f35c..faf6ccb 100644 --- a/lib/exping/supervisor.ex +++ b/lib/exping/supervisor.ex @@ -13,6 +13,7 @@ defmodule ExPing.Supervisor do @spec init(any) :: no_return def init(_) do children = [ + supervisor(Task.Supervisor, [[name: ExPing.Supervisor.Task]]) ] supervise(children, strategy: :one_for_one, name: __MODULE__) diff --git a/test/http_test.exs b/test/http_test.exs new file mode 100644 index 0000000..61847d4 --- /dev/null +++ b/test/http_test.exs @@ -0,0 +1,36 @@ +defmodule ExPing.HTTP.Test do + use ExUnit.Case + + describe "head requests" do + test "can get ok response" do + for code <- [200, 405] do + {:ok, _} = ExPing.HTTP.head(URI.parse("localhost/#{code}")) + end + end + + test "can get error response" do + {:error, _} = ExPing.HTTP.head(URI.parse("localhost/500")) + end + + test "can get timeout response" do + {:error, :timeout} = ExPing.HTTP.head(URI.parse("localhost/timeout")) + end + end + + describe "get requests" do + test "can get ok response" do + for code <- [200, 400, 404] do + {:ok, _} = ExPing.HTTP.get(URI.parse("localhost/#{code}")) + end + end + + test "can get error response" do + {:error, _} = ExPing.HTTP.get(URI.parse("localhost/500")) + end + + test "can get timeout response" do + {:error, :timeout} = ExPing.HTTP.get(URI.parse("localhost/timeout")) + end + end + +end diff --git a/test/support/test_http_client.ex b/test/support/test_http_client.ex new file mode 100644 index 0000000..24ca509 --- /dev/null +++ b/test/support/test_http_client.ex @@ -0,0 +1,48 @@ +defmodule ExPing.Test.HTTPClient do + @moduledoc """ + Mock HTTP client provides interface similar to `:httpc`. + """ + + def request(url), do: request(:get, {url, []}, [], []) + def request(:head, {url, _}, _, _) do + cond do + String.contains?(to_string(url), "/200") -> + {:ok, {status_line(200), [], []}} + String.contains?(to_string(url), "/405") -> + {:ok, {status_line(405), [], []}} + String.contains?(to_string(url), "/500") -> + Process.exit(self, :kill) + String.contains?(to_string(url), "/timeout") -> + :timer.sleep(20) + {:ok, {status_line(500), [], []}} + true -> + {:ok, {status_line(404), [], []}} + end + end + def request(:get, {url, _}, _, _) do + cond do + String.contains?(to_string(url), "/200") -> + {:ok, {status_line(200), [], []}} + String.contains?(to_string(url), "/400") -> + {:ok, {status_line(400), [], []}} + String.contains?(to_string(url), "/500") -> + Process.exit(self, :kill) + String.contains?(to_string(url), "/timeout") -> + :timer.sleep(20) + {:ok, {status_line(500), [], []}} + true -> + {:ok, {status_line(404), [], []}} + end + {:ok, {status_line(404), [], []}} + end + + for {code, reason} <- [{200, 'OK'}, + {400, 'Bad Request'}, + {404, 'Not Found'}, + {405, 'Method Not Allowed'}, + {500, 'Internal Server Error'}] do + defp status_line(unquote(code)) do + {'HTTP/1.1', unquote(code), unquote(reason)} + end + end +end -- cgit v1.2.1