Showing posts with label Erlang. Show all posts
Showing posts with label Erlang. Show all posts

Thursday, 28 April 2016

Taming configuration files - macro substitution

So What's the problem?

In the last few blog on global state, I proposed the solution to global state was a server.  I still stand by that, and that concept still needs a bit of fleshing out.  In the end the global state will probably be in a stand alone server connected to by clients using udp or tcp.  Before we get there I will define a server connection library that I can use in a number of servers.

The point is this approach will require late binding, configuration files that depend on the global state server will of necessity not be fully defined until runtime, and the current term parser has no concept of macro substitution.

Extension to term definition

The term definition is extended by the inclusion of a macro definition:

{macro, Name, DataDef} 

which is included in the configuration file as required.

To substitute a macro for a value, the result of the substitution must be the same as if the value were used in its stead, therefore our validation rule for macro substitution is:

maybe_validate(DataDef,{macro,_Name,DataDef},State) ->
    {true,State};

This rule is included in term_defs.erl.

Substitution Code

The code for the substitution is also relatively simple:

-module (substitute).

-export([substitute/2,test/0]).

-type proplist()::[{any(),any()}].
-spec substitute(Data,SubstList) -> OutData when
      Data :: any(),
      SubstList :: proplist(),
      OutData :: any().

substitute(X, SL) when is_list(X) ->
    list_sub(X ,[],SL);
substitute({macro,Name,Def},SL) ->
    macro_sub(Name,Def,SL);
substitute(X, SL) when is_tuple(X) ->
    L = tuple_to_list(X),
    NL = list_sub(L, [], SL),
    list_to_tuple(NL);
substitute(X,_) ->
    X.


list_sub([], Acc, _) ->
    lists:reverse(Acc);
list_sub([H|T], Acc, SL) ->
    list_sub(T,[substitute(H,SL)|Acc],SL).

macro_sub(Name,Def,SL) ->
    {_,V}=proplists:lookup(Name,SL),
    true = term_defs:validate(Def,V),
    V.




Sunday, 24 April 2016

Taming Configuration Files - Global State

Configuring for Global State

Warning: This blog is unashamedly Erlang centric.  The issues discussed do exist in other languages, but the solutions here are for Erlangers. 

In general global state is a pain in the arse.   There is no real reason why different processes should not use different databases for example.  As such I avoid global state whenever possible.    There are examples where global state is unavoidable.  In such cases there are advantages to a centralised store for such information.   IPv4 addresses assigned to computers is an example of global state, and without a central store, such as a DHCP server, difficulties with the allocation and management of these cause real problems.

The worked example below shows how configuration files with schema files and scripts to check dependencies can be used to write systems that manage global state and check that no rules are broken prior to deployment.  makefiles can be used to automate the application of the necessary tools and deploy the resulting files.

While it is true that this process could have been done by making a globals database, writing some referential integrity rules and processes could connect to that database and retrieve the data they require, this approach has certain advantages.
  1. The result is an OTP application without external dependencies
  2. Configuration file changes can be tracked with normal text based source control procedures.  This would not be possible with a traditional database solution.
  3. The OTP server has low resource requirements

A worked example


In this case we are concerned with a hypothetical example of a bureau service supplying accounting services.  The accounting system is broken up into modules that communicate via UDP.  Each client has their own set of accounting modules, therefore the pair {client,module} maps to a port.  Port sharing is not allowed thus in relational algebra terms, {client,module} and port are both candidate keys.  Every client and module used to define a port must exist in the client and modules sections respectively.   

The configuration file used in this example follows:

%% File globals.config
%% Good configuration file
%% obeys all the rules

%% comment out one of the module or client lines to generate
%% an error in the related tcp_port definition
{module,point_of_sale,pos}.
{module,stock,stock}.
{module,general_ledger,gl}.

{client,amco,amco}.
{client,bamco,bamco}.

{tcp_port,5000,{amco,point_of_sale}}.
{tcp_port,5001,{amco,stock}}.
{tcp_port,5002,{amco,general_ledger}}.
{tcp_port,5100,{bamco,general_ledger}}.

%% uncomment following line to cause a duplicate port number error
%{tcp_port,5000,{bamco,point_of_sale}}.

