Skip to content

Commit 7c3bbef

Browse files
committed
Base cache off of plug_mint_proxy
The cache now bases itself off of the plug_mint_proxy, making it slightly faster and somewhat easier to maintain. It is now in line with use on mu-dispatcher and mu-identifier.
1 parent 1949d1e commit 7c3bbef

File tree

6 files changed

+241
-130
lines changed

6 files changed

+241
-130
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule Manipulators.CacheKeyLogger do
2+
@moduledoc """
3+
Manipulates the response, logging the cache keys and the clear keys
4+
if this was requested by configuration.
5+
"""
6+
@behaviour ProxyManipulator
7+
8+
@impl true
9+
def headers(headers, _) do
10+
maybe_log_cache_keys(headers)
11+
maybe_log_clear_keys(headers)
12+
13+
:skip
14+
end
15+
16+
@impl true
17+
def chunk(_, _), do: :skip
18+
19+
@impl true
20+
def finish(_, _), do: :skip
21+
22+
defp maybe_log_clear_keys(headers) do
23+
if Application.get_env(:mu_cache, :log_clear_keys) do
24+
clear_keys = header_value(headers, "clear-keys")
25+
26+
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
27+
IO.inspect(clear_keys, label: "Clear keys")
28+
end
29+
end
30+
31+
defp maybe_log_cache_keys(headers) do
32+
if Application.get_env(:mu_cache, :log_cache_keys) do
33+
cache_keys = header_value(headers, "cache-keys")
34+
35+
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
36+
IO.inspect(cache_keys, label: "Cache keys")
37+
end
38+
end
39+
40+
defp header_value(headers, header_name) do
41+
header = Enum.find(headers, header_name)
42+
43+
if header do
44+
elem(header, 1)
45+
end
46+
end
47+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule Manipulators.RemoveCacheRelatedKeys do
2+
@moduledoc """
3+
Removes cache-keys and clear-keys from the incoming connection.
4+
5+
The intended use for this is as a response manipulator, removing
6+
cache keys and clear keys from the server response.
7+
"""
8+
9+
@behaviour ProxyManipulator
10+
11+
@impl true
12+
def headers(headers, connection) do
13+
new_headers =
14+
headers
15+
|> Enum.reject(fn
16+
{"cache-keys", _} -> true
17+
{"clear-keys", _} -> true
18+
_ -> false
19+
end)
20+
21+
{new_headers, connection}
22+
end
23+
24+
@impl true
25+
def chunk(_, _), do: :skip
26+
27+
@impl true
28+
def finish(_, _), do: :skip
29+
end

