Sunday, March 16, 2014

Counting subscripts in GTM using a simple method

Once you have many records inside your database, you may need a easy way to count your elements. There's many ways of doing that, let's look at some various techniques...
  1. using '$increment'
  2. using '+'
  3. using the 'for' parameters
The '$increment' method:
logins() ;
    n i,c
    f  s i=$o(^users(i)) q:i=""  d
    . s c=$increment(c,1)
    q c

The '+' method:
logins() ;
    n i,c
    f  s i=$o(^users(i)) q:i=""  d
    . s c=c+1
    q c

The 'for' method:
logins() ;
    n i,c
    f c=0:1 s i=$o(^users(i)) q:i=""
    q c

For me the best is the 'for' method because you can do other thing in the loop too. For example let's 'write' the counter:
GTM> f c=0:1 s i=$o(^users(i)) q:i=""  w c,!
0
1
2
3
..
58
59


Finally, look at this and train your brain to think in the GTM way:
add2(values,count)     ; w $$add2^test(.values,.count)
    n sum
    n c
    n i
    f c=0:1 s i=$o(values(i)) q:i=""  d
    . w c,") ",i,!
    . s sum=sum+i
    s count=c
    q sum


Inside a shell you test it like this:
GTM> s values(1)=""
GTM> s values(2)=""
GTM> s values(3)=""
GTM> n count
GTM> w $$add2^test(.values,.count)
0) 1
1) 2
2) 3
6
GTM> w count
3

Sharding globals with GTM

With GTM and "^variable" (globals variables), you can shard easily to handle the massive number of records. You can see that as partitioned tables for ODBC. But on steroids :)

For example we want to shard usernames and userid. Let's say that our userid is a non_neg_integer() of 10 digits. And that we want to shard on the 7 first digits...

Extracting 7 characters from a string is done using the '$e[xtract]' function:

GTM> w $extract("12345678910",0,7)
1234567

Now that you can extract characters, you can use the "_" operators to concatenate the results with whatever you want. For this experimentation we will use the "^users" global:

GTM> w "^users"_$extract("12345678910",0,7)
^users1234567


Here's the full code:
shard(userid)
    q $$%shard("users",userid)

%shard(root,id)
    n gbl
    n prefix s prefix=$e(id,0,7)
    q "^"_root_prefix


Everything from the shell:

GTM> zed "users"
[ Copy/Paste the code and exists your editor ]
GTM> zl "users"
GTM> w $$shard^users(1234567890)
^users1234567


With this new function you can easily access any userid without thinking about the shard method. You just need to use the 'indirection' notation from GTM i.e.'@variable'.

userinfo(userid)
    n gbl s gbl=$$shard(userid)
    w "Login for userid: ",userid,!
    w @gbl@("login")
    q

Saturday, March 15, 2014

The GTM $get operation

Introducing a new operation '$get' ('$g').
$get, try to retrieve the value then give it back or send its second parameter.
#GTM] s users("admin")="Administrator"
#GTM] s users("admin","password")="SecretP4ss"
#GTM] s users("admin","hint")="AntiNSQ"
#GTM] s user=$g(users("ADMIN"),"User not found")
#GTM] w user
User not found
#GTM] s user=$g(users("admin"),"User not found") w user
Administrator
#GTM]
Here's the code:

Your first GTM module

Let's create a module named 'hello' that can display 'world'.
  1. Start the client: mumps -dir
  2. Edit the file "hello": zed "hello"
  3. Write this code:  say w "hello",! q
  4. Save: :wq
  5. Compile: zl
  6. Run the code: d say^hello
  7. Enjoy !

The code in an indented version
say
  w "hello",!
  q
Here's the session:
#GTM] zed "hello"
[ INSIDE  YOUR $EDITOR ]
[ you save the file    ]
[ and quits the editor ]
#GTM] zl
#GTM] d say^hello
world

#GTM]

Some explanations:
say                   ; Name of the label
  w "hello",!         ; w[rite] the string "hello" followed by end of line: '!'
  q                   ; q[uit] return the control to the caller

You can use the fully qualified name for functions:
say                   ; Name of the label
  write "hello",!     ; w[rite] the string "hello" followed by end of line: '!'
  quit                ; q[uit] return the control to the caller

Egtm (GTM support for Erlang) using UTF-8 chset

