-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver_centralized.erl
252 lines (213 loc) · 9.2 KB
/
server_centralized.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
%% This is a simple implementation of the project, using one server process.
%%
%% It will create one "server" actor that contains all internal state (users,
%% their subscriptions, and their messages).
%%
%% This implementation is provided with unit tests, however, these tests are
%% neither complete nor implementation independent, so be careful when reusing
%% them.
-module(server_centralized).
-include_lib("eunit/include/eunit.hrl").
-export([initialize/0, initialize_with/1, server_actor/1, typical_session_1/1,
typical_session_2/1]).
%%
%% Additional API Functions
%%
% Start server.
initialize() ->
initialize_with(dict:new()).
% Start server with an initial state.
% Useful for benchmarking.
initialize_with(Users) ->
ServerPid = spawn_link(?MODULE, server_actor, [Users]),
catch unregister(server_actor),
register(server_actor, ServerPid),
ServerPid.
% The server actor works like a small database and encapsulates all state of
% this simple implementation.
%
% Users is a dictionary of user names to tuples of the form:
% {user, Name, Subscriptions, Messages}
% where Subscriptions is a set of usernames that the user follows, and
% Messages is a list of messages, of the form:
% {message, UserName, MessageText, SendTime}
server_actor(Users) ->
receive
{Sender, register_user, UserName} ->
NewUsers = dict:store(UserName, create_user(UserName), Users),
Sender ! {self(), user_registered},
server_actor(NewUsers);
{Sender, log_in, _UserName} ->
% This doesn't do anything, but you could use this operation if needed.
Sender ! {self(), logged_in},
server_actor(Users);
{Sender, follow, UserName, UserNameToFollow} ->
NewUsers = follow(Users, UserName, UserNameToFollow),
Sender ! {self(), followed},
server_actor(NewUsers);
{Sender, send_message, UserName, MessageText, Timestamp} ->
NewUsers = store_message(Users, {message, UserName, MessageText, Timestamp}),
Sender ! {self(), message_sent},
server_actor(NewUsers);
{Sender, get_timeline, UserName} ->
Sender ! {self(), timeline, UserName, timeline(Users, UserName)},
server_actor(Users);
{Sender, get_profile, UserName} ->
Sender ! {self(), profile, UserName, sort_messages(get_messages(Users, UserName))},
server_actor(Users)
end.
%%
%% Internal Functions
%%
% Create a new user with `UserName`.
create_user(UserName) ->
{user, UserName, sets:new(), []}.
% Get user with `UserName` in `Users`.
% Throws an exception if user does not exist (to help in debugging).
% In your project, you do not need specific error handling for users that do not exist;
% you can assume that all users that use the system exist.
get_user(UserName, Users) ->
case dict:find(UserName, Users) of
{ok, User} -> User;
error -> throw({user_not_found, UserName})
end.
% Update `Users` so `UserName` follows `UserNameToFollow`.
follow(Users, UserName, UserNameToFollow) ->
{user, Name, Subscriptions, Messages} = get_user(UserName, Users),
NewUser = {user, Name, sets:add_element(UserNameToFollow, Subscriptions), Messages},
dict:store(UserName, NewUser, Users).
% Modify `Users` to store `Message`.
store_message(Users, Message) ->
{message, UserName, _MessageText, _Timestamp} = Message,
{user, Name, Subscriptions, Messages} = get_user(UserName, Users),
NewUser = {user, Name, Subscriptions, Messages ++ [Message]},
dict:store(UserName, NewUser, Users).
% Get all messages by `UserName`.
get_messages(Users, UserName) ->
{user, _, _, Messages} = get_user(UserName, Users),
Messages.
% Generate timeline for `UserName`.
timeline(Users, UserName) ->
{user, _, Subscriptions, _} = get_user(UserName, Users),
UnsortedMessagesForTimeLine =
lists:foldl(fun(FollowedUserName, AllMessages) ->
AllMessages ++ get_messages(Users, FollowedUserName)
end,
[],
sets:to_list(Subscriptions)),
sort_messages(UnsortedMessagesForTimeLine).
% Sort `Messages` from most recent to oldest.
sort_messages(Messages) ->
% Sort on the 4th element of the message tuple (= timestamp, this uses 1-based
% indexing), and then reverse to put most recent first.
lists:reverse(lists:keysort(4, Messages)).
%%
%% Tests
%%
% These tests are for this specific implementation. They are a partial
% definition of the semantics of the provided interface but also make certain
% assumptions of the implementation. You can re-use them, but you might need to
% modify them.
% Test initialize function.
initialize_test() ->
catch unregister(server_actor),
initialize().
% Initialize server and test user registration of 4 users.
% Returns list of user names to be used in subsequent tests.
register_user_test() ->
initialize_test(),
?assertMatch({_, user_registered}, server:register_user(server_actor, "A")),
?assertMatch({_, user_registered}, server:register_user(server_actor, "B")),
?assertMatch({_, user_registered}, server:register_user(server_actor, "C")),
?assertMatch({_, user_registered}, server:register_user(server_actor, "D")),
["A", "B", "C", "D"].
% Test log in.
log_in_test() ->
[UserName1, UserName2 | _] = register_user_test(),
?assertMatch({_Server1, logged_in}, server:log_in(server_actor, UserName1)),
?assertMatch({_Server2, logged_in}, server:log_in(server_actor, UserName2)).
% Note: returned pids _Server1 and _Server2 do not necessarily need to be
% the same.
% Test follow: user 1 will follow 2 and 3.
follow_test() ->
[UserName1, UserName2, UserName3 | _ ] = register_user_test(),
{Server1, logged_in} = server:log_in(server_actor, UserName1),
?assertMatch(followed, server:follow(Server1, UserName1, UserName2)),
?assertMatch(followed, server:follow(Server1, UserName1, UserName3)),
{UserName1, Server1, [UserName2, UserName3]}.
% Test sending a message.
send_message_test() ->
{UserName1, Server1, Subscriptions} = follow_test(),
?assertMatch(message_sent,
server:send_message(Server1, UserName1, "Hello!")),
?assertMatch(message_sent,
server:send_message(Server1, UserName1, "How is everyone?")),
{UserName1, Server1, Subscriptions}.
% Test getting a timeline.
get_timeline_test() ->
{UserName1, Server1, [UserName2, UserName3]} = follow_test(),
% When nothing has been sent, the timeline is empty.
?assertMatch([], server:get_timeline(Server1, UserName1)),
?assertMatch(message_sent,
server:send_message(Server1, UserName2, "Hello I'm B!")),
% One message in the timeline.
?assertMatch([
{message, UserName2, "Hello I'm B!", _TimeB1}
], server:get_timeline(Server1, UserName1)),
?assertMatch(message_sent,
server:send_message(Server1, UserName2, "How is everyone?")),
?assertMatch(message_sent,
server:send_message(Server1, UserName3, "Hello I'm C!")),
% All three messages in the timeline, newest first.
?assertMatch([
{message, UserName3, "Hello I'm C!", _TimeC1},
{message, UserName2, "How is everyone?", _TimeB2},
{message, UserName2, "Hello I'm B!", _TimeB1}
], server:get_timeline(Server1, UserName1)),
% User 2 does not follow any so gets an empty timeline.
?assertMatch([], server:get_timeline(Server1, UserName2)).
% Test getting the profile.
get_profile_test() ->
{UserName1, Server1, [UserName2 | _]} = send_message_test(),
% Most recent message is returned first.
?assertMatch([
{message, UserName1, "How is everyone?", _TimeA2},
{message, UserName1, "Hello!", _TimeA1}
], server:get_profile(Server1, UserName1)),
% User 2 hasn't sent any messages.
?assertMatch([], server:get_profile(Server1, UserName2)).
% A "typical" session.
typical_session_test() ->
initialize_test(),
Session1 = spawn_link(?MODULE, typical_session_1, [self()]),
Session2 = spawn_link(?MODULE, typical_session_2, [self()]),
receive
{Session1, ok} ->
receive
{Session2, ok} ->
done
end
end.
typical_session_1(TesterPid) ->
{_, user_registered} = server:register_user(server_actor, "Alice"),
{Server, logged_in} = server:log_in(server_actor, "Alice"),
message_sent = server:send_message(Server, "Alice", "Hello!"),
message_sent = server:send_message(Server, "Alice", "How is everyone?"),
% Check own profile
[{message, "Alice", "How is everyone?", Time2},
{message, "Alice", "Hello!", Time1}] =
server:get_profile(Server, "Alice"),
?assert(Time1 =< Time2),
TesterPid ! {self(), ok}.
typical_session_2(TesterPid) ->
{_, user_registered} = server:register_user(server_actor, "Bob"),
{Server, logged_in} = server:log_in(server_actor, "Bob"),
% Sleep one second, while Alice sends messages.
timer:sleep(1000),
[] = server:get_timeline(Server, "Bob"),
followed = server:follow(Server, "Bob", "Alice"),
[{message, "Alice", "How is everyone?", Time2},
{message, "Alice", "Hello!", Time1}] =
server:get_timeline(Server, "Bob"),
?assert(Time1 =< Time2),
TesterPid ! {self(), ok}.