In my earlier post I discussed describing Erlang terms in a way that allows them to be parsed and checked.  In the course of this work I did find a bug in the choices construction so if you are following the code get the latest from https://github.com/tonywallace64/erlang_config_tamer.git.

The data definition for this configuration file is:

{list,{options,
[{tuple,[{value,module},{builtin,is_atom},term]},
{tuple,[{value,client},{builtin,is_atom},term]},
{tuple,[{value,tcp_port},{builtin,is_integer},
{tuple,[{builtin,is_atom},{builtin,is_atom}]}]}]}}.

This configuration file can be validated against its data definition by running the config_check script as in the earlier post.  If validated the file globals.config.etf is generated.  This configuration file is loaded into a gen_server and made available to other processes in the system.  This is the usual Erlang/OTP pattern for this type of situation.  Using the emacs editor a boilerplate gen_server is available in erlang mode from the menu: Erlang/skeleton/gen_server.  This skeleton is customised by:
  1. Loading globals.config.etf as state in init
  2. Writing some query routines
This simple gen_server is available from https://github.com/tonywallace64/global_resources/blob/master/src/globals.erl.  Now that the this gen_server is available a second level of checking can be performed.  Using this server a script to check consistency can be written.  Note that using a genserver in this way implies the need for an extra stage of deployment.  config file to config.etf.  Then check for consistency and finally copy (if passed) into production.  This is easily accomplished using Makefile.  

The consistency checking code follows:

-module(checker).
-export ([main/1]).

main(_) ->
    {ok,Pid} = globals:start_link(),
    Tcp_ports = gen_server:call(Pid,{lookup,{tcp_port,'_','_'}}),
    io:format("tcp_port lookup result: ~p~n",[Tcp_ports]),
    check_ports(Tcp_ports,Pid,gb_sets:empty()),
    file:write_file("config_checked_okay", <<>> ).

check_ports([],_,Acc) ->
    Acc;
check_ports([X={tcp_port,Port,{Client,Module}}|T],Svr,Acc) ->
    io:format("checking ~p",[X]),
    NewSet = gb_sets:insert(Port,Acc),
    [{client,Client,_}] = gen_server:call(Svr,{lookup,{client,Client,'_'}}),
    [{module,Module,_}] = gen_server:call(Svr,{lookup,{module,Module,'_'}}),
    io:format("passed~n"),
    check_ports(T,Svr,NewSet).

Note the main/1 entrypoint.  This makes the code escript compatible and hence easily callable from a Makefile.  In the check_ports function calls are made to the gen_server to retrieve data referred to in the tcp_port entries.  Each lookup should return a list of exactly one entry.  If it does not do so a pattern matching exception is generated and the script aborts.  Similarly gb_sets:insert will generate an exception if the same port number is added more than once.  If check_ports returns without generating an exception the file config_checked_okay is touched for use by make.

Finally, throughout this post reference has been made to make.  Here is a the makefile out of the examples directory.  There are other makefiles here such as one to make the checker script and these are available from github.  The point is make makes the magic happen, so that the config file can be changed, and all the checks and deployments applied from there.

# Author Tony Wallace
EFT=deps/erlang_config_tamer

all: config_checked_okay
config_checked_okay: globals.config.etf ebin/globals.beam
escript scripts/checker/ebin/checker.beam
globals.config.etf: globals.config ebin/globals.def.etf
$(EFT)/config_check globals.config ebin/globals.def.etf
ebin/globals.beam: ../src/globals.erl
erlc -o ebin $< 

