16.1 The Road to the Generic Server
We're going to write four little server called server1,server2...,each slightly
different from the last.The goal is to totally separate the non-functional parts
of the problem from the functional parts of the problem .
Server 1:The Basic Server
Here's our first attempt.It's a little server that we can parameterize with a callback
module:
点击(此处)折叠或打开
- -module(server1)
- -export([start/2,rpc/2])
- start(Name,Mod) ->
- register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).
- rpc(Name,Request) ->
- Name ! {self(),Request},
- receive
- {Name,Response} -> Response
- end.
- loop(Name,Mod,State) ->
- receive
- {From,Request} ->
- {Response,State1} = Mod:hanle(Request,State),
- From ! {Name,Response},
- loop(Name,Mod,State1)
- end.
a callback for server1.Here's a name server callback:
点击(此处)折叠或打开
- -module(name_server)
- -export([init/0,add/2,whereis/1,hanle/2]).
- -import(server1,[rpc/2]).
- %%client routines
- add(Name,Place) -> rpc(name_server,{add,Name,Place}).
- whereis(Name) -> rpc(name_server,{whereis,Name}).
- %llback routines
- init() -> dict:new().
- handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
- handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.
the server framework code,ant at the same time,it contains the interfaceing routines that
will be called by the client.The usual OTP convention is to combine both functions in the
same module.
Just to prove that it works,do this:
点击(此处)折叠或打开
- 1> server1:start(name_server,name_server).
- true
- 2>name_server:add(joe,"at home").
- ok
- 3>name_server:whereis(joe).
- {ok,"at home"}
,no receive,no register.It is pure sequential code-nothing else.What dose this
mean?
This means we can write client-server modules without understanding anything about
the underlying concurrency modules.
This is the basic pattern for all servers.Once you understand the basic structure,
it's easy to "roll your own."
Server 2:A Server with Transactions
Here's a server that crashes the client if the query in the server results in an
exception:
点击(此处)折叠或打开
- -module(server2)
- -export([start/2,rpc/2]).
- start(Name,Mod) ->
- register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).
- rpc(Name,Request) ->
- Name ! {self(),Request},
- receive
- {Name,crash} -> exit(rpc);
- {Name,ok,Response} -> Response
- end.
- loop(Name,Mod,OldState) ->
- receive
- {From,Request} ->
- try Mod:handle(Request,OldState) of
- {Response,NewState} ->
- From ! {Name,ok,Response},
- loop(Name,Mod,NewState)
- catch
- _:Why ->
- log_the_error(Name,Request,Why),
- From ! {Name,crash},
- loop(Name,Mod,OldState)
- end
- end.
- log_the_error(Name,Request,Why) ->
- io:format("Server ~p request ~p~n"
- "caused exception ~p~n",
- [Name,Request,Why]).
of State if an exception was raised in the handler function.But if the handler function
succeeded,then it loops with the value of newState provide by handler fucntion.
Why does it retain the original state? When the handler function fails,the client that
sent the message that caused the failure is sent a message thate causes it to crash.
The client cannot proceed,because the request it sent to the server caused the handler
function to crash.But any other client that wants to use the server will not be affected.
Moreover,the state of server is not changed when an error occurs in the handler.
Note that the callback module for this server is exactly the same as the callback module
we used for server1.By changing the server and keeping the callback module constant,we can
change the nonfunctional behavior of the callback module.
Note: The last statement wasn't strictly true.We have to make a very small change to the
callback module when we go from server1 to server2,and that is to change the name in the
-import declaration from server1 to server2.Otherwise,there are no changes.
Server 3: A Server with Hot Code Swapping
Now we'll add hot code swapping:
点击(此处)折叠或打开
- -module(server3).
- -export([start/2,rpc/2,swap_code/2]).
- start(Name,Mod) ->
- register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).
- swap_code(Name,Mod) -> rpc(Name,{swap_code,Mod}).
- rpc(Name,Request) ->
- Name ! {self(),Request},
- receive
- {Name,Response} -> Response
- end.
- loop(Name,Mod,OldState) ->
- receive
- {From,{swap_code,NewCallBackMod}} ->
- From ! {From,ack},
- loop(Name,NewCallBackMod,OldState);
- {From,Request} ->
- {Response,NewState} = Mod:handle(Request,OldState),
- From ! {Name,Response},
- loop(Name,Mod,NewState)
- end.
If we send the server a swap code message,then it will change the callback module to the new
module contained in the message.
We can demonstrate this by starting server3 with a callback module and then dynamically swapping the callback module.We can't use name_server as the callback module because we hard-compiled the name of the server into the module.So,we make a copy of this,calling it name_server1 where we change the name of the server:
点击(此处)折叠或打开
- -module(name_server1)
- -export([init/0,add/2,whereis/1,hanle/2]).
- -import(server3,[rpc/2]).
- %%client routines
- add(Name,Place) -> rpc(name_server,{add,Name,Place}).
- whereis(Name) -> rpc(name_server,{whereis,Name}).
- %llback routines
- init() -> dict:new().
- handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
- handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.
点击(此处)折叠或打开
- 1> server3:start(name_server,name_server1).
- true
- 2> name_server:add(joe,"at home").
- ok
- 3> name_server:add(helen,"at job").
- ok
With lightning speed,we fire up our text editor and write a new callback module:
点击(此处)折叠或打开
- -module(new_name_server)
- -export([init/0,add/2,whereis/1,all_names/0,delete/1,hanle/2]).
- -import(server3,[rpc/2]).
- %%client routines
- all_names() -> rpc(name_server,allNames).
- delete(Name) -> rpc(name_server,{delete,Name}).
- add(Name,Place) -> rpc(name_server,{add,Name,Place}).
- whereis(Name) -> rpc(name_server,{whereis,Name}).
- %llback routines
- init() -> dict:new().
- handle({add,Name,Place},Dict) -> {ok,dict:store(Name,Place,Dict)};
- handle(allNames,Dict) -> {dict:fetch_keys(Dict),Dict};
- handle({delete,Name},Dict) -> {ok,dict:erase(Name,Dict)};
- handle({whereis,Name},Dict) -> {dict:find(Name,Dict),Dict}.
点击(此处)折叠或打开
- 4> c(new_name_server).
- {ok,new_name_server}
- 5> server3:swap_code(name_server,new_name_server).
- ok
点击(此处)折叠或打开
- 6> new_name_server:all_names().
- [joe,helen]
Now stop and think again.The last two tasks we have done are generally considered to by pretty difficult,in fact,very difficult.Servers with "transactions semantics" are difficult to write;servers with dynamic code upgrade are very difficult to write.
This technique is extremely powerful.Traditionally we think of servers as programing with state that change state when we send them message.The code in the servers is fixed the first time it is called,and if we want to change the code in the server,we have to stop the server and change the code,and then we can restart the server.In the examples we have given,the code in the server can be changed just as easily as we can change the state of server.
Server 4: Transactions and Hot Code Swapping
In the last two servers,code upgrade and transaction semantics were separate.Let's combine them into single server.Hold onto you hats....
点击(此处)折叠或打开
- -module(server2)
- -export([start/2,rpc/2]).
- start(Name,Mod) ->
- register(Name,spawn(fun() -> loop(Name,Mod,Mod:init()) end)).
- rpc(Name,Request) ->
- Name ! {self(),Request},
- receive
- {Name,crash} -> exit(rpc);
- {Name,ok,Response} -> Response
- end.
- loop(Name,Mod,OldState) ->
- receive
- {From,{swap_code,NewCallBackMod}} ->
- From ! {Name,ok,ack},
- loop(Name,NewCallBackMod,OldState);
- {From,Request} ->
- try Mod:handle(Request,OldState) of
- {Response,NewState} ->
- From ! {Name,ok,Response},
- loop(Name,Mod,NewState)
- catch
- _:Why ->
- log_the_error(Name,Request,Why),
- From ! {Name,crash},
- loop(Name,Mod,OldState)
- end
- end.
- log_the_error(Name,Request,Why) ->
- io:format("Server ~p request ~p~n"
- "caused exception ~p~n",
- [Name,Request,Why]).
Server 5: Even More Fun
Now that we've got the idea of dynamic code change,we can have even more fun.Here's a server that does nothing at all until you tell it to become a particular type of server:
点击(此处)折叠或打开
- -module(server5).
- -export([start/0,rpc/2]).
- start() -> spawn(fun() -> wait() end).
- wait() ->
- receive
- {become,F} -> F()
- end.
- rpc(Pid,Q) ->
- Pid ! {self(),Q},
- receive
- {Pid,Reply} -> Reply
- end.
点击(此处)折叠或打开
- 1> Pid = server5:start().
- <0.57.0>
Let's now define a server function.It's nothing complicated,just something to compute factorial:
点击(此处)折叠或打开
- -module(my_fac_server).
- -export([loop/0]).
- loop() ->
- receive
- {From,{fac,N}} ->
- From ! {slef(),fac(N)},
- loop();
- {become,Something} ->
- Something()
- end.
- fac(0) -> 1;
- fac(N) -> N*fac(N-1).
点击(此处)折叠或打开
- 2> c(my_fac_server).
- {ok,my_fac_server}
- 3> Pid ! {become,fun my_fac_server:loop/0}.
- {become,#Fun<my_fac_server.loop.0>}
点击(此处)折叠或打开
- 4> server5:rpc(Pid,{fac,30}).
- 2652585981219058636308480000000
As you can see from the previous examples,we can make range of diffierent types of servers,with different semantics and some quite surprising properties.This technique is almost too powerful.Used to its full potential,it can yield very small programs of quite surprising power and beauty.When we make industrail-scale projects with dozens to hundreds of programmers involved,we might not actually want things to be too dynamic.We have to try to strike a balance between having something general and powerful and having something that is useful for commercial products.Having code that can morph into new sessions as runs is beautiful but terrible to debug if something goes wrong later.If we have made dozens of dynamic changes to our code and it then crashes,finding out exactly what went wrong is not easy.
The server example in this section are not actually not quite correct.They are written this way so as to emphasize the ideas involved, but they do have one or two extremely and subtle errors.I'm not going to tell you immediately what they are,but I'll give you some hints at the end of the chapter.
The Erlang module gen_server is the kind of logical conclusion of a succession of successively sophisticated servers (just like the one we've written so far in this chapter).
It has been in use in industrial products since 1998.Hundreds of servers can be part of single product.These servers have been written by programmers using regular sequential code.All the error handling and all the nonfunctional behavior is factor out in the generic part of the server.
So now we'll make a great leap of imagination and look at the real gen_server.