Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Controller caching with cache_/2-3

  • Loading branch information...
commit bd63572fce32523ea59c4b3306c41bf6a6fe5859 1 parent 1c2f620
@evanmiller evanmiller authored
View
4 README_ELIXIR
@@ -84,8 +84,8 @@ in an explicit style:
{:output, "Hello, world!"}
end
-Other macros available include "before_", "after_", and "lang_", which take the
-same arguments as their Erlang counterparts.
+Other macros available include "before_", "cache_", "after_", and "lang_",
+which take the same arguments as their Erlang counterparts.
Tokens are passed in as Elixir strings (binaries), not Erlang strings.
View
56 doc-src/api-controller.html
@@ -4,6 +4,7 @@
&nbsp; <a href="#routes">Routes</a>
&nbsp; <a href="#auth">Authorization</a>
&nbsp; <a href="#return_values">Return values</a>
+&nbsp; <a href="#cache">Caching</a>
&nbsp; <a href="#lang">Content-Language</a>
&nbsp; <a href="#filter">Post-processing</a>
&nbsp; <a href="#simplebridge">SimpleBridge request object</a></p>
@@ -119,7 +120,7 @@
<li>the request method as an atom</li>
<li>the list of URL tokens</li>
</ol>
-<p><code>before_/3 should return one of:</P>
+<p><code>before_/3</code> should return one of:</P>
<div class="code spec">
{ok, ExtraInfo}
@@ -261,6 +262,47 @@
<br />
+<a name="cache"></a>
+<h3>Caching</h3>
+<p>Caching should be a part of any scalability strategy. In addition to caching the results of database queries (see <a href="api-config.html">config</a>), Chicago Boss can cache the list of variables returned from controller actions. CB can also cache entire rendered web pages, but in doing so you will lose the benefit of customizing the page contents with the <code>_before</code> variable, which is not cached.</p>
+
+<p>To enable caching, first make sure the <code>cache_enable</code> is set to <code>true</code> in your configuration and that you've configured cache servers. At present only Memcached cache servers are supported, but additional adapters will be added in the future.</p>
+
+<p>Next, define a <code>cache_</code> function with the following arguments:</p>
+<ol>
+ <li><code>Action</code> - the action name (a string)</li>
+ <li><code>Tokens</code> - a list of tokens</li>
+ <li><code>AuthInfo</code> (optional) - authorization information returned from the <code>before_</code> filter (see <a href="#auth">Authorization</a>)</li>
+</ol>
+
+<p>The <code>cache_</code> function should return one of:</p>
+<ul>
+ <li><code>{vars, CacheOptions}</code> - cache the variable list returned by the controller action</li>
+ <li><code>{page, CacheOptions}</code> - cache the rendered page contents</li>
+ <li><code>none</code> - don't cache</li>
+</ul>
+
+<p>Finally, <code>CacheOptions</code> is a proplist possibly containing:</p>
+<ul>
+ <li><code>seconds</code> - length of time to cache the result, in seconds</li>
+ <li><code>watch</code> - a <a href="api-news.html">topic string</a> defining a collection of records to watch for changes. When a change is observed, the cached data will be invalidated</li>
+</ul>
+
+<p>Note that separate cache entries are created for each language as returned by <code>lang_</code> (see <a href="#lang">Content-Language</a>).</p>
+
+<p>Example <code>cache_</code> function:</p>
+
+<div class="code">
+ cache_(<span class="string">"index"</span>, []) -&gt;<br />
+ &nbsp;&nbsp;&nbsp;&nbsp;{page, [{seconds, 30}]};<br />
+ cache_(<span class="string">"profile"</span>, [ProfileId]) -&gt;<br />
+ &nbsp;&nbsp;&nbsp;&nbsp;{vars, [{seconds, 300}, {watch, ProfileId ++ <span class="string">".*"</span>}]};<br />
+ cache_(_, _) -&gt;<br />
+ &nbsp;&nbsp;&nbsp;&nbsp;none.
+</div>
+
+<br />
+
<a name="lang"></a>
<h3>Content-Language</h3>
<p>CB application views can be multi-lingual. By default, the language served to the client is chosen by comparing the incoming Accept-Language header to the available translations in a given view (see <a href="https://github.com/evanmiller/ChicagoBoss/wiki/How-Chicago-Boss-Chooses-Which-Language-To-Serve">"How Chicago Boss Chooses Which Language To Serve"</a>. This can be overridden in two ways:</p>
@@ -322,21 +364,27 @@
<p>Get the request method, e.g. GET, POST, etc.</p>
<div class="code spec">
- query_param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined
+ protcol() -&gt; http | https
+</div>
+
+<p>Get the request protocol (HTTP or HTTPS)</p>
+
+<div class="code spec">
+ query_param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined<br />
query_param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -&gt; string() | Default Value
</div>
<p>Get the value of a given query string parameter (e.g. "?id=1234")</p>
<div class="code spec">
- post_param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined
+ post_param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined<br />
post_param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -&gt; string() | Default Value
</div>
<p>Get the value of a given POST parameter</p>
<div class="code spec">
- param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined
+ param( Key<span class="typevar">::string()</span> ) -&gt; string() | undefined<br />
param( Key<span class="typevar">::string()</span>, DefaultValue<span class="typevar">::term()</span> ) -&gt; string() | Default Value
</div>
View
20 priv/web_controller.ex
@@ -52,15 +52,15 @@ defmodule Boss.WebController do
end
end
- defmacro after_(action, result, block) do
+ defmacro cache_(action, tokens, block) do
quote do
- def after_(var!(req), var!(session_id), unquote(action), unquote(result)), unquote(block)
+ def cache_(var!(req), var!(session_id), unquote(action), unquote(tokens)), unquote(block)
end
end
- defmacro after_(action, result, info, block) do
+ defmacro cache_(action, tokens, info, block) do
quote do
- def after_(var!(req), var!(session_id), unquote(action), unquote(result), unquote(info)), unquote(block)
+ def cache_(var!(req), var!(session_id), unquote(action), unquote(tokens), unquote(info)), unquote(block)
end
end
@@ -76,6 +76,18 @@ defmodule Boss.WebController do
end
end
+ defmacro after_(action, result, block) do
+ quote do
+ def after_(var!(req), var!(session_id), unquote(action), unquote(result)), unquote(block)
+ end
+ end
+
+ defmacro after_(action, result, info, block) do
+ quote do
+ def after_(var!(req), var!(session_id), unquote(action), unquote(result), unquote(info)), unquote(block)
+ end
+ end
+
defp handle(method, action, tokens, block) do
action = to_action(action)
route = {action, to_route_tokens(tokens)}
View
95 src/boss/boss_web_controller.erl
@@ -2,7 +2,12 @@
-behaviour(gen_server).
-export([start_link/0, start_link/1, handle_request/3, process_request/5]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+-export([handle_news_for_cache/3]).
-define(DEBUGPRINT(A), error_logger:info_report("~~o)> " ++ A)).
+-define(PAGE_CACHE_PREFIX, "boss_web_controller_page").
+-define(PAGE_CACHE_DEFAULT_TTL, 60).
+-define(VARIABLES_CACHE_PREFIX, "boss_web_controller_variables").
+-define(VARIABLES_CACHE_DEFAULT_TTL, 60).
-record(state, {
applications = [],
@@ -788,27 +793,97 @@ execute_action({Controller, Action, Tokens} = Location, AppInfo, Req, SessionID,
Method -> Method
end,
- ActionResult = Adapter:action(AdapterInfo, Action, EffectiveRequestMethod, Tokens, Info),
LangResult = Adapter:language(AdapterInfo, Action, Info),
- LangHeaders = case LangResult of
- auto -> [];
- _ -> [{"Content-Language", LangResult}]
+ CacheKey = {Controller, Action, Tokens, LangResult},
+
+ CacheInfo = case (boss_env:get_env(cache_enable, false) andalso
+ EffectiveRequestMethod =:= 'GET') of
+ true -> Adapter:cache_info(AdapterInfo, Action, Tokens, Info);
+ false -> none
+ end,
+
+ CachedRenderedResult = case CacheInfo of
+ {page, _} -> boss_cache:get(?PAGE_CACHE_PREFIX, CacheKey);
+ _ -> undefined
+ end,
+
+ {CacheTTL, CacheWatchString} = case CacheInfo of
+ {page, CacheOptions} ->
+ {proplists:get_value(seconds, CacheOptions, ?PAGE_CACHE_DEFAULT_TTL),
+ proplists:get_value(watch, CacheOptions)};
+ {vars, CacheOptions} ->
+ {proplists:get_value(seconds, CacheOptions, ?VARIABLES_CACHE_DEFAULT_TTL),
+ proplists:get_value(watch, CacheOptions)};
+ _ ->
+ {undefined, undefined}
end,
- Result = case ActionResult of
+
+ RenderedResult = case CachedRenderedResult of
undefined ->
- render_view(Location, AppInfo, Req, SessionID, [{"_before", Info}], LangHeaders);
- ActionResult ->
- process_action_result({Location, Req, SessionID, [Location|LocationTrail]},
- ActionResult, LangHeaders, AppInfo, Info)
+ CachedActionResult = case CacheInfo of
+ {vars, _} -> boss_cache:get(?VARIABLES_CACHE_PREFIX, CacheKey);
+ _ -> undefined
+ end,
+
+ ActionResult = case CachedActionResult of
+ undefined -> Adapter:action(AdapterInfo, Action, EffectiveRequestMethod, Tokens, Info);
+ _ -> CachedActionResult
+ end,
+
+ case (CachedActionResult =/= undefined andalso is_tuple(ActionResult) andalso element(1, ActionResult) =:= ok) of
+ true ->
+ case CacheWatchString of
+ undefined -> ok;
+ _ ->
+ boss_news:set_watch({?VARIABLES_CACHE_PREFIX, CacheKey}, CacheWatchString,
+ fun ?MODULE:handle_news_for_cache/3, {?VARIABLES_CACHE_PREFIX, CacheKey},
+ CacheTTL)
+ end,
+ boss_cache:set(?VARIABLES_CACHE_PREFIX, CacheKey, ActionResult, CacheTTL);
+ false ->
+ ok
+ end,
+
+ LangHeaders = case LangResult of
+ auto -> [];
+ _ -> [{"Content-Language", LangResult}]
+ end,
+
+ case ActionResult of
+ undefined ->
+ render_view(Location, AppInfo, Req, SessionID, [{"_before", Info}], LangHeaders);
+ ActionResult ->
+ process_action_result({Location, Req, SessionID, [Location|LocationTrail]},
+ ActionResult, LangHeaders, AppInfo, Info)
+ end;
+ Other ->
+ Other
end,
- Adapter:after_filter(AdapterInfo, Action, Result, Info);
+ case (CachedRenderedResult =/= undefined andalso is_tuple(RenderedResult) andalso element(1, RenderedResult) =:= ok) of
+ true ->
+ case CacheWatchString of
+ undefined -> ok;
+ _ ->
+ boss_news:set_watch({?PAGE_CACHE_PREFIX, CacheKey}, CacheWatchString,
+ fun ?MODULE:handle_news_for_cache/3, {?PAGE_CACHE_PREFIX, CacheKey}, CacheTTL)
+ end,
+ boss_cache:set(?PAGE_CACHE_PREFIX, CacheKey, RenderedResult, CacheTTL);
+ false ->
+ ok
+ end,
+
+ Adapter:after_filter(AdapterInfo, Action, RenderedResult, Info);
{redirect, Where} ->
{redirect, process_redirect(Controller, Where, AppInfo)}
end
end.
+handle_news_for_cache(_, _, {Prefix, Key}) ->
+ boss_cache:delete(Prefix, Key),
+ {ok, cancel_watch}.
+
process_location(Controller, [{_, _}|_] = Where, AppInfo) ->
{_, TheController, TheAction, CleanParams} = process_redirect(Controller, Where, AppInfo),
ControllerModule = list_to_atom(boss_files:web_controller(AppInfo#boss_app_info.application, Controller)),
View
8 src/boss/controller_adapters/boss_controller_adapter_elixir.erl
@@ -21,6 +21,14 @@ before_filter({Module, ExportStrings, Req, SessionID}, Action, RequestMethod, To
_ -> ok
end.
+cache_info({Module, ExportStrings, Req, SessionID}, Action, Tokens, AuthInfo) ->
+ BinTokens = lists:map(fun list_to_binary/1, Tokens),
+ case proplists:get_value("cache_", ExportStrings) of
+ 4 -> Module:cache_(Req, SessionID, Action, BinTokens);
+ 5 -> Module:cache_(Req, SessionID, Action, BinTokens, AuthInfo);
+ _ -> ok
+ end.
+
action({Module, ExportStrings, Req, SessionID}, Action, RequestMethod, Tokens, AuthInfo) ->
BinTokens = lists:map(fun list_to_binary/1, Tokens),
case proplists:get_value(Action, ExportStrings) of
View
7 src/boss/controller_adapters/boss_controller_adapter_pmod.erl
@@ -24,6 +24,13 @@ before_filter({ControllerInstance, ExportStrings}, Action, RequestMethod, Tokens
_ -> ok
end.
+cache_info({ControllerInstance, ExportStrings}, Action, Tokens, Info) ->
+ case proplists:get_value("cache_", ExportStrings) of
+ 3 -> ControllerInstance:cache_(Action, Tokens);
+ 4 -> ControllerInstance:cache_(Action, Tokens, Info);
+ _ -> ok
+ end.
+
action({ControllerInstance, ExportStrings}, Action, RequestMethod, Tokens, AuthInfo) ->
case proplists:get_value(Action, ExportStrings) of
3 ->
Please sign in to comment.
Something went wrong with that request. Please try again.