lib/manipulators/store_response.ex

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
defmodule Manipulators.StoreResponse do
2+
alias Cache.Registry, as: Cache
3+
4+
@moduledoc """
5+
Stores the response in a shared cache for reuse.
6+
7+
This manipulator must run as a response manipulator because it needs
8+
the full response.
9+
10+
We store the information on what is `conn_out` in from this
11+
manipulator's perspective as we can hopefully keep that alive in the
12+
future, even when the client connection has dropped out. Our
13+
experimentation shows that to be the Mint request, thus the
14+
connection to the `backend` hostname.
15+
"""
16+
17+
@behaviour ProxyManipulator
18+
19+
@impl true
20+
def headers(headers, {conn_in, conn_out}) do
21+
should_cache = Enum.any?(headers, &match?({"cache-keys", _}, &1))
22+
should_clear = Enum.any?(headers, &match?({"clear-keys", _}, &1))
23+
24+
if should_cache or should_clear do
25+
conn_out_with_headers =
26+
conn_out
27+
|> Mint.HTTP.put_private(:mu_cache_should_cache, should_cache)
28+
|> Mint.HTTP.put_private(:mu_cache_should_clear, should_clear)
29+
|> Mint.HTTP.put_private(:mu_cache_original_headers, headers)
30+
31+
{headers, {conn_in, conn_out_with_headers}}
32+
else
33+
:skip
34+
end
35+
end
36+
37+
@impl true
38+
def chunk(chunk, {conn_in, conn_out}) do
39+
should_cache = Mint.HTTP.get_private(conn_out, :mu_cache_should_cache)
40+
41+
if should_cache do
42+
current_chunks = Mint.HTTP.get_private(conn_out, :mu_cache_chunks, [])
43+
# reversed, see finish
44+
new_chunks = [chunk | current_chunks]
45+
46+
conn_out_with_new_chunks = Mint.HTTP.put_private(conn_out, :mu_cache_chunks, new_chunks)
47+
48+
{chunk, {conn_in, conn_out_with_new_chunks}}
49+
else
50+
:skip
51+
end
52+
end
53+
54+
@impl true
55+
def finish(_, {conn_in, conn_out}) do
56+
has_cache = Mint.HTTP.get_private(conn_out, :mu_cache_should_cache)
57+
58+
cond do
59+
has_cache ->
60+
reversed_chunks = Mint.HTTP.get_private(conn_out, :mu_cache_chunks, [])
61+
response_body = Enum.reduce(reversed_chunks, "", &(&1 <> &2))
62+
63+
all_response_headers = Mint.HTTP.get_private(conn_out, :mu_cache_original_headers)
64+
65+
allowed_groups =
66+
all_response_headers
67+
|> Enum.find({nil, "[]"}, &match?({"mu-auth-allowed-groups", _}, &1))
68+
|> elem(1)
69+
70+
cache_keys =
71+
all_response_headers
72+
|> Enum.find({nil, "[]"}, &match?({"cache-keys", _}, &1))
73+
|> elem(1)
74+
|> Poison.decode!()
75+
76+
clear_keys =
77+
all_response_headers
78+
|> Enum.find({nil, "[]"}, &match?({"clear-keys", _}, &1))
79+
|> elem(1)
80+
|> Poison.decode!()
81+
82+
# IO.inspect( {conn_in.method, conn_in.request_path, conn_in.query_string, allowed_groups}, label: "Signature to store" )
83+
84+
Cache.store(
85+
{conn_in.method, conn_in.request_path, conn_in.query_string, allowed_groups},
86+
%{
87+
body: response_body,
88+
headers:
89+
Enum.reject(all_response_headers, fn
90+
{"cache-keys", _} -> true
91+
{"clear-keys", _} -> true
92+
_ -> false
93+
end),
94+
status_code: conn_in.status,
95+
cache_keys: cache_keys,
96+
clear_keys: clear_keys,
97+
allowed_groups: allowed_groups
98+
}
99+
)
100+
101+
Mint.HTTP.get_private(conn_out, :mu_cache_should_clear) ->
102+
# If we don't cache a response, we need to send the clear keys
103+
# in another way.
104+
all_response_headers = Mint.HTTP.get_private(conn_out, :mu_cache_original_headers)
105+
106+
clear_keys =
107+
all_response_headers
108+
|> Enum.find({nil, "[]"}, &match?({"clear-keys", _}, &1))
109+
|> elem(1)
110+
|> Poison.decode!()
111+
112+
IO.inspect(clear_keys, label: "Clear keys")
113+
114+
Cache.clear_keys(clear_keys)
115+
116+
true ->
117+
nil
118+
end
119+
120+
:skip
121+
end
122+
end

lib/mu_cache_plug.ex

Lines changed: 27 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
alias Cache.Registry, as: Cache
2-
31
defmodule MuCachePlug do
2+
alias Cache.Registry, as: Cache
3+
44
@moduledoc """
55
Router for receiving cache requests.
66
"""
@@ -10,6 +10,17 @@ defmodule MuCachePlug do
1010
plug(:match)
1111
plug(:dispatch)
1212

13+
@request_manipulators []
14+
@response_manipulators [
15+
Manipulators.CacheKeyLogger,
16+
Manipulators.StoreResponse,
17+
Manipulators.RemoveCacheRelatedKeys
18+
]
19+
@manipulators ProxyManipulatorSettings.make_settings(
20+
@request_manipulators,
21+
@response_manipulators
22+
)
23+
1324
get "/cachecanhasresponse" do
1425
send_resp(conn, 200, "debugging is a go")
1526
end
@@ -20,21 +31,22 @@ defmodule MuCachePlug do
2031
|> Enum.map(&Poison.decode!/1)
2132
# remove nil values
2233
|> Enum.filter(& &1)
23-
|> Enum.map(&maybe_log_clear_keys/1)
34+
|> Enum.map(&maybe_log_delta_clear_keys/1)
2435
|> Enum.map(&Cache.clear_keys/1)
2536

2637
Plug.Conn.send_resp(conn, 204, "")
2738
end
2839

2940
match "/*path" do
30-
full_path = Enum.reduce(path, "", fn a, b -> b <> "/" <> a end)
41+
full_path = conn.request_path
3142
known_allowed_groups = get_string_header(conn.req_headers, "mu-auth-allowed-groups")
3243
conn = Plug.Conn.fetch_query_params(conn)
3344

3445
cond do
3546
known_allowed_groups == nil ->
3647
# without allowed groups, we don't know the access rights
37-
calculate_response_from_backend(full_path, conn)
48+
# calculate_response_from_backend(full_path, conn)
49+
ConnectionForwarder.forward(conn, path, "http://backend/", @manipulators)
3850

