Thursday, January 14, 2010

Minor Erlang Interface Tricks

erlrc requires that you drop a file whose name is your node name and whose contents is the node cookie into a particular directory so that the packaging system can find running Erlang VMs and ask them to do hot-code upgrades. It's not that hard, but I figured I would put some shell scripts into the erlrc google code project demonstrating how to do this and other minor tricks that make it a little nicer to talk to an Erlang VM from the (non-Erlang) command line.
All of these scripts are driven by a POSIX shell syntax configuration file describing an Erlang VM, which can be pretty short. Here's one I'm using right now for my personal stuff:
% cat /usr/share/myerlnode/myerlnode.rc
node_name_file='/etc/erlrc.d/nodes/erlang'

run_extra_args='+A 64 -noshell -noinput -s crypto -s mnesia -eval "case erlrc_boot:boot () of ok -> ok; _ -> init:stop () end" >erlang.out 2>erlang.err &'

node_name_file (the only required config setting) defines what the node name will be (via the basename). This config file drives four scripts:
  1. erlstart-run-erlang: starts an Erlang VM. doesn't do that much, really: creates the node file for erlrc, and sets the heart command in case you want to use heart.
  2. erlstart-remsh: starts a remote shell on an Erlang VM.
  3. erlstart-eval: takes the argument, evals it on an Erlang VM, and prints the result to standard out. very useful shell script glue for maintenance scripts.
  4. erlstart-etop: runs etop on an Erlang VM.
If you put your config file in the default location (/etc/erlstart.rc) or export an environment variable indicating the location (ERLSTART_CONFIG_FILE) then things are pretty zero-configuration. Doing this kind of thing at the (non-Erlang) shell gets very addictive:
% erlstart-eval 'application:which_applications()'
[{anwhereos,"Rest API for time series persistence and retrieval.","0.1.0"},
{drurlyjsclientsrv,"Serve the drurly jsclient from the drurly server.",
"0.0.1"},
{drurly,"Social sharing server.","2.2.0"},
{mcedemo,"TinyMCE + Nitrogen demo.","1.3.0"},
{inets,"INETS CXC 138 49","5.0.12"},
{mochiweb,"MochiWeb is an Erlang library for building lightweight HTTP servers.",
"0.2009.05.26"},
{nitrogen,"Nitrogen web framework for Erlang.","0.2009.05.12.3"},
{nitromce,"A Nitrogen element which corresponds to a TinyMCE editor instance.",
"4.0.1"},
{sgte,"String template language for Erlang.","0.7.1"},
{signzor,"Erlang library to generate signed printable encodings.","0.0.1"},
{tcerl,"Erlang driver for tokyocabinet.","1.3.1h"},
{webmachine,"An Erlang REST framework.","0.2009.09.24b"},
{erlrc,"Extensible application management.","0.2.3"},
{mnesia,"Mnesia storage API extensions.","4.4.7.6.1"},
{crypto,"CRYPTO version 1","1.5.3"},
{sasl,"SASL CXC 138 11","2.1.5.4"},
{stdlib,"ERTS CXC 138 10","1.15.5"},
{kernel,"ERTS CXC 138 10","2.12.5"}]
erlstart-run-erlang is fairly low level, so for an init script I would do something like the following: first, a little stub installed into /etc/rc.d
#! /bin/sh

# chkconfig: 2345 20 80
# description: Control my personal Erlang node.

exec myerlnodectl "$@"

and then the actual guts installed as myerlnodectl
#! /bin/sh

eval_with_main_node () \
{
erl -name myerlnodetmp$$ \
-hidden \
-setcookie "$cookie" \
-noshell -noinput \
-eval "MainNode = list_to_atom (\"$1\"), $2" \
-s erlang halt
}

get_hostname () \
{
erl -name myerlnodetmp$$ -setcookie $$ -noshell -noinput -eval '
[ Host ] = tl (string:tokens (atom_to_list (node ()), "@")),
io:format ("~s~n", [ Host ])
' -s init stop
}

id=`basename "$0"`

if test -d /root
then
HOME=${HOME-/root}
else if test -d /var/root
then
HOME=${HOME-/var/root}
fi
fi
export HOME

ERLSTART_CONFIG_FILE=${ERLSTART_CONFIG_FILE-/usr/share/myerlnode/myerlnode.rc}
export ERLSTART_CONFIG_FILE

. "$ERLSTART_CONFIG_FILE"

cookie=${cookie-turg}
user=${user-erlang}
hostname=${hostname-`get_hostname`}
node_name=`basename "$node_name_file"`
full_name="$node_name@$hostname"
shutdown_file=${shutdown_file-/var/run/myerlnode.shutting_down}

ERL_CRASH_DUMP=${ERL_CRASH_DUMP-/dev/null}
export ERL_CRASH_DUMP

case ${1-"status"} in
start)
test "`id -u`" -eq 0 || exec sudo $0 "$@"

printf "Starting Erlang... "

if test -f "$node_name_file" && \
test true = "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "already started."
exit 0
fi

