One of the more powerful capabilities of Erlang is the ability to take server processes and factor them into generic features and server-specific features. In the Open Telecom Platform (OTP), we put these generic features into a behavior module, and the server-specific features are stored into a callback module. This division of features allows us to build generic server frameworks with very useful capabilities, and it also reduces the effort of writing standalone servers into the simple task of implementing a handful of purely sequential functions.
This week we will update our RSS feed aggregator to use the Erlang/OTP gen_server behavior.
We will actually only update the rss_queue process to use the gen_server, since the only other process we have right now is the rss_reader, and it's a dedicated tool managed directly by the rss_queue. Make a copy of your work into a new ~/cs11/erlang/lab6 directory, and then you can begin updating your rss_queue process to work with gen_server.
Here is a genserver_template.erl template file, which contains all callbacks for the gen_server behavior. You will definitely want to use this template next week, but this week you can just cut pieces from it and put them into your rss_queue.erl file. Note that you can use the stubbed implementations for several of the callbacks. Although you won't provide a specialized implementation for every single one, you still need to provide some implementation of each one, so that the gen_server behavior doesn't try to call a nonexistent function and then crash on you. The implementations provided should be sufficient to keep gen_server happy.
So far your RSS queues handle a certain number of messages. Some of these messages require a synchronous response, and others do not. Here are the messages you should already handle so far, and whether they are synchronous or asynchronous:
You will need to update your queue to use handle_call/3 or handle_cast/2 to handle these incoming messages, depending on whether they are synchronous or not. However, you will have to make some changes:
Of course, your queue also receives process-exit notifications from other processes; if it is linked to an rss_reader process, as well as linking to (or monitoring) all subscribers. These messages are processed via the handle_info callback, so you will need to update this callback to properly handle process-exit signals. Don't forget that you need to set up process-exit signal trapping in your init/1 callback; it does not happen automatically!
Probably the biggest amount of work will be implementing a new init function. This function takes a single argument, which is a list of values to use in initializing server instances. Your queues have several ways of being initialized, but they must all work through this single init/1 function, with different sets of arguments.
You should already have an rss_queue:start/0 function that starts a queue in standalone mode, and an rss_queue:start/1 function that starts a queue with a URL as an argument. You will definitely keep these helper functions going forward, but you also need to update them since the gen_server module provides its own startup functions. So, you will have two main tasks for queue-startup this week:
Create an init/1 callback for the gen_server module to use, which supports both of these call patterns: one version of init will take an empty list, and start the queue in "standalone" mode, and the other version of init will take a URL, and start the queue in "reader" mode.
Of course, as mentioned in the previous section, your init/1 callback also needs to trap process-exit signals and stuff like that, so don't forget to do this initialization.
Update your start helper functions to call gen_server:start(ServerName, Module, Args, Options) instead of spawning the server process directly. Note that this version of gen_server:start takes a server-name, and registers the server under that name. Thus, you also need to update your rss_queue:start helpers to take a server-name (atom) to register each queue under. In other words, you will have rss_queue:start/1 and rss_queue:start/2, instead of start/0 and start/1.
Use {local, Name} for the server name, not the global version. We aren't quite that cool.
Once you have made these updates, you can see that the process will be slightly different for starting new RSS queues. First of all, the arguments are slightly different, and second, the return-value will be a little different as well. You might have an interaction like this:
1> {ok, QPid1} = rss_queue:start(cnn_top_stories, "http://rss.cnn.com/rss/cnn_topstories.rss"). {ok,<0.30.0>} 2> {ok, QPid2} = rss_queue:start(bbc_top_stories, "http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml"). {ok,<0.32.0>} 3> {ok, QPid3} = rss_queue:start(top_stories). {ok,<0.34.0>} 4> rss_queue:subscribe(top_stories, cnn_top_stories). ok 5> rss_queue:subscribe(QPid3, QPid2). % another way of subscribing ok
Of course, your rss_queue module doesn't have a subscribe function yet, so that will be the next task, implementing some helper functions.
Most gen_server callback modules provide a variety of helper functions to facilitate interacting with the server via simple callbacks. Although it is a little annoying to have to write a bunch of wrappers, it's also very helpful because it encapsulates the details of the message protocol so that callers don't need to care how the protocol is specified. This is actually a big benefit, and definitely outweighs the annoyance of having to write these functions the first time.
Besides that, gen_server provides several helper functions to make this much easier. There is gen_server:call for synchronous calls, and gen_server:cast for asynchronous calls. You will also notice from the API docs that these functions can handle several different ways of specifying a server. A PID can be specified, or a name (atom) corresponding to the server, or even a name corresponding to another node in an Erlang cluster. This gives us even more capability in writing our servers.
You should already have these helper functions:
Of all these helpers, only add_feed/2 doesn't correspond to a specific message. Update add_item/2 and get_all/1 to use the gen_server:call or gen_server:cast functions (remember, the {add_item, RSSItem} message is asynchronous, so you don't use call). Also, update all of these functions to take a PID or a name. This shouldn't be hard, since call and cast already know how to handle a PID or a name.
Once you have done this, you should also add these helper functions:rss_queue:subscribe(Queue1, Queue2)
This function causes Queue1 to become subscribed to Queue2. Since you are sending the message to Queue2, this can be a PID or name very easily; gen_server:call will handle this for us. However, your helper function should also support Queue1 being a PID or a name, so use the whereis/1 BIF to resolve a name to a PID, in the case that Queue1 is an atom and not a PID.
Obviously, if the whereis/1 call returns undefined, report an error with a line like this: exit({noproc, {?MODULE, subscribe, [Queue1]}}). This is similar to what the gen_server:call code does if you specify an invalid server name, so you might as well do the same thing.
rss_queue:unsubscribe(Queue)
For this function, assume that the caller (i.e. self()) is unsubscribing from Queue. This should be an easy helper to implement using gen_server:cast.
Once you have completed all of the above tasks, you should again try to construct a set of RSS queues like the configuration shown last week:
Write up the sequence of operations into a text file testing.txt, and submit this file along with your completed sources on csman.