ebin/globals.def.etf: src/globals.def $(EFT)/datadef.def
$(EFT)/config_check $< $(EFT)/datadef.def
mv src/*.etf ebin
.PHONY: clean
clean:
rm  ebin/*.beam
rm config_checked_okay






Saturday, 2 April 2016

Taming Configuration Files - Example deployment

Overview

Over the past few weeks, this blog has been on building configuration file checking into the application.  The strategy has been to make a tool that checks the configuration file matches a grammar and to build a way to automate this process with make.  The past two posts have been on matching the grammar, and building the script respectively.

This post builds a simple example which shows this system in use.  A helloworld type program which reads name from a configuration file.

The test

The configuration file hello.conf contains the entry:

{name,"Tony"}.

The checker will confirm that this matches the datadef stored in hello_conf.def which contains:

{property_list,[{reqd,name,{list,{builtin,is_integer}}}]}.

Although it is true that the definition is longer than the data it describes, the definition of a configuration file is an application development activity, not an operational matter.  It is conceivable that definitions will be provided in a machine readable (rather than human readable) form.  Note that in Erlang a string is a list of integers.

The program that reads this configuration file, hello.erl is:

-module (hello).

-export([main/1]).

main(_) ->
    Name = get_name(),
    io:format("Hello ~s~n",[Name]).

get_name() ->
    {ok,Bin} = file:read_file("hello.conf.etf"),
    PL = binary_to_term(Bin),
    proplists:get_value(name,PL).

So this program produces the string "Hello Tony". This program reads the compiled configuration file hello.conf.etf which is produced by the config_check program we built last blog post.

Finally the compilation and test is run by make.  Here is the Makefile:

test: hello.conf.etf ebin/hello.beam
escript ebin/hello.beam

hello.conf.etf:hello_conf.def hello.conf
./config_check hello.conf hello_conf.def

ebin/hello.beam: src/hello.erl
erlc -o ebin src/hello.erl

clean:
rm hello.conf.etf ebin/hello.beam

To run the test type make from the terminal prompt.  To run with a different name, simply edit hello.conf and type make again.




Friday, 1 April 2016

Taming configuration files - making an executable

Overview

In my last blog I showed how to test that an Erlang term matched an arbitrary pattern, including testing for an iolist, a recursive structure.  The objective here is to package this code in an executable that can be deployed in our projects.  The output of a successful check will be a new config file with an ".eft" suffix.  This is to allow tools like make to do consistency checks.

To do this we package this code into an escript.  This requires a short driver program that presents the main/1 interface that escript requires.

The driver program

The name of the escript, and the name of the escript's main module must be the same, and this module must export main/1.  Having decided to name this script "config_check" then the main module bears that name. 

The main/1 entry point takes all of the parameters of the script as a list.  Here is the code.

-module (config_check).
-export([main/1,test/0]).

-purpose (
<<"Write etf version of Config file if valid.",
  "Other requirements are: runable as escript">>).

main([ConfigFile,SchemaFile]) ->
    DataDef = get_schema(SchemaFile),
    {ok,Config}  = file:consult(ConfigFile),
    Result       = term_defs:validate(DataDef,Config),
    write_results(Result,ConfigFile,Config).

write_results(true,ConfigFile,Config) ->
    OutputFile = [ConfigFile,".etf"],
    EtfData = term_to_binary(Config,[]),
    ok = file:write_file(OutputFile,EtfData);

write_results(false,_,_) ->
    ok.

get_schema(SchemaFile) ->
    get_schema_by_ext(filename:extension(SchemaFile),SchemaFile).

get_schema_by_ext(".etf",SchemaFile) ->
    {ok,Etf} = file:readfile(SchemaFile),
    binary_to_term(Etf);
get_schema_by_ext(_,SchemaFile) ->
    {ok,[DataDef]} = file:consult(SchemaFile),
    DataDef.
    

test() ->
    DataDef = "{list,{value,valid}}.",
    DefFile = "main_test.config_def",
    ok = file:write_file(DefFile,DataDef),
    ok = file:write_file("main_test_valid","valid."),
    ok = file:write_file("main_test_invalid","invalid."),
    main(["main_test_valid",DefFile]),
    %% correct behaviour of second test is to throw an exception
    %% catch it and clean up
    catch main(["main_test_invalid",DefFile]),
    true = filelib:is_regular("main_test_valid.etf"),
    false = filelib:is_regular("main_test_invalid.etf"),
    file:delete(DefFile),
    file:delete("main_test_valid"),
    file:delete("main_test_invalid"),
    file:delete("main_test_valid.etf"),
    pass.
    
As usual I have placed a test routine at the end of the module.  Note that as yet there is not test for definition files with ".etf" suffix, this is very naughty.  Notice too that the test DataDef is of the form
{list, {valid,value}}, not {valid,value}.  This is because all files read by file:consult are returned as lists.  In general any configuration file will be a list (or a property list which is still a list).

Making the script

Here I use my own customised version of mad available from https://github.com/tonywallace64/mad.  It does need a rebar.config but that can be empty.  To make the release using the mad executeable:

./mad release script config_check

To automate the process I then made the Makefile:

# Author Tony Wallace
all:config_check

config_check:ebin/config_check.beam ebin/term_defs.beam rebar.config
./mad release script config_check

ebin/%.beam: src/%.erl
erlc -o ebin $< 

clean:
rm config_check ebin/*.beam




Thursday, 31 March 2016

Taming Configuration Files - general term processing

Overview

This instalment allows the matching of general Erlang terms including recursive structures such as filename and iolist.  Several weeks ago I did some work on matching Erlang terms.  This unpublished work has been extended.  

This extension involved the storing of parser state, and the change of the validation rules from two parameters to three parameters.  Code was broken in this process.  This state could have been stored in the process dictionary with a lot less rework, and perhaps where time is money, such as in a commercial environment it would have been done that way.  In this rework the existing tests became invaluable in ensuring that there was no loss of quality in the process.

Specifications

As per the earlier work, the intention is to check that an Erlang term conforms to a definition which is also an Erlang term.  This comparison is performed by term_defs:validate/2, which takes two parameters, the term specification, and the term respectively.  If the two do not match an error is thrown, listing both the rule and the data.

Syntactic Conventions

{} - denotes a tuple structure as per normal Erlang
Captialised - denotes a syntactic construction.  The details of which are covered in this article.
[] - denotes a list as per normal Erlang

Matching Atomic Values

The test for a given value is done by the {value,Value} term where the corresponding term in the expression to be tested must match Value.  Where a type test is required, the {builtin,TypeTest} test is used.  TypeTest is any unary function exported from the Erlang module that returns boolean.

Matching from a list of possible values

{options,[Item_Spec]}

In a options construction, one of the Item_Spec's in the list must match.  For example: {options,[{value,male},{value,female}]} indicates that the valid contents are one of the set male or female.

Matching a list

{list,TypeTest}

All items within the list must pass the type test.  For example to test that all the items in the list are integers use {list,{builtin,is_integer}}.

Defining a pattern

{define, Key, Pattern}

This associates a Pattern with a Key.  For example {define int {builtin is_integer}} associates the atom int with the check for an integer.  This symbolic referencing allows recursive data patterns to be defined.  Defining always returns true.

Matching all items in a list of tests

{match_all,[TypeTest]}

Although this could be used with several filters, this functionality is not usable because of the lack of user functions in this specification.  This is currently used to specify macros for recursive type tests using define.

Matching a tuple structure

{tuple, [Test]}

The number of tests in the test list must match the number of positions in the tuple, and each test must be passed for the tuple to be valid.  For example {tuple,[{value,employee},{list,{builtin,is_integer}]} will match {employee,"Joe Smith"}.

Matching a property list

{property_list, [ KeyValuePairTest ]}

The KeyValuePairTest is a tuple containing an optionality flag  which satisfies the rule {options,[{value,opt},{value,reqd}]}, a KeyName (which is an atom), and a data specification.  For every key with the reqd keyword, that item must exist in the data, and its associated value must be valid.  For every key with the opt keyword, if it exists then its associated value must be valid.  For example, a property list must contain gender, and the value must be either male or female.

{property_list [{reqd,gender,{options[{value,male},{value,female}]}]}

Although the property list construct could have been defined from other rules in this specification, it is a commonly used structure and was therefore given its own abstraction.

A complex example

{match_all,[
     {define,iolistmember,{options,[{builtin,is_integer},iolist]}},
     {define,iolist,{options,[{builtin,is_binary},{list,iolistmember}]}},
     iolist]}

This example tests that data is an iolist.  A valid iolist might be  [<<"This is a valid ">>,"iolist"].

An iolist is either a binary of a list of integers and io lists.  This example shows how recursive structures can be defined using a combination of match_all and define.  Notice how it is the last item in the match_all that causes the test to fire, and this item references back to the earlier defines.


The code


-module(term_defs).

-export ([validate/2,test/0]).

-author('Tony Wallace').
-purpose(  <<"Confirm that an erlang term matches a specification.",
  "The atom datadef matches any data definition",
  "Type testing is done by matching the erlang term to {builtin,functionanme}",
  "functionname is a unary function exported from the erlang module, for example"
  "{builtin,is_integer} will check that the matched term is an integer./n",
  "Where an erlang term must match a given term the {value,Term} pattern is used.",
  "The {value,Term} construction is valuable where there is a list of valid options",
           ", for example {options,[{value,option1},{value,option2}]}.  In this case the",
  "term can match either option1 or option 2./n",
  "A list is defined by the construction {list,DataDef}, where each item of the",
  "list must conform the the specification in DataDef.  For example a list of"
  "integers is defined as {list,{builtin,is_integer}}.\n",
  "Tuples are matched to {tuple,[DataDef]}.  Each term contained within the tuple",
  "must match its associated DataDef./n",
  "Property lists are given special treatment.  A property list is defined its contents",
  ".  The specification is:/n ",
  "     {property_list,[ ",
  "         {tuple, ",
  "             {options,{value,opt},{value,reqd}}, ",
  "             Key,Specification}]} ",
    " opt - this key is optional, reqd this key is required",
    "Key - is an erlang term, normally an atom\n ",
    "Specification a datadef that that key value must satisfy.">>).

validate(Def,Term) ->
    {R,_}=validate(Def,Term,dict:new()),
    R.
validate(Def,Term,State) ->
    %io:format("validate(~p,~p,~p)~n~n",[Def,Term,State]),
    case maybe_validate(Def,Term,State) of
{true,NewState} ->
   {true,NewState};
{false,_} ->
   not_valid(Def,Term)
    end.
%maybe_validate([Def],Term,State) ->
%    validate(Def,Term,State);
maybe_validate({define,Key,Value},_Term,State)  ->
    NewState = dict:store(Key,Value,State),
    {true,NewState};
maybe_validate(any,_,S) ->
    {true,S};
maybe_validate({property_list,KeyDefs},PL,State) ->
    Checked = [pl_entry(KeyDef,PL,State) || KeyDef <- KeyDefs],
    R=lists:foldl(fun(true,A) -> A;(_,_)->false end, true, Checked),
    {R,State};
maybe_validate({tuple,DefList},Term,S) 
  when is_tuple(Term) andalso (length(DefList) =:= tuple_size(Term)) ->
    TermList = tuple_to_list(Term),
    tuple_flds(DefList,TermList,S);
maybe_validate({tuple,_},_,S) ->
    %% tuple sizes do not match or Term is not a tuple
    {false,S};
maybe_validate({list,TermDef},Term,State) 
  when is_list(Term)  ->
    Valid=[validate(TermDef,X,State) || X <- Term],
    R=lists:foldl(fun({X,_},A) -> A and X end,true,Valid),
    {R,State};
maybe_validate(TermDef={builtin,Atom},Term,State)   ->
    case catch(apply(erlang,Atom,[Term])) of
{'EXIT',_} ->
   not_valid(TermDef,'undefined');
true -> {true,State};
false -> {false,State};
X -> 
   not_valid(TermDef,{'not_boolean',X})
    end;
maybe_validate({options,TermDef},Term,State) when is_list(TermDef) ->
    choices(TermDef,Term,State);
maybe_validate({match_all,[]},_,State)  ->
    {true,State};
maybe_validate({match_all,[H|T]},Term,State)  ->
    {R1,State1} = validate(H,Term,State),
    case R1 of
true ->
   validate({match_all,T},Term,State1);
false ->
   not_valid(H,Term)
    end;
maybe_validate({value,X},X,State) ->
    {true,State};
maybe_validate(datadef,{value,_},State) ->
    {true,State};
maybe_validate(datadef,{builtin,Fname},State)
  when is_atom(Fname)->
    EE = erlang:module_info(exports),
    case proplists:get_value(Fname,EE) of
1 -> {true,State};
_ -> {false,State}
    end;
maybe_validate(datadef,{list,ItemDef},State) ->
    validate(datadef,ItemDef,State);
maybe_validate(datadef,{property_list,[{Opt,_KeyName,DataDef}|KeyList]},State) ->
    validate([{value,opt},{value,reqd}],Opt,State),
    {true,NewState} = validate(datadef,DataDef,State),
    maybe_validate(property_list,KeyList,NewState);
maybe_validate(datadef,{property_list,[]},S) -> {true,S};
maybe_validate(datadef,{tuple,DefList},State) 
  when is_list(DefList)->
    Validated = [validate(datadef,X,State) || X <- DefList],
    R=lists:foldl(fun({true,_},A) -> A;(_,_) -> false end,true,Validated),
    {R,State};
maybe_validate(Key,Term,State) when is_atom(Key) ->
    case lookup(Key,State) of
undefined -> {false,State};
Pattern -> validate(Pattern,Term,State)
    end;
maybe_validate(_,_,State) ->
    {false,State}.

lookup(Key,Dict) ->
    lookup2(dict:is_key(Key,Dict),Key,Dict).
lookup2(false,_,_) ->
    undefined;
lookup2(true,Key,Dict) ->
    dict:fetch(Key,Dict).

pl_entry({Opt,Key,Def},PL,State) ->
    case proplists:get_value(Key,PL) of
undefined ->
   %% it is valid for an optional key to be undefined
   (Opt =:= opt);
Data ->
   %% if it exists it must be valid
   {true,_}=validate(Def,Data,State),
   true
    end.

tuple_flds([H1|T1],[H2|T2],State) ->
    case validate(H1,H2,State) of
{true,NewState} ->
   tuple_flds(T1,T2,NewState);
{false,_S} ->
   not_valid(H1,H2)
    end;
    

tuple_flds([],[],State) ->
    {true,State}.

choices([H|T],Term,State) ->
    %io:format("choices ([~p|~p],~p,~p)~n",[H,T,Term,State]),
    case maybe_validate(H,Term,State) of
{true,NewState} -> 
   {true,NewState};
{false,NewState} -> 
   choices(T,Term,NewState)
    end;

choices([],_,State) -> 
    {false,State}.


not_valid(Def,Term) ->
    throw({invalid,Def,Term}).
    
   
test() ->
    T=test([
   {datadef,{builtin,is_integer},true},
   {datadef,{value,value},true},
   {datadef,{tuple,[{value,employee}]},true},
   {datadef,{tuple,employee},invalid},
   {datadef,{list,{builtin,is_integer}},true},
   {datadef,{property_list,[]},true},
   {{builtin,is_integer},5,true},
   {{builtin,is_integer},atom,invalid},
   {{options,[{value,option1},{value,option2}]},option2,true},
   {{options,[{value,option1},{value,option2}]},option3,invalid},
   {{tuple,[{value,employee},{builtin,is_list}]},{employee,"Robert"},true},
   {{tuple,[{value,employee},{builtin,is_list}]},{emp,"Robert"},{invalid,{value,employee},emp}},
   {{tuple,[{value,employee},{builtin,is_list}]},{employee,"Robert",male},invalid},
   {{list,{builtin,is_integer}},[3,4,5],true},
   {{list,{builtin,is_integer}},[3,a,5],{invalid,{builtin,is_integer},a}},
   {{property_list,[]},[],true},
   {{property_list,[{opt,gender,{options,[{value,male},{value,female}]}}]},[],true},
   {{property_list,[{opt,gender,{options,[{value,male},{value,female}]}}]},[{gender,male}],true},
   {{property_list,[{opt,gender,{options,[{value,male},{value,female}]}}]},[{gender,transgender}],
    {invalid,{options,[{value,male},{value,female}]},transgender}},
   {{property_list,[{reqd,gender,{options,[{value,male},{value,female}]}}]},[],invalid},
   {{match_all,[{define,int,{builtin,is_integer}},int]},5,true},
   {{match_all,[{define,int,{builtin,is_integer}},int]},'hello',{invalid,{builtin,is_integer},hello}},
   {{match_all,[
     {define,iolistmember,{options,[{builtin,is_integer},iolist]}},
     {define,iolist,{options,[{builtin,is_binary},{list,iolistmember}]}},
     iolist]},
    <<"This is a valid iolist">>,true},
   {{match_all,[
     {define,iolistmember,{options,[{builtin,is_integer},iolist]}},
     {define,iolist,{options,[{builtin,is_binary},{list,iolistmember}]}},
     iolist]},
     [<<"This is a valid ">>,"iolist"],true},
   {{match_all,[
     {define,iolistmember,{options,[{builtin,is_integer},iolist]}},
     {define,iolist,{options,[{builtin,is_binary},{list,iolistmember}]}},
     iolist]},
     'This is not a valid iolist',
      {invalid,{options,[{builtin,is_binary},{list,iolistmember}]},'This is not a valid iolist'}},
   {{match_all,[
     {define,iolistmember,{options,[{builtin,is_integer},iolist]}},
     {define,iolist,{options,[{builtin,is_binary},{list,iolistmember}]}},
     iolist]},
     "This is a valid iolist",true}
 ]),
    Filtered=[{X,Y} || {X,Y} <- T, Y =/= pass],
    io:format("~p~n",[Filtered]).
test(L) when is_list(L) ->
    [test1(H) || H <- L].

test1(T={Def,Arg,_R}) ->
    ExpectedResult = make_result(T),
    case catch(validate(Def,Arg)) of
ExpectedResult -> {T,pass};
Failed -> {T,Failed}
    end.

make_result({_,_,true}) ->
    true;
make_result({Def,Arg,invalid}) ->
    {invalid,Def,Arg};
make_result({_Def,_Arg,Term}) ->
    Term.


Thursday, 17 March 2016

File contexts in Erlang

The problem

I decided to do some testing.  Each test was independent, and some were designed to throw errors.  As these processes throw errors, why not put them in separate processes and trap the exit signals.  Each test can be in its own directory with its own files this makes these tests independent of each other.  Finally now that they are independent run them in parallel and collect the results.

Pretty simple, what could go wrong?  Of course being computing nothing works as planned.  The tests worked individually but running in parallel they failed.  Tests were picking up files intended for the other test in the other directory.  Current directory was shared state, and what the state of that location is is determined by the last process to change it.  Relative file names were mapped against that global working directory.

The solution

The current directory was made part of process state, that is a change directory command that stored a value in the process dictionary.  Local versions of file operations, file:consult, file:write_file and others were written to execute in a local context.  Local io_format versions were written to output to the local directory to aid debugging.  Problem solved.

Thinking generally

This problem is not that different from the global name problem I wrote on earlier.  The context system should be extended with local directory and context sensitive file operations.  It is understandable why the Erlang runtime system is built the way it is, but as this experience shows, we can do better.

Friday, 11 March 2016

Taming Erlang Configuration files

Problem Outline

When building systems we normally have several configuration files.  We have these files for a number of reasons.
  • As part of the system boot.  The system may need to access various resources such as databases and webservers in order to start.  These resources in turn need data to be supplied to them for them to establish their function.
  • To cope with site specific conditions or business rules.
  • To set settings that tend to be static and considered unworthy of (or too sensitive for) a user interface.
Configuration files are an essential part of our delivered systems and are here to stay.

There are several aspects of configuration files that tend to be problematic.  
  • They are interpreted.  While it is true that the must conform to an overall syntax to be readable by the system, their contents are read and interpreted by the system in an adhoc and as needed basis.  There tends not to be the static analysis that would be performed on compiled code.
  • Their effects are dispersed throughout the system making it often difficult to associate the manifestation of the problem with its cause.
What I desire is a static analysis tool, one that says this is an invalid value, or this value is missing, at the time the configuration file is updated, rather than a strange fault of unknown origin occurring some time later.  Certainly there are tool such as XML parsers that can be used for this: putting configuration data in xml and parsing against a schema.  Once validated the data could become available.

Erlang systems tend to be configured with lists of erlang terms loaded in with file:consult.   This is a system understood and familiar to Erlang Programmers.  Besides is it really a good use of time rewriting existing erlang term configurations into XML?  What is needed is a way to assert certain properties of the configuration file.  By adding a definition file to existing Erlang configuration requirements, existing files can be used without modification, and the schema files are transferable between sites and projects.

Requirements

  • Everything is Erlang.  This is an Erlang shop.  Language choice is a strategic decision.
  • Do static analysis of configuration files.  Justification is the above section.
  • Keep it simple.  Rather a limited but useful tool new than a more sophisticated tool that is never delivered or is difficult to use or unreliable.
  • Sensible workflow/tool integration.  Static analysis should be part of deployment.  Tools like make should be able to detect out of date files and act accordingly.
  • Erlang configuration files tend to be property lists, which sometimes recursively embed property lists.  The system must be able to check that an arbitrary nested key exists.

Design

  1. Every configuration file has a schema entry which defines the definition to which the file conforms.  
  2. The definition file is a configuration file with a schema entry also.
  3. After checking a non human readable form of the configuration file is generated and it is this version that may be used by applications.
  4. By checking file timestamps, tools such as make can determine if a configuration file has been changed and needs rechecking.
  5. The Erlang module that provides check and compile facilities exports a "main/1" entrypoint so that is easy to integrate into tools like make using escript.
  6. Recursive processing of property lists to check key existence.
The master configuration file is as follows:

File conf_def.conf:

{schema,"conf_def.conf.etf"}.
{required_keys,[schema,required_keys]}.

As we can see this configuration file defines itself and validates against a compiled version of itself.  This is a chicken and egg situation resolved by the method conf:compile/1, which allows a compile without validation.  As we can see the value of required_keys is a list of keys, in this case the two keys that are required are schema and required, so this file conforms with its own definition.

A key can also be a list.  For example a key could be [webserver,[listen_port]] which says there should be a key in the configuration file called webserver, which has a property list for a value, and in that property list should be an entry with the key listen_port.

Code

-module (conf).
-export([main/1,compile/1,check/1,consult/1,checkcompile/1,test/0]).

main([]) ->
    ok;
main([H|T]) ->
    checkcompile(H),
    main(T).

checkcompile(F) ->
    check(F),
    compile(F).

consult(ConfFile) ->
    must_read(ConfFile++".etf").

compile(ConfFile) ->
    {ok,SrcData} = file:consult(ConfFile),
    BinData = erlang:term_to_binary(SrcData),
    FileName = ConfFile++'.etf',
    ok=file:write_file(FileName,BinData).

check(ConfFile) ->
    {ok,ConfData} = file:consult(ConfFile),
    ok=check_key(schema,ConfData),
    Schema = proplists:get_value(schema,ConfData),
    B=must_read(Schema),
    Spec = binary_to_term(B),
    Errors =
[
do_it(X,ConfData) || X <- Spec
],
    ErrorResults =
lists:filter(fun (X) -> ok =/= X end,Errors),  
    Status = (ErrorResults =:= []),
    case Status of
true ->
   ok;
false ->
   KL1 = [atom_to_list(X) || X <- ErrorResults],
   KL2 = lists:foldl(fun(X,Acc) -> X ++ ", " ++ Acc end,[],KL1),
   [$,,KL3] = KL2,
   Msg = "Errors for "++ConfFile++KL3,
   throw(Msg)
    end.

must_read(File) ->
    {A,B} = file:read_file(File),
    ok=match(A,ok,fun() -> ["Failed to read file ",File," reason ",atom_to_list(B)] end),
    B.
   
match(A,A,_) ->
    ok;
match(_,_,B) ->
    throw(iolist_to_binary(B())).

do_it({schema,_},_) ->
    %% already checked
    ok;
do_it({required_keys,KeyList},Data) ->
    MissingKeys = [check_key(X,Data)  || X <- KeyList],
    ErrorResults =
lists:filter(fun (X) -> ok =/= X end,MissingKeys),  
    Status = (ErrorResults =:= []),
    case Status of
true ->
   ok;
false ->
   KL1 = [atom_to_list(X) || X <- MissingKeys],
   KL2 = lists:foldl(fun(X,Acc) -> X ++ ", " ++ Acc end,[],KL1),
   [$,,KL3] = KL2,
   io_lib:format("~nMissing keys:~s~n",
[KL3])
    end.

check_key(Key,Data) when is_atom(Key) ->
    case proplists:is_defined(Key,Data) of
true ->
   ok;
false ->
   Key
     end;

check_key([],_Data) ->
    ok;
check_key(_L=[H|T],Data) ->
    %io:format("Checking key ~p in ~p~n",[_L,Data]),
    case check_key(H,Data) of
ok ->
   SubData = proplists:get_value(H,Data),
   check_key(T,SubData);
K -> K
    end.

test() ->
    WsSchemaString=
"{schema,\"conf_def.conf.etf\"}." ++ [$\r] ++
"{required_keys,[schema,webserver,[webserver,port]]}.",
    ok = file:write_file("webserver_schema.conf",WsSchemaString),    
    WsConfig = 
"{schema, \"webserver_schema.conf.etf\"}." ++ [$\r] ++
"{webserver, [{port,9600}]}.",
    ok = file:write_file("webserver.conf",WsConfig),  
    main(["webserver_schema.conf","webserver.conf"]).