Managing social data in Silverlight: do you push or pull?
Share
Here’s a problem you want to have: many concurrent connections to your web application, all vying for your precious data. Before you enjoy the challenges of that problem, you should consider whether your application works best in a push or pull scenario. In summary, pull applications are the most common; any time your client makes a service call to your application or another, expecting relevant data in the response, you are “pulling” data from a source and into your application. This is a fine way to go, as you are only keeping a thread in use long enough to retrieve your data or an error, and you’re off to greater things.
And then things go south
But what if your application is social, and the underlying data changes frequently? Let’s use Twitter as the typical example. Caching restrictions aside, how often do you want each and every one of your users to hit your server asking for the latest tweets? The numbers add up quickly if you have a popular application, and bandwidth is still a factor, even if you’re holding on to the actual data from Twitter for dear life.
Pushing through the dip
Have you heard of gnip? It is a freemium service that solves this problem for popular applications: rather than having clients continously pull data from a server, gnip lets your applications know when there is fresh data available to be had on social sites, so you only use exactly the amount of bandwidth you need to update your data. Even with a server set up to intelligently update its back-end social stores, you may also want to consider having a similar set up for your Silverlight (and any WCF-supported client) applications: your clients are “pushed” data when its new and relevant, with no polling required. Of course there are performance considerations to consider in this scenario as well, since your server will need to track “who” is listening for messages, but this may work well for the application you’re building, as it’s working well for mine.
Building a push client
A push client is made possible through WCF’s duplex polling feature. Most of the plumbing for this example comes directly from MSDN’s article, so if you care deeply about the implementation details then I suggest you start here first. A duplex service defines two contracts, the one for messages your client sends to the server to let it know its alive, and the one your server “calls back” to the client; the latter contains all of the messages we can push to our Silverlight applications.
Let’s build a Twitter client that receives fresh tweet statuses only when they are available, rather than asking for new statuses from the server at regular intervals, like we might conceive of in a “pull”-based application. We’ll use tweet# to handle the communication between Twitter and the server.
// These are the messages we can send from
/// the client to the server
[ServiceContract(Namespace = "Push.Server",
CallbackContract = typeof(ITwitterClient))]
public interface ITwitterService
{
[OperationContract(IsOneWay = true)]
void SignIn(Message receivedMessage);
[OperationContract(IsOneWay = true)]
void SignOut(Message receivedMessage);
}
// These are our "push" messages from
// the server back to the client
[ServiceContract]
public interface ITwitterClient
{
[OperationContract(IsOneWay = true)]
void ReceivedStatuses(Message returnMessage);
[OperationContract(IsOneWay = true)]
void SignedIn(Message returnMessage);
[OperationContract(IsOneWay = true)]
void SignedOut(Message returnMessage);
}
Notice we’re defining the callback contract for the main service, and all messages are one way. We definitely don’t want to block any threads as messages are sent and received, and we need to distinguish between what service operation is doing the pushing (in this case, signing in, signing out, and receiving the latest tweets) so our client can respond appropriately.
When a user signs in, we want to store a reference to the connecting client, and push data updates to them when relevant. A Twitter application likely has a central store of “tweets”, that is the same for each client; when new data is available, every client that is signed in will receive the same information. In the implementation of our service we’ll spin off a new thread that polls Twitter in the background looking for a response that differs from our last poll. We’ll cache this as a static variable containing the most recent update.
static TwitterService()
{
StartStatusesQuery();
}
private static void StartStatusesQuery()
{
// Build a request to get the application's followers
var request = FluentTwitter.CreateRequest()
.Statuses().OnPublicTimeline().AsJson()
.Configuration.CacheUntil(6.Seconds().FromNow())
.CallbackTo(ProcessStatusesResponse);
// Re-query every five seconds
new Timer(callback =>
{
_statusesQuery = request.Root;
_statusesQuery.RequestAsync();
}, null, 0, 5000);
}
In the query callback, we’ll check to see if the JSON reply from Twitter is different than the previous result. If it is, we’ll call up all of the signed in clients and let them know.
private static void ProcessStatusesResponse
(object sender, WebQueryResponseEventArgs e)
{
var json = e.Response;
if (json == null)
{
return;
}
try
{
var same = _statusesResult != null ?
_statusesResult.Equals(json) :
false;
if (String.IsNullOrEmpty(_statusesResult) || !same)
{
_statusesResult = json;
var message = string.Format("{0}#{1}",
STATUSES_ACTION,
_statusesResult);
CallClients(message, STATUSES_ACTION, (c, m) => c.ReceivedStatuses(m));
}
}
catch (TimeoutException)
{
//
}
catch (CommunicationException)
{
//
}
}
Now on the client we can demonstrate how to manage an open channel back to the server and work with the messages that we receive.
public Page()
{
InitializeComponent();
_ui = SynchronizationContext.Current;
// Create a new manager to receive data pushes from the server
_manager = new ChannelManager(_ui);
ChannelManager.ResponseReceived += manager_ResponseReceived;
StartTweetTicker();
}
private void StartTweetTicker()
{
var timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 2)};
timer.Tick += ((sender, e) =>
{
if (_queue.Count == 0)
{
return;
}
var status = _queue.Dequeue();
_statuses.Add(status);
var tweet = new TextBlock
{
Text = String.Format("{0}: {1}", status.User.ScreenName, status.Text),
Width = 500,
Height = 20
};
// This came from a different thread
_ui.Post(s => waterfall.Children.Add(tweet), null);
_ui.Post(s => UpdateQueue(), null);
});
timer.Start();
}
In the page constructor above, we’re creating a new channel manager, a convenience class that wraps up the duplex polling logic, and passing it a handler for when a new server-side message is received. Then we make a call to spin up a new timer thread that “peels” from our batch of tweets and displays them on-screen.
void manager_ResponseReceived(object sender, DuplexResponseEventArgs e)
{
switch (e.Action)
{
case ChannelManager.STATUSES_ACTION:
{
var statuses = e.Response.AsStatuses();
var fresh = statuses.Where(s => !_queue.Contains(s) && !_statuses.Contains(s));
foreach (var status in fresh)
{
_queue.Enqueue(status);
}
// This came from a different thread
_ui.Post(s => UpdateQueue(), null);
break;
}
case ChannelManager.SIGNED_IN_ACTION:
break;
case ChannelManager.SIGNED_OUT_ACTION:
break;
default:
throw new NotSupportedException("Unknown callback contract operation");
}
}
Above, we’re handling the response to each unique message action, whether it’s confirming sign-in and sign-out, or receiving new tweets. In the latter case, we make sure we aren’t looking at any duplicate tweets from those we’ve received before, before queueing them up for display.
I won’t dwell on the fine details of the channel management code itself; for that you can refer to the MSDN article mentioned earlier. I have prepared a reference application you can use to see the duplex behavior in action. I like how twitterfall.com queues and then “peels” tweets off the queue for display; the reference application shows this type of behavior in action.
It’s probably true that for many situations a pull-based architecture is appropriate. How well do you think push-based services scale? You can use this example as an exploration for your own applications.
Socialized