If you've already seen this line, you know how this one can be frustrating :)
EGTM Common Error: egtm: 150373066,call^%egtmapi,%GTM-E-INVOBJ, Cannot ZLINK object file due to unexpected format,%GTM-I-TEXT, Object compiled with CHSET=UTF-8 which is different from $ZCHSET

Fighting with GTM to have the UTF8 routines of EGTM, I've finally won the round !
Here's my strategy: in rebar.config add the UTF-8 line ( and set the correct path for your gtm_dist:
{port_envs, [
  %% GT.M flags
  {".*", "gtm_dist", "/opt/application/lib/fis-gtm/V6.0-003_x86_64/utf8"},
  {".*", "gtm_chset", "UTF-8"},
  {".*", "LDFLAGS", "$LDFLAGS -I$gtm_dist -L$gtm_dist -Wl,-rpath -Wl,$gtm_dist -lgtmshr -lc"}
]}.
Then remove the 'egtm_worker.so' from the src directory, and finally recompile:
./rebar comp

Monday, September 3, 2012

Easy ejabberd clustering procedure

I was fed up with lack of easy ways to connect multiple ejabberd together. The documentation contains what you need but because of not so friendly command lines, I was never satisfied by the procedure.

So I've added a simple command "attach" to "ejabberdctl" that speed up the procedure, basically it uses already defined vars from the script:


    $EXEC_CMD "$ERL \
      $NAME $ERLANG_NODE \
      -mnesia dir \"\\\"$SPOOLDIR\\\"\" \
        -mnesia extra_db_nodes \"[$MASTER]\" \
      -s mnesia"

}
Apply this patch, and enjoy the "attach" command:
=== modified file 'src/ejabberdctl.template'
--- src/ejabberdctl.template    2012-08-02 14:39:16 +0000
+++ src/ejabberdctl.template    2012-08-31 09:06:56 +0000
@@ -364,13 +364,63 @@
         status=0
     }
     return $status
+}
+
+attach ()
+{
+
+RUNAS=${INSTALLUSER-root}
+
+cat <<END
+- Attaching Procedure - (running as '$RUNAS')
+
+You're about to initialise this local mnesia with another running instance.
+Data will be written to '$SPOOLDIR'
+
+First, determine which node do you want to use as master, and answer the question.
+
+Now, once you're in the erlang shell, type:
+mnesia:info().
+
+Look at the line that starts with: "running db nodes", and observe that your node
+is listed.
+
+Then type the following:
+mnesia:change_table_copy_type(schema, node(), disc_copies).
+
+You'll normally get: "{atomic,ok}"
+
+You can now close the shell using:
+q().
+
+You can start ejabberd now, and observe this new node in the webadmin...
+END
+
+echo -n "Specify which master do you want to use (ex: ejabberd@node02): "
+read MASTER
+
+        if [[ -n $MASTER ]]
+        then
+
+    $EXEC_CMD "$ERL \
+      $NAME $ERLANG_NODE \
+      -mnesia dir \"\\\"$SPOOLDIR\\\"\" \
+        -mnesia extra_db_nodes \"[$MASTER]\" \
+      -s mnesia"
+
+        else
+                echo
+
+        fi
 }

+
 case $ARGS in
     ' start') start;;
     ' debug') debug;;
     ' live') live;;
     ' started') wait_for_status 0 30 2;; # wait 30x2s before timeout
     ' stopped') wait_for_status 3 15 2; stop_epmd;; # wait 15x2s before timeout
+    ' attach') attach;;
     *) ctl $ARGS;;
 esac



Ejabberd cluster connection problem using mnesia ?

If you try to connect two ejabberd servers and still can't get the job done because mnesia seems to be blind, check out the content of the file "ejabberdctl.cfg". By default ejabberd vm don't accept network connections ...
#.
#' INET_DIST_INTERFACE: IP address where this Erlang node listens other nodes
#
# This communication is used by ejabberdctl command line tool,
# and in a cluster of several ejabberd nodes.
# Notice that the IP address must be specified in the Erlang syntax.
#
# Default: {127,0,0,1}

INET_DIST_INTERFACE={0,0,0,0}
Fix your file like the code above and retry...

Configuring ejabberd for BOSH using a specific url

Put this inside the "listen" part:
  {5280, ejabberd_http, [
                         {request_handlers,
                                [
                                        {["boshurl"], mod_http_bind}
                                ]
                        },
                         http_bind,
                         web_admin
                        ]}

