If you have used Nitrogen for a while, you might start wandering how Nitrogen actually processes your requests and what is happening under the hood. Unfortunately the documentation on this part is particularly patchy. There is a very good introductory tutorial which says this:
How is a Page Rendered? User hits a URL. URL is mapped to a module. Nitrogen framework calls module:main() module:main() calls a #template #template calls back into the page (or other modules) Nitrogen framework renders the output into HTML/Javascript. (This is the simple version. Complex version will come later.)
Well, I never saw the “Complex version”.
This is the attempt to explain some of the steps that Nitrogen takes to render the page.
(I am treating this as study notes so I am more than certain that I will make some mistakes or even get something completely wrong – feel free to correct me. I am also only studying how Nitrogen works with Cowboy http server.).
Big picture
When a user hits an URL serving Nitrogen pages, the request first goes to http server. Nitrogen doesn’t not parse raw http protocol, instead it uses one of four available http servers: Yaws, Mochiweb, Inets or Cowboy.
Http server (Cowboy for example) will have a routing table which you need to define during server initialization.
The table might look like this (this is Cowboy 0.6 specific! routing format changed a little in Cowboy 0.8):
dispatch_rules() -> %% {Host, list({Path, Handler, Opts})} [{'_', [ {[<<"content">>, '...'], cowboy_http_static, [{directory, {priv_dir, ?APP, [<<"content">>]}}, {mimetypes, [{<<".css">>, [<<"text/css">>]}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}]}, {[<<"static">>, '...'], cowboy_http_static, [{directory, {priv_dir, ?APP, [<<"static">>]}}, {mimetypes, [{<<".css">>, [<<"text/css">>]}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}]}, {[<<"plugins">>, '...'], cowboy_http_static, [{directory, {priv_dir, ?APP, [<<"plugins">>]}}, {mimetypes, [{<<".css">>, [<<"text/css">>]}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}]}, {[<<"doc">>, '...'], cowboy_http_static, [{directory, {priv_dir, ?APP, [<<"doc">>]}}, {mimetypes, [{<<".css">>, [<<"text/css">>]}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}]}, {[<<"get_jqgrid_data">>, '...'], get_jqgrid_data, []}, {'_', nitrogen_cowboy, []} ] }].
This tells Cowboy that request for “css” and “images” are processed by “cowboy_http_static” handler but all other requests are routed to “nitrogen_cowboy” handler which might look like the following:
-module(nitrogen_cowboy). -include_lib("nitrogen_core/include/wf.hrl"). -export([init/3, handle/2, terminate/2]). -record(state, {headers, body}). init(_Transport, Req, []) -> {ok, Req, #state{}}. handle(Req, State) -> {ok, DocRoot} = application:get_env(nitrogen_elements_examples, document_root), RequestBridge = simple_bridge:make_request(cowboy_request_bridge, {Req, DocRoot}), ResponseBridge = simple_bridge:make_response(cowboy_response_bridge, RequestBridge), %% Establishes the context with the Request and Response Bridges nitrogen:init_request(RequestBridge, ResponseBridge), {ok, NewReq} = nitrogen:run(), %% This will be returned back to cowboy {ok, NewReq, State}. terminate(_Req, _State) -> ok.
In order to be able to use multiple http servers, Nitrogen is using the abstraction level, called “Simple_Bridge”. There are implementations of RequestBridge and ResponseBridge interfaces for each http server and the main idea here is that each implementation knows enough of specifics of the underlying server to create a “Request” / “Response” object which Nitrogen knows how to consume. “Request” is more complex as each implementation needs to parse http headers and body of raw http request to create it.
Here we find the first problem – Simple_Bridge uses parametrized Erlang modules to construct “Request” and “Response”. It’s kind of making sense as this is the way to implement polymorphism in Erlang. Simple_Bridge calls this:
RequestBridge = simple_bridge_request_wrapper:new(Module, RequestData1, false, [], [], none),
where Module is the name of the simple_bridge_request implementation specific for a given http server, e.g. cowboy_request_bridge.
This means that when later Nitrogen calls, for example, RequestBridge:request_body(), RequestBridge interface will call actual implementation for request body : cowboy_request_bridge:request_body().
request_body() -> Mod:request_body(Req).
where “Mod” is “cowboy_request_bridge” or implementation for another http server.
This is neat and definitely makes a code more compact but parametrized modules are being removed from Erlang R16 as it has always been an experimental feature and Ericsson is taking it out now. Ericsson OTP team provided a workaround in a form of pmod_transform module, which allows to use parametrized modules in the future releases of OTP (of course this is completely un-supported by OTP anymore).
The next step after we got RequestBridge and ResponseBridge objects is to setup “Context”
nitrogen:init_request(RequestBridge, ResponseBridge),
Context is a structure which contains *everything* to know about the page which is being built – each request routed to Nitrogen will result in Context object which is used as a “State” in gen_server for example.
And here is another little dirty secret – Nitrogen keeps this state in process dictionary. I personally don’t find it especially shocking because Nitrogen uses the same process which was created by Cowboy and from design point of view you would either need to pass the “State” object to the every single function call (the code would become unmaintainable) or spawn a new process (gen_server maybe) which would hold a shared state. This would result in doubling a number of processes for each request.
So, using process dictionary was a compromise to keep the code relatively clean but it also earned Nitrogen a bit of nasty reputation – e.g. Zotonic guys stoped using Nitrogen citing the use of dictionaries as one of the reasons.
Ok, this post is getting too long so I will continue the next time.