Skip to content

Instantly share code, notes, and snippets.

@sgobotta
Last active June 25, 2020 07:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sgobotta/08a1dc72b3f6c240c9db6cab2eb4c861 to your computer and use it in GitHub Desktop.
Save sgobotta/08a1dc72b3f6c240c9db6cab2eb4c861 to your computer and use it in GitHub Desktop.
Gist for the FeatureLearn Concurrent Programming In Erlang course (1.5) "Exercise: Trying it for yourself"
-module(server).
-author("Santiago Botta <santiago@camba.coop>").
-include_lib("eunit/include/eunit.hrl").
-export([server/0, server/1, proxy/1]).
-export([start/1, check/2, stop/1]).
% Testing purpose
-export([send_multiple_requests/3]).
%%%-------------------------------------------------------------------
%% @doc Server tests
%% @end
%%%-------------------------------------------------------------------
server_test() ->
Assertions = [
{"Adam", {result, "Adam is not a palindrome."}},
{"Madam Im Adam", {result, "Madam Im Adam is a palindrome."}}
],
Proxy = start(4),
ok = lists:foreach(
fun ({Request, _Response}) ->
mock_check_request(self(), Proxy, Request)
end,
Assertions
),
ok = lists:foreach(
fun ({Request, Response}) ->
?assertEqual(Response, mock_check_response(Request))
end,
Assertions
),
stop = server:stop(Proxy),
ok.
mock_check_request(From, Server, Request) ->
spawn(fun () ->
Response = server:check(Server, Request),
From ! {Request, Response} end
).
mock_check_response(Request) ->
receive
{Request, Response} ->
Response
end.
%% @doc Given a process id, listens to palindrome requests to return a processed
%% result.
%% Usage:
%% ServerPid = spawn(server, server, [self()]).
%% ServerPid ! {check, "MadamImAdam"}.
%% flush().
%% @end
server(From) ->
receive
{check, String} ->
IsPalindromeResult = is_palindrome(String),
From ! {result, String ++ IsPalindromeResult},
server(From);
stop ->
ok
end.
%% @doc Takes requests from multiple clients
%% Usage:
%% ServerPid = spawn(server, server, []).
%% ServerPid ! {check, "MadamImAdam", self()}.
%% flush().
%% @end
server() ->
receive
{check, String, From} ->
IsPalindromeResult = is_palindrome(String),
io:format("~p ::: checks palindrome ~s from: ~p~n", [self(), String, From]),
From ! {result, String ++ IsPalindromeResult},
server();
stop ->
ok
end.
%% Replicating the server
%% @doc Given a list of server pids, calls a function that accepts a request to
%% one of them and distributes the next requests to the rest of the servers
%% indefinately.
%% Usage:
%% Server1 = spawn(server, server, []).
%% Server2 = spawn(server, server, []).
%% Server3 = spawn(server, server, []).
%% Proxy = spawn(server, proxy, [[Server1, Server2, Server3]]).
%% @end
proxy(Servers) ->
proxy(Servers, Servers).
%% @doc Given a list of server pids and a pid accumulator listens to requests
%% and delegates future requests to the next pid in the servers list
%% indefinately.
proxy([], Servers) ->
proxy(Servers, Servers);
proxy([S|Svrs], Servers) ->
receive
stop ->
lists:foreach(
fun (Server) ->
Server ! stop,
io:format("Terminating ~p...~n", [Server])
end,
Servers
),
ok;
{check, String, From} ->
S ! {check, String, From},
proxy(Svrs, Servers)
end.
%% @doc Given a list of servers, sends a stop message to each one.
stop(Server) ->
io:format("Terminating ~p...~n", [Server]),
Server ! stop.
%%%-------------------------------------------------------------------
%% @doc server API
%% @end
%%%-------------------------------------------------------------------
%% @doc Given an integer, spawns a proxy server with N servers as argument.
start(N) ->
start(N, []).
%% @doc Starts N servers to return a tuple where the first component is the
%% proxy pid and the second component the list of spawned server pids.
start(0, Servers) ->
spawn(?MODULE, proxy, [Servers]);
start(N, Servers) ->
Server = spawn(?MODULE, server, []),
io:format("Starting... ~p~n", [Server]),
start(N-1, [Server | Servers]).
%% @doc Given a server pid() and a string sends a request to the server to
%% return an evaluated expression for a palindrome query.
-spec check(pid(), string()) -> {{atom(), string()}}.
check(Server, String) ->
Server ! {check, String, self()},
receive
Response -> Response
end.
%% @doc Given a server pid, a client pid and a number of requests, sends N
%% similar requests to the server pid.
send_multiple_requests(_ServerPid, _From, 0) ->
ok;
send_multiple_requests(ServerPid, From, N) ->
From ! check(ServerPid, "Madam Im Adam"),
send_multiple_requests(ServerPid, From, N-1).
%%%-------------------------------------------------------------------
%% @doc Palindrome Auxiliary functions
%% @end
%%%-------------------------------------------------------------------
%% @doc Given a string, returns a string telling whether it's a palindrome or not.
-spec is_palindrome(string()) -> string().
is_palindrome(String) ->
IsPalindrome = palin:palindrome(String),
case IsPalindrome of
true -> " is a palindrome.";
false -> " is not a palindrome."
end.
is_palindrome_test() ->
IsPalindrome = " is a palindrome.",
IsNotPalindrome = " is not a palindrome.",
?assertEqual(IsPalindrome, is_palindrome("Madam I'm Adam")),
?assertEqual(IsNotPalindrome, is_palindrome("Madam I'm Adams")).
@elbrujohalcon
Copy link