Reload ejabberd, and connect to "yourdomain:5280/boshurl" and enjoy :)

Wednesday, October 12, 2011

"Pseudo Randomly" retrieve data...

When you want to crawl some websites and you want to hide yourself a little, here's a simple trick to change randomly your user-agent string.
There's a very convenient function in erlang called 'uniform' from the module 'random', you can use it by calling 'random:uniform(MaxValue)' where MaxValue is the high limit.

So if you want to generate a random value from 1 to 5 you can simple use, 'random:uniform(5)'...

Now that you know how to generate random values, here's some code that do just what's the title say:

Thursday, July 29, 2010

erlang: Simple Debug macro

When testing things I need some quick way to debug "a la" printf() style. Old habits :)

Here's is the simple debug macro I use:
-ifdef(debug).
-define(DEBUG(Format, Args),
  io:format("~s.~w: DEBUG: " ++ Format, [ ?MODULE, ?LINE | Args])).
-else.
-define(DEBUG(Format, Args), true).
-endif.
Write it down in a "debug.hrl" file, then you only need to add this line in any file header:
-include("debug.hrl").
This simple macro gives you the module name and the line number. This saves me a lot of time.

Then you need to define the "debug" atom to let your macro do what you want. The compile:file/2 handles options for this, the syntax is {d, debug}

Wednesday, February 24, 2010

Filtering lines efficiently

Whenever you are dealing with log lines or that you're program is filtering data you always have to handle 'escaping' efficiently.

While developing a log module using a gen_event, I needed to escape simple quotes.
Sometime thoses quotes were already escaped...

I've found this regexp to handle gracefully the case:
re:replace( Bin, "(?<!\\\\)'", "\\\\'", [ global ] ).

Tuesday, February 23, 2010

Reading an openssl .priv.key file and extracting the key

Extracting the private key from a .priv.key file is simple.
The private key is encrypted using a AES-128 with your passphrase.

The initial vector is also stored in the file, you can extract it directly from the first line:
get_salt( <<"Salted__", Salt:8/binary, Rest/binary>> ) ->
        {Salt, Rest}.

Tuesday, September 22, 2009

erlang: Parsing binary data dynamically

Hi,
here's a quick tip for parsing binary data which format is unknown at compile time...

Let's say that you have a binary string and that later you receive its structure. Take for
example the code below:
-module(binm).

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

test() ->
        test(<<4,0,0,0,5,0,0,0,7,0,8,0,33,1>>, [ 4, 4, 2, 2]).

test(Bin, List) ->
        {Final, End} = lists:foldl( fun(Len, {Res, Rest}) ->
                case Rest of 
                        <<M:Len/binary, NewRest/binary>> ->
                                {[ M | Res ], NewRest};
                        <<_:1/binary, NewRest/binary>> ->
                                { Res, NewRest}
                end
                end, {[], Bin}, List),
        {lists:reverse(Final), End}.

Precisely, we want to slice the binary part into 4 parts described as '[4, 4, 2, 2]' where each element is the size.
test() ->
        test(<<4,0,0,0,5,0,0,0,7,0,8,0,33,1>>, [ 4, 4, 2, 2]).

Let's compile and run:
2> c(binm).    
{ok,binm}
3> binm:test().
{[<<4,0,0,0>>,<<5,0,0,0>>,<<7,0>>,<<8,0>>],<<33,1>>}
4> 
Isn't this nice ? :p

Tuesday, August 18, 2009

erlang: testing many conditions easily with lists of funs...

Sometimes you have to test many things before being able to choose the next action...

In many languages, you'll end up using a bunch of "if then else".
But in erlang, and the power of fun()s, you can efficiently write a simple function that will do all the job for you :p

Here is our purpose: call many functions with one argument.
For this example, we need to determine a file type with its filename.

Let's say that:
- the filename could be a valid 'word' temporary file,
- or a 'excel' temporary file or
- a known file type.

Firts let's define a simple fun that takes a list of fun and stop evaluating those fun as soon as a result is found:
% the simple case where the list is empty
any(_, []) -> undefined;

% the general case when the list contains funs...
any(Arg, [ {F, PrepareFun} | Funs] ) ->
        case F( PrepareFun(Arg) ) of
                undefined ->
                        any(Arg, Funs);

                _V ->
                        _V
        end.

