28 Apr 09

A User Timeline: Publishing events for retrieval by DekiScript

As promised previously, this article will go over building an extension to MindTouch using the Event Bus. This article is primarily an overview; for the detailed walkthrough of the code check out the tutorial. The service described is a sample showing how the Event Bus could be used in a real world scenario, however while the code dealing the the Event Bus is production ready code, there a number of additional features the timeline should have to real use. I will cover the possible improvements to the sample that would make the service a production feature.

What is a User Timeline
The purpose of this service is to capture all comments made by a user or about a user as a timeline of their activity. Borrowing some twitterism, we can use the convention of a user name preceded with an @ to let our users easily mention other users in their comments. And since comments are always associated with a page, the timeline automatically captures the context in which the user’s comments were made. An example of this would be telling another user about a page in the Wiki, by simply saying “Hey @bob, check out this page” in the comments of that page.

The service should provide the following functionality:

  • Retrieve the timeline by username
  • Track @replies to the user in their timeline
  • Provide DekiScript for accessing the timeline with max entries and start offset

Implementation Approach 1: Directly integrate into the existing code

One way that this service could be built is by modifying the main MindTouch service itself, creating a new set of database calls that scour the database for all comments eligible for the timeline, and adding new API calls to expose this data. This approach has multiple drawbacks:

  • Having to be familiar with the internal workings of MindTouch and its Database schema
  • Introducing a maintenance burden to merge code changes when the next release comes out
  • If used heavily, the timeline DB queries will become too expensive

Implementation Approach 2: Feed off the Event Bus

Fortunately, all comment creation, editing and deletion generates events that we can subscribe to, allowing us to receive the data in real time and store it ourselves for efficient, per user retrieval.

This article gives you an overview of a service for the event driven approach, for more detail read the “Creating a Comment Timeline using the Deki Event Bus” tutorial. The service described has the following responsibilities:

  • Set up and tear down subscriptions on Start/Stop
  • Capture events, process them for @replies and retain them as per user timelines
  • Expose a DekiScript function to retrieve a user’s timeline

The channels we are interested in are event://{wikiid}/deki/comments/create and event://{wikiid}/deki/comments/update. A subscription endpoint is any DreamService feature that accepts a POST. The subscription document that we submit in the service’s Start() coroutine looks like this (using the XDoc class):

XDoc subscription = new XDoc("subscription-set")
  .Elem("uri.owner", Self.Uri.AsServerUri().ToString())
  .Start("subscription")
    .Elem("channel", string.Format("event://{0}/deki/comments/create", _wikiId))
    .Elem("channel", string.Format("event://{0}/deki/comments/update", _wikiId))
    .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri)
                    .AsSetCookieDocument)
    .Start("recipient")
      .Attr("authtoken", _apikey)
      .Elem("uri", Self.Uri.AsServerUri().At("notify").ToString())
    .End()
  .End();

With the subscription set up, the service will receive all comment events, leaving us to simply process the incoming data and store it. The format for a comment event looks like this (a complete list ofevents published by Deki can be found here):

<deki-event wikiid="..." event-time="...">
  <channel>...</channel>
  <uri>...</uri>
  <pageid>...</pageid>
  <uri.page>...</uri.page>
  <user id="..." anonymous="...">
    <uri>...</uri>
  </user>
  <path>...</path>
  <!-- content/@type and value are only included if smaller than 255 chars -->
  <content uri="..." type="...">...</content>
</deki-event>

Since we want to expose the users (sender and recipient in case of @reply) and page related to a comment, we need to call back into the Deki API to retrieve the appropriate API documents in order to create the timeline event. For the sender and the page, we are provided uri’s that we can query. However, for @reply users, we only have the username, so we have to construct a Uri into the Deki API to query by username. All of these requests are done using the Plug class and require the apikey, in case any of the resources we are trying to access are restricted.

For simplicity the user retrieval is factored into a separate coroutine GetUserInfo, which can retrieve the user document based on either Uri or username. Using a coroutine (i.e. invoking it via Coroutine.Invoke() instead of straight method invocation) allows us to use yield to asychronously request the user data rather than blocking the current thread, as regular methods have to. For more information on Coroutines and the Yield pattern check out my previous article “Dream Asynchronicity Coroutines“.

Finally we expose a DekiScript function for retrieving the timeline:

messagestream.timeline(username : str)

The function requires a username. It returns a list of timeline event maps. We chose DekiScript data as the return format instead of formatted html, because it gives us a lot more flexibility in using the timeline. Instead of having to change the service to filter the list, or change the Html generated, we can use DekiScript templates to keep the logic editable directly via the Wiki interface.

Improving the Timeline

As stated at the beginning, the subscription and event processing of this sample is fully implemented, but the service lacks a number of additional features to make it useful in production. The main areas that need improvement are persistence and performance.

The sample punts on persistence and keeps the timeline in memory, which is both undesirable for scalability and does not survive a service restart. Instead of keeping the events in a dictionary, the service should serialize each event to disk or into a database and retrieve the events when messagestream.timeline is called.

From a performance perspective, the repeated calls back to the Deki API and the requirement for the user of the DekiScript function to fetch the user and page data would likely create a lot of undesirable work for your Wiki. Both could easily be addressed by fetching the data on demand and keeping the data in a memory cache for a short time period, so that repeated access to the same timeline would not require rebuilding all the information every time.

While the sample shouldn’t be put into production, we hope that the ideas and  code provided will serve as a good base for building Event Bus driven extensions.

Leave a Reply

Copyright © 2011 MindTouch, Inc. Powered by