Nice code! You're only missing a good set of tests for your servers, proxy, etc… and maybe a couple of API functions to allow clients to call server:check(Server, ThisString) instead of writing the whole message sending and receiving logic there ;)

@sgobotta
Copy link
Author

Hey @elbrujohalcon! Nice to hear from you again!

Nice code! You're only missing a good set of tests for your servers, proxy, etc…

Yeah, I felt bad for not submitting those. Do you know if there's a unit framework with support for receive statements? Maybe I just can define a function that accepts a message and returns a value to implement assertions. What do you think?

and maybe a couple of API functions to allow clients to call server:check(Server, ThisString) instead of writing the whole message sending and receiving logic there ;)

Ohh, you mean defining separate functions to be called from each case in the receive cases?

receive
 {check, String, From} ->
    From ! server:check(Server, String)

@elbrujohalcon
Copy link

You don't need a framework… and hopefully this answers your other concerns, too…
Check what I did: https://gist.github.com/elbrujohalcon/8d3366fe1765c63a3e48d6d5589f1391#file-palin-multi-server-erl
I basically created an API (with functions like start, check, stop, etc…) so that I hide the fact that those things run in different processes. Then, I tested those API functions.

@sgobotta
Copy link
Author

You don't need a framework… and hopefully this answers your other concerns, too…
Check what I did: https://gist.github.com/elbrujohalcon/8d3366fe1765c63a3e48d6d5589f1391#file-palin-multi-server-erl
I basically created an API (with functions like start, check, stop, etc…) so that I hide the fact that those things run in different processes. Then, I tested those API functions.

Thank you! I've been looking at your examples and I'll definitely apply those strategies.

@sgobotta
Copy link
Author

@elbrujohalcon, I particularly liked the way you make use of lists compehension funcions. I had to take a look at the documentation. I've seen them before but had no idea what they were supposed to do, haa!

@sgobotta
Copy link
Author

Well, it took me a little while but I see now how you didn't have to use eunit but just did the assertion evaluation the {I, O} = ... expression. Nice one. It feels so much better using booleans as palindrome output instead of "it is a palin... it isnt't a p...", haha

@sgobotta
Copy link
Author

So, something got me thinking quite a bit. Trying to figure out how every process is receiving a response during the second iteration in your test: is it possible that it's done 'cause you're matching with a particular message in the mailbox? I mean when you do:

check_test_client(Input) ->
    receive
      {Input, Output} ->
          Output
end.

specifically matching the Input value.

@elbrujohalcon
Copy link

Yeah, exactly! I'm doing a selective receive there, matching only on messages that are 2-sized tuples starting with Input.

@elbrujohalcon
Copy link

Well, it took me a little while but I see now how you didn't have to use eunit but just did the assertion evaluation the {I, O} = ... expression. Nice one. It feels so much better using booleans as palindrome output instead of "it is a palin... it isnt't a p...", haha

Oh, yeah… I ignored the whole stringifying nonsense with the goal of highlighting the important pieces. 🙄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment