Packages
template_compiler
3.6.0
3.6.0
3.5.0
3.4.1
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.14.0
2.13.2
2.13.1
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.2
1.0.1
1.0.0
1.0.0-alpha8
1.0.0-alpha7
1.0.0-alpha6
1.0.0-alpha5
1.0.0-alpha4
1.0.0-alpha3
1.0.0-alpha2
Zotonic Template Compiler
Current section
Files
Jump to
Current section
Files
src/template_compiler_admin.erl
%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2016-2026 Marc Worrell
%% @doc Administrate all compiled templates and compilers in flight.
%% @end
%% Copyright 2016-2026 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(template_compiler_admin).
-author('Marc Worrell <marc@worrell.nl>').
-behaviour(gen_server).
-export([
lookup/3,
register/4,
flush/0,
flush_debug/0,
flush_debug/1,
flush_file/1,
flush_context_name/1
]).
-export([
start_link/0,
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2
]).
-include_lib("kernel/include/logger.hrl").
-type gen_server_from() :: {pid(),term()}.
-record(tpl, {
key :: template_compiler:template_key(),
module :: atom()
}).
-record(state, {
compiled :: ets:tab(),
compiling = [] :: list({reference(), template_compiler:template_key(), gen_server_from()}),
waiting = [] :: list({template_compiler:template_key(), gen_server_from()}),
filename_keys = [] :: list({file:filename_all(), template_compiler:template_key()})
}).
%%% ---------------------- API ----------------------
%% @doc Find a template, start a compilation if not found
-spec lookup(file:filename_all(), template_compiler:options(), any()) -> {ok, atom()} | {error, term()}.
lookup(Filename, Options, Context) ->
Runtime = template_compiler:get_option(runtime, Options),
ContextName = Runtime:get_context_name(Context),
TplKey = {ContextName, Runtime, Filename},
case ets:lookup(?MODULE, TplKey) of
[#tpl{module=Module}] ->
case Runtime:is_modified(Filename, Module:mtime(), Context) of
true -> compile_file(Filename, TplKey, Options, Context);
false -> {ok, Module}
end;
[] ->
compile_file(Filename, TplKey, Options, Context)
end.
-spec register(file:filename_all(), template_compiler:options(), any(), atom()) -> ok.
register(Filename, Options, Context, Module) when is_atom(Module) ->
Runtime = template_compiler:get_option(runtime, Options),
ContextName = Runtime:get_context_name(Context),
TplKey = {ContextName, Runtime, Filename},
gen_server:call(?MODULE, {register, Filename, TplKey, Module}, infinity).
compile_file(Filename, TplKey, Options, Context) ->
case gen_server:call(?MODULE, {compile_request, TplKey, Filename}, infinity) of
{ok, {compile, TplKey}} ->
% Unknown, compile the template
Result = try
template_compiler:compile_file(Filename, Options, Context)
catch
What:Error:Stack ->
% io:format("Error compiling template ~p: ~p:~n~p at~n ~p~n",
% [Filename, What, Error, Stack]),
?LOG_ERROR(#{
text => <<"Error compiling template">>,
template => Filename,
result => What,
reason => Error,
stack => Stack
}),
{error, Error}
end,
ok = gen_server:call(?MODULE, {compile_done, Result, TplKey}, infinity),
Result;
{ok, Module} when is_atom(Module) ->
{ok, Module};
{error, _} = Error ->
Error
end.
%% @doc Flush all template mappings
-spec flush() -> ok.
flush() ->
gen_server:cast(?MODULE, flush).
%% @doc Flush all template mappings for modules compiled with debug points enabled
-spec flush_debug() -> ok.
flush_debug() ->
gen_server:call(?MODULE, {flush_debug, all}, infinity).
%% @doc Flush all template mappings for modules compiled with debug points enabled for a context name
-spec flush_debug(term()) -> ok.
flush_debug(ContextName) ->
gen_server:call(?MODULE, {flush_debug, ContextName}, infinity).
%% @doc Ping that a template has been changed
-spec flush_file(file:filename_all()) -> ok.
flush_file(Filename) ->
gen_server:cast(?MODULE, {flush_file, Filename}).
%% @doc Ping that a context has changed
-spec flush_context_name(ContextName::term()) -> ok.
flush_context_name(ContextName) ->
gen_server:cast(?MODULE, {flush_context_name, ContextName}).
-spec start_link() -> {ok, pid()} | {error, any()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%% ---------------------- Callbacks ----------------------
init([]) ->
Compiled = ets:new(?MODULE, [set, {keypos, #tpl.key}, named_table, protected]),
{ok, #state{
compiled = Compiled,
compiling = [],
waiting = [],
filename_keys = []
}}.
handle_call({compile_request, TplKey, Filename}, From, State) ->
case is_compiling(TplKey, State) of
true ->
{noreply, wait_for_compile(TplKey, From, State)};
false ->
FTpl = {Filename, TplKey},
State1 = case lists:member(FTpl, State#state.filename_keys) of
false ->
State#state{
filename_keys=[ FTpl | State#state.filename_keys ]
};
true ->
State
end,
{noreply, start_compile(TplKey, From, State1)}
end;
handle_call({compile_done, Result, TplKey}, _From, State) ->
State1 = case lists:keytake(TplKey, 2, State#state.compiling) of
{value, {MRef, _TplKey, _CFrom}, Compiling1} ->
erlang:demonitor(MRef),
State#state{compiling=Compiling1};
false ->
State
end,
case Result of
{ok, Module} ->
ets:insert(?MODULE, #tpl{key=TplKey, module=Module});
{error, _} ->
ok
end,
{Waiters, State2} = split_waiters(TplKey, State1),
lists:foreach(
fun({_TplKey, Waiter}) ->
gen_server:reply(Waiter, Result)
end,
Waiters),
{reply, ok, State2};
handle_call({register, Filename, TplKey, Module}, _From, State) ->
ets:insert(?MODULE, #tpl{key=TplKey, module=Module}),
FTpl = {Filename, TplKey},
State1 = case lists:member(FTpl, State#state.filename_keys) of
false ->
State#state{
filename_keys=[ FTpl | State#state.filename_keys ]
};
true ->
State
end,
{reply, ok, State1};
handle_call({flush_debug, ContextName}, _From, State) ->
{reply, ok, flush_debug_state(ContextName, State)};
handle_call(Msg, _From, State) ->
{stop, {unknown_call, Msg}, State}.
handle_cast(flush, State) ->
true = ets:delete_all_objects(?MODULE),
State1 = State#state{
filename_keys = []
},
{noreply, State1};
handle_cast({flush_file, Filename}, State) ->
{ChangedKeys, FnKeys} = lists:partition(
fun ({F,_}) ->
F =:= Filename
end,
State#state.filename_keys),
lists:foreach(
fun({_,TplKey}) ->
ets:delete(?MODULE, TplKey)
end,
ChangedKeys),
{noreply, State#state{filename_keys=FnKeys}};
handle_cast({flush_context_name, ContextName}, State) ->
Matched = ets:foldl(
fun
(#tpl{key={Ctx, _, _}} = Tpl, Acc) when Ctx =:= ContextName ->
[Tpl|Acc];
(_, Acc) ->
Acc
end,
[],
?MODULE),
% TODO: add module to list of modules that might be purged
lists:foreach(
fun(#tpl{key=Key}) ->
ets:delete(?MODULE, Key)
end,
Matched),
FilenameKeys = lists:filter(
fun({_Fn, K}) ->
element(1, K) =/= ContextName
end,
State#state.filename_keys),
{noreply, State#state{filename_keys=FilenameKeys}};
handle_cast(Msg, State) ->
{stop, {unknown_cast, Msg}, State}.
handle_info({'DOWN', MRef, process, _Pid, Reason}, State) ->
case lists:keytake(MRef, 1, State#state.compiling) of
{value, {_MRef, TplKey, _From}, Compiling1} ->
?LOG_ERROR(#{
text => <<"Compiler process down; restarting with other waiter">>,
tpl_key => TplKey,
result => error,
reason => Reason
}),
{noreply, restart_compile(TplKey, State#state{compiling=Compiling1})};
false ->
{noreply, State}
end;
handle_info(Msg, State) ->
{stop, {unknown_info, Msg}, State}.
code_change(_FromVersion, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
%%% ---------------------- Internal ----------------------
is_compiling(TplKey, State) ->
lists:keymember(TplKey, 1, State#state.waiting)
orelse lists:keymember(TplKey, 2, State#state.compiling).
wait_for_compile(TplKey, From, State) ->
State#state{waiting=[{TplKey,From} | State#state.waiting]}.
start_compile(TplKey, From, State) ->
State1 = wait_for_compile(TplKey, From, State),
restart_compile(TplKey, State1).
restart_compile(TplKey, State) ->
case lists:keytake(TplKey, 1, State#state.waiting) of
{value, {_,{Pid, _} = From}, Waiting} ->
MRef = erlang:monitor(process, Pid),
gen_server:reply(From, {ok, {compile, TplKey}}),
State#state{waiting=Waiting, compiling=[{MRef,TplKey,From}|State#state.compiling]};
false ->
% Drop this request, no one is waiting anymore
State
end.
split_waiters(TplKey, State) ->
{Ready,Waiting} = lists:partition(
fun({K,_From}) ->
K =:= TplKey
end,
State#state.waiting),
{Ready, State#state{waiting=Waiting}}.
flush_debug_state(ContextName, State) ->
DebugCompiled = ets:foldl(
fun
(#tpl{key={Ctx, _, _}, module=Module} = Tpl, Acc)
when ContextName =:= all orelse Ctx =:= ContextName ->
case Module:is_debug_compiled() of
true ->
[Tpl|Acc];
false ->
Acc
end;
(_, Acc) ->
Acc
end,
[],
?MODULE),
lists:foreach(
fun (#tpl{key=Key}) ->
ets:delete(?MODULE, Key)
end,
DebugCompiled),
DebugKeys = [ Key || #tpl{key=Key} <- DebugCompiled ],
FilenameKeys = lists:filter(
fun({_Fn, Key}) ->
not lists:member(Key, DebugKeys)
end,
State#state.filename_keys),
State#state{filename_keys=FilenameKeys}.