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.
- The result is an OTP application without external dependencies
- Configuration file changes can be tracked with normal text based source control procedures. This would not be possible with a traditional database solution.
- 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:
- Loading globals.config.etf as state in init
- 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