In this code you'll notice that there are two fun()s:
- the 'F',
- the 'PrepareFun'.
The idea is that 'PrepareFun' will be called before calling 'F' to filter the argument 'Arg'.
Imagine that sometimes you need to extract the basename from the filename, or whatever else...

The code is a simple list iteration that recurse only if the result of the function call is 'undefined'.

Now that we have a valid fun that can iterate over a list of funs and stop whenever a valid result is found (or end of list), let's get back to our example, and build our 'filetype' function:
fileType(File) ->
        any( File, [ 
                        {fun word_temp/1, fun filename:basename/1}, 
                        {fun db_extension/1, fun lists:reverse/1},
                        {fun excel_temp/1, fun filename:basename/1}
                ]).

You can read this code like this:
"any of the funs from the list may determine the type of the file".
And once found, stops.

Let's describe those called functions 'db_extension/1', 'word_temp/1', 'excel_temp/1'...

First 'db_extension':
You'll notice that we test only the end of the filename, that's why the filename is
reversed before being passed to the function:
db_extension( "pmt."  ++ _ ) -> temp;
db_extension( "PMT."  ++ _ ) -> temp;
db_extension( "xcod." ++ _ ) -> doc;
db_extension( "cod."  ++ _ ) -> doc;
db_extension( "xslx." ++ _ ) -> xls;
db_extension( "slx."  ++ _ ) -> xls;
db_extension( "xtpp." ++ _ ) -> ppt;
db_extension( "tpp." ++ _ ) -> ppt;
db_extension( _ ) -> undefined.


The 'word_temp/1' need to call the basename of the file but we don't need the full path, so 'PrepareFun' is simply 'filename:basename/1' in this case:
word_temp( "~$"   ++ _) -> temp;
word_temp( "~WRD" ++ _) -> temp;
word_temp( "~WRL" ++ _) -> temp;
word_temp( _ ) -> undefined.


For 'excel_temp/1', the temp file is determined by a number written as 8 hexadecimal values. We use the re module to easily match this with the filename. In this case the 'PrepareFun' is also the 'filename:basename/1':
excel_temp( File ) ->
        ReList = [ <<"^[0-9A-Z]{8}$">> ],
        do_re(File, ReList).
        
% We are able to test many re but in the specific 
% case the list contains only one element...
do_re(_, []) -> undefined;
do_re(Subject, [ Re | Rest ]) ->
        case re:run(Subject, Re, [{capture,none}]) of
                nomatch ->
                        do_re(Subject, Rest);

                match ->
                        temp
        end.

