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






Tuesday 19 April 2016

Gospel in 12 steps, step 2 God is real

God is real


Came to believe that a Power greater than ourselves could restore us to sanity.

Those in AA often call this higher power God, as in the serenity prayer:

God, grant me the serenity to accept the things I cannot change,
Courage to change the things I can,
And wisdom to know the difference.

By definition this higher power must be greater than the self, because the self has been unable to stop the drinking or whatever other sin or addiction is involved.   The self is too weak and someone, or something greater is required.

Similarly in Hebrews 11:6 we read:

And without faith it is impossible to please God, because anyone who comes to him must believe that he exists and that he rewards those who earnestly seek him.

At this point many Christians say "The don't believe in Jesus!  The bible says there is no other name under heaven by which man may be saved!" Acts 4:12.  I say to such people, did Abraham, or David know the name of Jesus?

These people remind me of the Pharisees in John 7:52 "They replied, “Are you from Galilee, too? Look into it, and you will find that a prophet does not come out of Galilee.”  In other words God must come on their terms.  In my introduction I spoke of actions speaking louder than words.  A dry drunk is a daily miracle.  Finally I would say, can our puny minds comprehend the Almighty?  Of course not!   Every one of us fails to grasp the eternal one, so if God grants them sobriety or overcomes some other addiction let us rejoice with them.  

Everyone's perception of God is in some way deficient, and so we are all in the same boat.  God understands our weakness, our need for a saviour, someone bigger and stronger than us to stand up for us, a cosmic big brother.  God is pleased when we take our emptiness and failure to him and say, I have tried and failed, I can't do this on my own.

Sunday 17 April 2016

Step 1: Powerlessness

The gift of powerlessness

  1. We admitted we were powerless over alcohol—that our lives had become unmanageable.
AA first step

The Parable of the Pharisee and the Tax Collector
9 To some who were confident of their own righteousness and looked down on everyone else, Jesus told this parable: 10 “Two men went up to the temple to pray, one a Pharisee and the other a tax collector. 11 The Pharisee stood by himself and prayed: ‘God, I thank you that I am not like other people—robbers, evildoers, adulterers—or even like this tax collector. 12 I fast twice a week and give a tenth of all I get.’

13 “But the tax collector stood at a distance. He would not even look up to heaven, but beat his breast and said, ‘God, have mercy on me, a sinner.’

14 “I tell you that this man, rather than the other, went home justified before God. 

Luke 18:9-14

(C) Freedom's just another word, for (G) nothing left to lose 
(D) Nothing ain't worth nothing but it's (G) free 
(C) Feeling good was easy Lord when (G) Bobby sang the blues 
(D) Feeling good was good enough for me 
Good enough for me and Bobby Mc(G)Gee

Me and Bobby McGee
Lyrics  Kris Kristofferson and Fred Foster

Powerlessness and pain.  A normal human condition that is for some the beginning of the journey of hope which is the gospel, the good news.  Indeed this powerlessness becomes the start of our desperate search for salvation.  

Consider the beatitudes:

Blessed are the poor in spirit, theirs is the kingdom of heaven.

Out of our desperate spiritual poverty, we cry out, and God hears.


Tuesday 12 April 2016

The Gospel of the 12 steps

A personal introduction

When I was a teenager, around 16 or 17 years of age, I used to go and visit a neighbour a couple of doors down the street.  To save time I would cut through the next door neighbour, and I would visit Michael.

I imagine at the time he would have been in his mid forties.  We would sit at his kitchen table have a coffee and talk late into the night.  He would talk to me about life, but mostly he would talk about AA, Alcoholics Anonymous.  

A couple of years later, I sat in the lounge of another man and I became a Christian.

These blog posts are intended to bring some synchronism between the two.  There is no doubt that the history of AA is indebted to Christianity, yet at the same time, I contend, much of the church needs to be learning from AA.

Every dry day for every member of AA is a new miracle, a new testimony to deliverance.  This is salvation.  We have proverbs such as "seeing is believing", "actions speak louder than words", "the proof of the pudding is in the eating".  Jesus himself asked those who witnessed his works to believe his words because of his works.  The man born blind certainly regarded Jesus as a prophet because of his deeds.  

I do remember as a young Christian hearing AA being criticised as not acknowledging Jesus.  I will deal with that in a later post, but apart from that there is little that a Christian could object to in the 12 steps.  Indeed these steps should be part of the life of every Christian.

It is my intention to unpack these steps over the next few weeks.  So much of our organized Christianity is in opposition to the gospel.  Remember Jesus teaching of the Pharaisee and the Tax Collector.  Which do we welcome in our churches, the drunks, the druggies, the smelly, or do we all have to appear to have it all together?  Are our churches safe places or places of hypocracy, judgement and moral superiority.  What would Jesus say about our churches today?  Are we really ready to admit our faults and be real?  Is it safe to do so?  How can we truly become the "Body of Christ"?

I truly believe that we can learn from AA the reality of the saving power of Jesus Christ.  AA has codified the gospel into baby steps for us to follow.

The 12 steps

  1. We admitted we were powerless over alcohol—that our lives had become unmanageable.
  2. Came to believe that a Power greater than ourselves could restore us to sanity.
  3. Made a decision to turn our will and our lives over to the care of God as we understood Him.
  4. Made a searching and fearless moral inventory of ourselves.
  5. Admitted to God, to ourselves, and to another human being the exact nature of our wrongs.
  6. Were entirely ready to have God remove all these defects of character.
  7. Humbly asked Him to remove our shortcomings.
  8. Made a list of all persons we had harmed, and became willing to make amends to them all.
  9. Made direct amends to such people wherever possible, except when to do so would injure them or others.
  10. Continued to take personal inventory, and when we were wrong, promptly admitted it.
  11. Sought through prayer and meditation to improve our conscious contact with God as we understood Him, praying only for knowledge of His will for us and the power to carry that out.
  12. Having had a spiritual awakening as the result of these steps, we tried to carry this message to alcoholics, and to practice these principles in all our affairs.

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"]).