pid=`eval_with_main_node "$full_name" '
io:format ("~p", [
case rpc:call (MainNode, os, getpid, []) of
{ badrpc, _ } -> undefined;
Pid -> list_to_integer (Pid)
end ])'`

test "$pid" -gt 0 2>/dev/null && {
test -f "$shutdown_file" && {
oldpid=`cat "$shutdown_file"`
test "$pid" -eq "$oldpid" && {
echo "shutdown in progress (pid = '$pid')." 1>&2
exit 1
}
}
}

rm -f "$shutdown_file"

# check for -s shell support
su -l -s /bin/sh $user -c true >/dev/null 2>/dev/null

if test $? = 0
then
dashs="-s /bin/sh"
else
dashs=""
fi

${niceness+ nice -n $niceness} \
su -l $dashs "$user" -c \
"env cookie=\"$cookie\" erlstart-run-erlang \"$ERLSTART_CONFIG_FILE\""

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case net_adm:ping (MainNode) of
pong ->
ok;
pang ->
timer:sleep (100),
Cont (Cont, Max - 1)
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: could not connect to node after 10 seconds" 1>&2
exit 1
}

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case rpc:call (MainNode, init, get_status, []) of
{ started, _ } ->
ok;
{ starting, _ } ->
timer:sleep (100),
Cont (Cont, Max - 1);
{ Status, _ } ->
{ failed, Status }
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: node did not boot after 10 seconds" 1>&2
exit 1
}

echo "done."
;;

stop)
test "`id -u`" -eq 0 || exec sudo $0 "$@"

printf "Stopping Erlang... "

if test ! -f "$node_name_file" || \
test true != "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "not running."
exit 0
fi

pid=`erlstart-eval 'os:getpid ()' 2>/dev/null`

test "$pid" -gt 0 2>/dev/null && {
printf '%s' $pid > "$shutdown_file"
chown $user:$user "$shutdown_file"
}

erlstart-eval 'init:stop ()' >/dev/null 2>/dev/null

eval_with_main_node "$full_name" \
"Wait = fun (_, 0) ->
failed;
(Cont, Max) ->
case net_adm:ping (MainNode) of
pong ->
timer:sleep (100),
Cont (Cont, Max - 1);
pang ->
ok
end
end,
DontTellMe = 100,
ok = Wait (Wait, DontTellMe)" || {
echo "" 1>&2
echo "$id: node still responsive after 100 seconds" 1>&2
exit 1
}

rm -f "$node_name_file"

echo "done."
;;

status)
if test -f "$node_name_file" && \
test true = "`erlstart-eval 'true' 2>/dev/null`" 2>/dev/null
then
echo "Erlang is running"
else
echo "Erlang is not running"
fi

;;

*)
echo "$id: unknown command $1" 1>&2
exit 1
esac

exit 0

Hopefully you found that inspirational.

Wednesday, December 23, 2009

erlrc and rpm

Two of the Dukes now work at OpenX, which is starting to dip its toe into the Erlang waters. They use CentOS so they agreed to fund porting framewerk and erlrc to rpm; previously I'd only used them with deb.

A bit of background: erlrc is a set of Erlang modules and shell scripts that are designed to be integrated into packaging hooks so that installation, upgrade, or removal of a package causes corresponding hot-code activity to happen inside registered Erlang VMs on the box. Since erlrc was designed to easily integrate with multiple package managers, getting it to work with rpm was mostly about me understanding rpm's package hooks model. The result is an experience like this,

% sudo yum -q -y install lyet
erlrc-start: Starting 'lyet': (erlang) started
% sudo yum -q -y remove lyet
erlrc-stop: Stopping 'lyet': (erlang) unloaded

i.e., installing an rpm causes a running Erlang VM on the box to hot-code load the new modules and start the associated application, and removing the rpm causes the associated application to be stopped and the corresponding modules to be hot-code unloaded.

If you use fw-template-erlang than the appropriate packaging hooks are added for you automatically, both for deb and now rpm. However even manual creation of rpm spec files is pretty easy:
  • erlrc-stop is called in %preun if the installation count indicates removal
  • erlrc-upgrade is called in %preun if the installation count indicates upgrade
  • erlrc-start is called in %posttrans
Also, the erlrc shell scripts want to know the previously installed version, so I call rpm -q in a %pretrans hook and save the result. Longer term, erlrc should probably ask the Erlang VM it is talking to what version is running to eliminate the need for this argument (I was a bit surprised that rpm doesn't provide this argument to the package hook like debian does; it seems very useful for creating version-specific upgrade fixes).

Tuesday, October 6, 2009

Linear Programming with Erlang

So you have to solve a linear program, so naturally the first language you think of is Erlang. Actually, it's not a natural first choice for most people, but if you are solving a linear program as part of an automatic control strategy for an internet facing application, the choice is better motivated.

Since I faced this situation recently I wrote a binding for GLPK for Erlang. Writing port drivers is a drag so I actually wrote a program to generate the C and Erlang for me. Perhaps with the new FFI these sort of games will not be necessary, but I was happy with the approach because I anticipate experimenting with several linear programming packages, which should be much easier to accommodate.

Available at Google code.