From the re module, options "capture none" is used to only returns if the re match, and not the part that successfully match...
(this is simple optimisation, since we don't care about the matching part)

If we look at back at what we've done here, we can see that
fileType(File) ->
        any( File, [ 
                        {fun word_temp/1, fun filename:basename/1}, 
                        {fun db_extension/1, fun lists:reverse/1},
                        {fun excel_temp/1, fun filename:basename/1}
                ]).

can really easily extended with other functions, as long as those new functions take only one parameter...
fileType(File) ->
        any( File, [ 
                        {fun word_temp/1, fun filename:basename/1}, 
                        {fun db_extension/1, fun lists:reverse/1},
                        {fun excel_temp/1, fun filename:basename/1},
                        {fun firefox_temp/1, fun filename:basename/1},
                        {fun directory_temp/1, fun(X) -> X end}
                ]).

Conclusion:
Building list of functions is an efficient way of "testing many conditions".

erlang: how to make a windows service

Tired of fighting with the command line to make erlsrv work ?
I have a solution for you !
The problem are always the quotes, you have quotes for erlang and quotes for the windows command line...
Here's what I use to test my service:

(Pack everything in a simple "install.bat")

erlsrv remove "YourService"
erlsrv add "YourService" -stopaction "init:stop()." -sname Service -debugtype reuse -args "-kernel error_logger {file,\\""C:/Test/kernel.txt\\""} -setcookie YourCookie -s YourInit"


YourInit is the name of the module you want to start. The fun "start/0" will be called by "erl".

This install.bat is meant to be your debug version of your service, because the log file will grow indefinitely.

See the documentation for more information:

DebugType: Can be one of none (default), new, reuse or console. Specifies that output from the Erlang shell should be sent to a "debug log". The log file is named "servicename".debug or "servicename".debug."n", where "n" is an integer between 1 and 99. The log-file is placed in the working directory of the service (as specified in WorkDir). The reuse option always reuses the same log file ("servicename".debug) and the new option uses a separate log file for every invocation of the service ("servicename".debug."n"). The console option opens an interactive Windows® console window for the Erlang shell of the service.
The console option automatically disables the StopAction and a service started with an interactive console window will not survive logouts, OnFail actions do not work with debug-consoles either. If no DebugType is specified (none), the output of the Erlang shell is discarded.
The consoleDebugType is not in any way intended for production. It is only a convenient way to debug Erlang services during development. The new and reuse options might seem convenient to have in a production system, but one has to take into account that the logs will grow indefinitely during the systems lifetime and there is no way, short of restarting the service, to truncate those logs. In short, the DebugType is intended for debugging only. Logs during production are better produced with the standard Erlang logging facilities.


If you don't define the "WorkDir" (-w option) your debug file will be located in the "WINDOWS\system32" directory.

Finally, the service will be described in the registry in

HKEY_LOCAL_MACHINE\SOFTWARE\Ericsson\Erlang\ErlSrv\1.1\YourService

Monday, August 17, 2009

erlang: Extracting values from binary streams with macros

Writing a lot of binary matching strings, I now use simple macros to synchronise erlang code with others language...
Let me explain a bit, there were many lines who look like theses:


parse(<< Id:32/little-unsigned, Oid:32/little-unsigned, Soid:16/little-unsigned >>, State) ->
...


Now I really prefer to see lines looking like this:


parse(<< ?UINT32( Id ),
?UINT32( Oid ),
?UINT16( Soid ) >>, State) ->
...


The magic trick was to define macros at the beginning of the erl module like this:

-define( UINT32(X), X:32/little-unsigned).
-define( UINT16(X), X:16/little-unsigned).


Now everyone can read those parse lines easily...

erlang: Unicode support for your filenames...

From R13B you have full unicode support for strings.

I'm involved in some kind of interface between the windows kernel and an erlang vm, and I find this "unicode" module really
helpful.

For your information, internally every file path or file name is encoded as a little endian utf16 string in the windows kernel.
Exchanging information between those two world means that you'll have to convert utf16 into ansi strings.

For example you can create an utf16 binary string using this

unicode:characters_to_binary("your string" latin1, {utf16,little}).


This means that your string is "latin1" and you want a binary utf16 little endian encoded.
Really easy !

Here's some free code that let you easily manipulate file paths and filenames...
I hope this will help someone :p


-module(filename_utils).

-export([extension/1, basename/1, dirname/1]).
-export([normalize/1, utf16toansi/1]).
-export([test/1]).


extension(Bin) ->
filename:extension( utf16toansi(Bin) ).

basename(Bin) ->
filename:basename( utf16toansi(Bin) ).

dirname(Bin) ->
filename:nativename( filename:dirname( utf16toansi(Bin) ) ).

test(Mode) ->
Word = "C:\\Program Files\\WINWORD.EXE",
File = unicode:characters_to_binary(Word, latin1, {utf16,little}),
?MODULE:Mode( File ).

utf16toansi(Bin) ->
unicode:characters_to_list(Bin, {utf16,little}).

normalize(File) when is_list(File) ->
Path = filename:dirname(File),
Base = filename:basename(File),
Ext = filename:extension(File),
{Base, Path, Ext};

normalize(Bin) when is_binary(Bin) ->
Path = dirname(Bin),
Base = basename(Bin),
Ext = extension(Bin),
{Base, Path, Ext}.

Monday, November 3, 2008

CEAN 1.4 is released

Tdoay is a great day, today you can read on the erlang mailling list that "CEAN 1.4 is released" ! ( this time it's R12B4 based release )
There's also a new website, new design and new Cean packages.

Go grab it !

Saturday, October 18, 2008

Secure Cookies for your web application...

Now that new erlang web framework are here, I think that sessions are still today a weakness.

Session and Cookies must be secure, there's no single day without some new vulnerability about session hijacking.

That's why very clever people design the secure cookie protocol [PDF].

Here's the Cookie value:
user name|expiration time|(data)k|HMAC( user name|expiration time|data|session key, k)

where
k=HMAC(user name|expiration time, sk)

and where sk is a secret key

Now you can verify the cookie using theses techniques:
1. Compare the cookie’s expiration time and the server’s current
time. If the cookie has expired, then return FALSE.
2. Compute the encryption key as follows:
k=HMAC(user name|expiration time, sk)
3. Decrypt the encrypted data using k.
4. Compute HMAC(user name|expiration time|data|session key, k),
and compare it with the keyed-hash message authentication code
of the cookie. If they match, then return TRUE;
otherwise return FALSE.
TRUE

Here's the erlang module
-module(scookies).
-export([start/0, gen_auth/1, gen_build/2, gen_check/2, read/1, check/4, test/0]).
-export([message/1]).

start() ->
application:start(crypto).

gen_build(ServerKey, IVec) ->
fun(Username, D, SessionKey) ->
Expiration = integer_to_list(1212559656),
Key = crypto:md5_mac( [Username, Expiration], ServerKey), %16bytes
Data = crypto:aes_cbc_128_encrypt(Key, IVec, D),
Hmac = crypto:sha_mac([Username, Expiration, Data, SessionKey], Key),
io:format("Build: ~p ~p ~p ~p ~p~n",[Username, Expiration, Data, SessionKey, Key]),
iolist_to_binary([ Username, $,, Expiration, $,, Data, $,, Hmac ])
end.

read(Cookie) ->
{A, B, C} = Cookie,
{A, B, C}.


gen_check(ServerKey, IVec) ->
fun(Cookie, SessionKey) ->
[ Username, Expiration, Crypted, Hmac ] = string:tokens(binary_to_list(Cookie), ","),
Key = crypto:md5_mac([ Username, Expiration ], ServerKey),
Data = crypto:aes_cbc_128_decrypt(Key, IVec, Crypted),
MAC = crypto:sha_mac([ Username, Expiration, Crypted, SessionKey], Key),
io:format("Check: ~p ~p ~p ~p ~p~n",[Username, Expiration, Data, SessionKey, Key]),
<<Len:16,Message:Len/binary,_/binary>> = Data,
io:format("Decrypted: ~p '~s'~n'~p'~n'~p'~n", [Len, Message, MAC, list_to_binary(Hmac)]),
[ Username, Expiration, {Len, Message}, MAC, list_to_binary(Hmac)]
end.

% Returns the build fun and check fun
% This is a helper fun to let you build in q simple way bot the build fun and
% the decode fun...
gen_auth(ServerKey) ->
IVec = <<"3985928509201031">>, %16bytes Must be Random
[ gen_build(ServerKey, IVec), gen_check(ServerKey, IVec) ].


check(Cookie, ServerKey, InitVec, SessionKey) ->
{Username, ExpirationTime, Crypted, CookieMAC} = read(Cookie),
case check_time(ExpirationTime) of % see later check_time...
ok ->
Key = crypto:sha_mac([ Username, ExpirationTime ], ServerKey),
Data = crypto:aes_cbc_128_decrypt(Key, InitVec, Crypted),
MAC = crypto:sha_mac([ Username, ExpirationTime, Data, SessionKey], Key),
compare(MAC, CookieMAC, Data);

_E ->
{error, _E}
end.

compare(_A, _A, Data) ->
{ok, Data};
compare(_A, _B, _Data) ->
{error, nomatch}.

check_time(1212559656) -> % It's up to you to set it
true;
check_time(_) ->
false.

message(Text) ->
Len = size(Text),
Pad = 64 - Len - 2,
<<Len:16,Text/binary, 0:Pad/unit:8>>.

test() ->
ServerKey = <<"serverkey">>,
SessionKey = <<"3ID409a0sd09">>,
[ Enc, Dec ] = gen_auth(ServerKey),
CCookie = Enc("rolphin", message(<<"stream/128693">>), SessionKey),
DCookie = Dec(CCookie, SessionKey),
io:format("C: ~s~n", [ CCookie ]),
display(DCookie).

display([Username, Expiration, {Len, Message}, _Mac, _Mac]) ->
io:format("Message ok: ~s (~s) ~p: '~s'~n", [Username, Expiration, Len, Message]);
display([Username, Expiration, {Len, Message}, _Mac, _OtherMac]) ->
io:format("Invalid Mac ! ~s (~s) ~p: '~s'~n", [Username, Expiration, Len, Message]).


Thursday, October 9, 2008

Benchmarking must be done carefully !

An example is better than a long post !

Timeout were hidden, smp by default modified the expected behaviour, and more...
Don't assume things, just take time to verify :)

Explained here

From the beginning:
started here

Sticky