Eptic FE

Since 1.3

Overview

Building a fully scalable web services includes defining a clear separation between the front-end and the back-end nodes. The scalable architecture of Erlang Web assumes that we can partition the cluster of machines hosting the service into those two groups. Functionality of the front-end node is implemented in the eptic_fe (eptic front-end) application, which provides cache storage and renders HTML templates. The front-end node is considered to be stateless and therefore it can be copied (horizontal scalability) over a number of machines.

Moreover, it is possible to run the eptic_fe application on the same node as the database in order to provide the cache feature to our service.

Architecture

Usually, when the request comes in, it is processed by the dispatcher that decides what to do with it. The decision is made based on the rules defined in the dispatcher configuration file. There are four possible operations available:

  1. the request could be passed to a controller
  2. a template could be rendered
  3. a file could be served
  4. an error could be displayed (e.g. 404 when no pattern is describing the incoming request's URL).

The requests that require the controller to be called, usually use data from the database or from the user's session. In such case the controller computes some values, fills in the request dictionary with them and returns a template to be rendered. If the services we are building serves a lot of content that changes from time to time, but the change frequency is relatively small to the number of page hits, it can make sense have a mechanism for caching the server's responses. This way the same data won't need to be recomputed.

Depending on the service requirements, the Erlang Web server can be in one of the four modes:

The backend server keeps track of the frontend servers health - it monitors the other nodes and when something goes wrong (a node is down/connection is lost) it starts pinging the given frontend. When the situation is recovered, it registers to that node once again.

A list of the frontend nodes must be present in the project.conf file of the backend server: the option is: {fe_servers, [ListOfNodes]}. By default it is set to an empty list.

How does it work

In order to understand how it works we have to explain the control flow of the request. Let's start with the simplest case - static one (static rule in dispatcher):

  1. The request comes in
  2. We call the is_cacheable_mod:is_cacheable/0 to check if we should cache the request (this function will be described later). We save the result of the call.

  3. The dispatcher maps the request to some template (let's say - index.html)

  4. We check the result of the is_cacheable call - if it says the content should be cached (or read from cache) we check the cache storage

  5. Here we have two possibilities - either we can find the expanded template in the cache or it is not there:
    1. If the template is already cached, we serve it
    2. If there is no such template cached, we expand it and once again check if we should save it to the cache
  6. The result is served to the user

A more complicated sequence of events takes place if we have a dynamic rule defined. Then we must do the following:

  1. The request comes in
  2. We call the is_cacheable_mode:is_cacheable/0 to check if we should cache the request (this function will be described later). We save the result of the call.

  3. The dispatcher maps the request to some controller call (let's say - {main, home})

  4. We check the result of the is_cacheable call - if it says the content should be cached (or read from cache) we check the cache storage

  5. Here we have two possibilities - either we can find the expanded template in the cache or if it is not there:
    1. If the template is already cached, we serve it
    2. If it is not cached:
      1. The back-end node is asked to process the request (we pass the request dictionary together with the function arguments)
      2. The back-end node computes the answer, changes/uses (optionally) the request dictionary and returns back the tuple for the HTTP server
      3. The result is passed back to the front-end (together with the request dictionary) where the tuple is processed
      4. If the result of the call is a template, it is expanded
      5. If "is cacheable" conditions are fulfilled, the response is cached
  6. The result is served to the user

Now let's take a look what control on the cache engine we have.

Controlling cache

There are three types of cache included:

The dispatcher options element (a proplist) can be used to specify the type of cache, in which the content should be stored:

   1 {dynamic, "^/index\.html$", {main, home}, [{cache, persistent}]}.
   2 {static, "^/faq$", "doc/faq.html", [{cache, normal}]}.
   3 {dynamic, "^/list", {blog, list}, [{cache, {timeout, 20}]}.
   4 {dynamic, "^/normal_cache$", {blog, normal}}.
   5 {dynamic, "^/current_time$", {blog, time}, [{cache, no_cache}]}.

The above dispatch.conf entries express the following logic:

Apart from the dispatcher configuration we might want to decide if we should cache the response basing on the session (or other information - such as IP address or type of connection - HTTP/HTTPS). Then we should provide a module that exports a callback function - is_cacheable_mod. The following entry put inside the project.conf file will enable it for us:

{is_cacheable_mod, my_mod}.

Each time, before processing the request, the my_mod:is_cacheable() will be called and it should check if the request should use cache (for read/write). The is_cacheable function should return:

Having those rules specified (dispatcher's URL-based and is_cacheable's context-based) we can fully control the cache engine on the request level.

But what if we want to cache only some parts of our site? What if we have an element that will be displayed on many pages (e.g. latest news banner)?

wpart:cache

Using wpart:cache tag allows us to provide fine-grained cache mechanism. The syntax and the behavior is as follows:

<wpart:cache id="ID" groups="GROUPS" type="TYPE">
   SOME CONTENT
</wpart:cache>

The meanings of the uppercased labels:

During the template expanding process - when the expander parses the wpart:cache tag - it firstly checks if it is stored in cache. If it is, it skips the content and serves it directly from cache. Otherwise, it expands the content, saves it in the cache and serves it back to the expander.

Cache groups

After spending some time on developing the service it is very easy to get lost in the jungle of the cache ID's: some of them are simply URLs, other are those returned from is_cacheable function and finally - some of them are defined in templates, as the ids in wpart:cache tags.

In order to maintain such a huge number of identifiers Erlang Web defines the groups term. The group is identified by an arbitrary string. Each cached entry can belong to any number of the groups (and of course does not have belong to any). groups should be used to logically bind some resources and managed them as they were a single entry.

The cache entry has its group defined in the following places:

Invalidating

After performing some requests on the database some of the content of our site will be out-of-date. In case of timeout or normal cache we can wait for some time (sometimes very long if we set up the cache in that way) - but the persistent cache will last till the end virtual machine's life. The cache designed in that way will be totally useless feature. Because of that, Erlang Web provides us the mechanisms for invalidating the cached content.

There are two ways to invalidate some cache entries:

Note that the invalidation must be triggered from the back-end node, since the changes must be populated all over the cluster: to the all of the front-end nodes.

Calling the invalidators

eptic application - when the node is started in the backend/single_node_with_cache mode - starts the gen_server process responsible for keeping the cluster of machines in the consistent state. We should look closer at its API to understand how to invalidate things:

e_cluster:Fun

arguments

description

invalidate/1

[IdRegexp]

Function that takes a list of regular expressions as an argument (list of strings - not compiled ones) and invalidates all the entries which matches at least one of the regular expression on all front-end nodes. The regular expression's format is PCRE (re Erlang module).

invalidate_groups/1

[Group]

Function that takes a list of groups (strings) and invalidates all the entries that belongs at least to the one of the groups on the passed list.

synchronize_docroot/1

Filename

Function that copies the file stored on the back-end server to the all front-end nodes.

The call to the e_cluster must be performed on the back-end node.

Using the e_annotations

Erlang Web provides also the annotation-like mechanism which could be used for a meta-programming. In order to use the annotation, write the following code right before your function declaration:

   1 ?ANNOTATION_NAME(ARGS).
   2 desired_function(...) ->
   3    ...

Moreover the e_cluster_annotations.hrl header must be included in the module you are going to use to define your annotations (it is placed in the eptic/include/ directory). The annotations are implemented using the parse_transform mechanism. Visit annotations wiki page for details.

Let's take a look at the annotation closer.

INVALIDATE

Invalidates the content on the front-end servers.

For example the following piece of code:

   1 ?INVALIDATE(["^/blog/", "^/index\.html$"]).
   2 create_new_post(Content) ->
   3     wtype_post:create(Content),
   4     mailing_list:announce(new_post, Content),
   5    
   6     {redirect, "/index.html"}.

is equivalent to the:

   1 create_new_post(Content) ->
   2     Result = begin
   3                  wtype_post:create(Content),
   4                  mailing_list:announce(new_post, Content),
   5    
   6                  {redirect, "/index.html"}
   7                 end,
   8 
   9     case application:get_env(eptic, node_type) of
  10         {ok, NodeType} when NodeType == backend; 
  11                             NodeType == single_node_with_cache ->
  12             e_cluster:invalidate(["^/blog/", "^/index\.html$"]);
  13         _ ->
  14             ok
  15     end,
  16     Result.

INVALIDATE_IF

Invalidates the content on the front-end servers only when the evaluation of the Pred function (could be either a local call - then the Pred is an atom or a remote call - then the Pred is a tuple of atoms: {Mod, Fun}) that takes a one argument - a result of the actual function evaluation - returns true. Otherwise, the invalidation process is not performed.

The following piece of code:

   1 ?INVALIDATE_IF(["^/blog/", "^/index\.html$"], checker).
   2 create_new_post(Content) ->
   3     wtype_post:create(Content),
   4     case mailing_list:announce(new_post, Content) of
   5            ok ->
   6                {redirect, "/index.html"};
   7            {error, Reason} ->
   8                wpart:fset("error_msg", Reason),
   9                {template, "error/mailing_list_error.html"}
  10     end.
  11 
  12 checker({redirect, _}) ->
  13     true;
  14 checker(_) ->
  15     false.

Is equivalent to:

   1 create_new_post(Content) ->
   2     Result = begin
   3                  wtype_post:create(Content),
   4                  mailing_list:announce(new_post, Content),
   5    
   6                  {redirect, "/index.html"}
   7              end,
   8 
   9     case checker(Result) of
  10       true ->
  11              case application:get_env(eptic, node_type) of
  12                  {ok, NodeType} when NodeType == backend; 
  13                                      NodeType == single_node_with_cache ->
  14                      e_cluster:invalidate(["^/blog/", "^/index\.html$"]);
  15                  _ ->
  16                      ok
  17              end,
  18              Result;
  19       _ ->
  20            ok
  21    end,
  22    Result.

INVALIDATE_GROUPS & INVALIDATE_GROUPS_IF

The usage of those two annotations is the same as above except that they operate on groups and do not use regexps.

Configuration

In case of running the project in the interactive mode, running the project section describes all the necessary commands.

When Erlang Web is run in the embedded mode, the proper options should be set. At first, servers that we will be using on the frontend sides should be properly configured. The server configuration section shows how to do it. Apart of that, the special application variable should be set - it must be done by editing the eptic.app file (stored in the lib/eptic/ebin/ directory). The node_type environment variable should be set to the proper value (single_node, single_node_with_cache, frontend or backend). Note that after each change of the .app file the new release should be created (see running for details).

EpticFE (last edited 2009-05-30 07:01:13 by mwillson)