3951
cached_value =
4052
Cache.find_cache({conn.method, full_path, conn.query_string, known_allowed_groups}) ->
@@ -43,93 +55,11 @@ defmodule MuCachePlug do
4355

4456
true ->
4557
# without a cache, we should consult the backend
46-
IO.puts("Cache miss")
47-
calculate_response_from_backend(full_path, conn)
48-
end
49-
end
50-
51-
defp maybe_log_clear_keys(clear_keys) do
52-
if Application.get_env(:mu_cache, :log_clear_keys) do
53-
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
54-
IO.inspect(clear_keys, label: "Clear keys")
55-
end
56-
57-
clear_keys
58-
end
58+
# IO.inspect(
59+
# {conn.method, full_path, conn.query_string, known_allowed_groups}, label: "Cache miss for signature")
5960

60-
defp maybe_log_cache_keys(cache_keys) do
61-
if Application.get_env(:mu_cache, :log_cache_keys) do
62-
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
63-
IO.inspect(cache_keys, label: "Cache keys")
61+
ConnectionForwarder.forward(conn, path, "http://backend/", @manipulators)
6462
end
65-
66-
cache_keys
67-
end
68-
69-
@spec calculate_response_from_backend(String.t(), Plug.Conn.t()) :: Plug.Conn.t()
70-
defp calculate_response_from_backend(full_path, conn) do
71-
# Full path starts with /
72-
url = "http://backend" <> full_path
73-
74-
url =
75-
case conn.query_string do
76-
"" -> url
77-
query -> url <> "?" <> query
78-
end
79-
80-
opts = PlugProxy.init(url: url)
81-
82-
processors = %{
83-
header_processor: fn headers, _conn, state ->
84-
headers = downcase_headers(headers)
85-
86-
{headers, cache_keys} = extract_json_header(headers, "cache-keys")
87-
{headers, clear_keys} = extract_json_header(headers, "clear-keys")
88-
89-
maybe_log_cache_keys(cache_keys)
90-
maybe_log_clear_keys(clear_keys)
91-
92-
{headers,
93-
%{
94-
state
95-
| headers: headers,
96-
allowed_groups: get_string_header(headers, "mu-auth-allowed-groups"),
97-
cache_keys: cache_keys,
98-
clear_keys: clear_keys
99-
}}
100-
end,
101-
chunk_processor: fn chunk, state ->
102-
# IO.puts "Received chunk:"
103-
# IO.inspect chunk
104-
{chunk, %{state | body: state.body <> chunk}}
105-
end,
106-
body_processor: fn body, state ->
107-
# IO.puts "Received body:"
108-
# IO.inspect body
109-
{body, %{state | body: state.body <> body}}
110-
end,
111-
finish_hook: fn state ->
112-
# IO.puts "Fully received body"
113-
# IO.puts state.body
114-
# IO.puts "Current state:"
115-
# IO.inspect state
116-
Cache.store({conn.method, full_path, conn.query_string, state.allowed_groups}, state)
117-
{true, state}
118-
end,
119-
state: %{
120-
is_processor_state: true,
121-
body: "",
122-
headers: %{},
123-
status_code: 200,
124-
cache_keys: [],
125-
clear_keys: [],
126-
allowed_groups: nil
127-
}
128-
}
129-
130-
conn
131-
|> Map.put(:processors, processors)
132-
|> PlugProxy.call(opts)
13363
end
13464

13565
defp respond_with_cache(conn, cached_value) do
@@ -138,30 +68,16 @@ defmodule MuCachePlug do
13868
|> send_resp(200, cached_value.body)
13969
end
14070

141-
defp extract_json_header(headers, header_name) do
142-
case List.keyfind(headers, header_name, 0) do
143-
{^header_name, keys} ->
144-
new_headers = List.keydelete(headers, header_name, 0)
145-
{new_headers, Poison.decode!(keys)}
146-
147-
_ ->
148-
{headers, []}
71+
defp maybe_log_delta_clear_keys(clear_keys) do
72+
if Application.get_env(:mu_cache, :log_clear_keys) do
73+
# credo:disable-for-next-line Credo.Check.Warning.IoInspect
74+
IO.inspect(clear_keys, label: "Clear keys")
14975
end
15076
end
15177

15278
defp get_string_header(headers, header_name) do
153-
case List.keyfind(headers, header_name, 0) do
154-
{^header_name, string} ->
155-
string
156-
157-
_ ->
158-
nil
159-
end
160-
end
161-
162-
defp downcase_headers(headers) do
163-
Enum.map(headers, fn {header, content} ->
164-
{String.downcase(header), content}
165-
end)
79+
headers
80+
|> List.keyfind(header_name, 0, {nil, nil})
81+
|> elem(1)
16682
end
16783
end

0 commit comments

Comments
 (0)