This series is about a promotion game we built to support our iPhone apps.
You can play the game here – it is “early stage” but you can really win!
As I told before we used Silverlight to implement this game. And we do some UI localization (English, German) using Database Tables with “simple HTML”. I described this here.
I guess most of you know that Silverlight is a client technology. This (among others) means that we have no direct database access.
All we can do must be done with some kind of “Web Server Communication”. There are RIA Services (too enhanced for my simple project) and of course “plain” Windows Communication Foundation – or WCF Services.
That’s what I use. So I have a service which provides a list of “ManualPage entries”.
A manual page has: PageNumber, PageTopic, PageContent and “PageNumberText” (a formatted version of PageNumber).
The service method looks like this:
Snippet created with CBEnhancer [
OperationContract]
public List<
PromoManualPage> GetManual(
string strLangCode) {
return (
new PromoManual(strLangCode).PageList);
}
The constructor of PromoManual reads the pages from the database and provides it in it’s “PageList” memeber.
This looks good – BUT – there is a but.
Assume a lot of players are online. Every time a new player joins the game we create our instance of PromoManual, retrieve the manual from the database – send it to the client…
How often will this manual change? OK, while development often – you are right. But then? Almost never compared to the frequency of calls to this WCF method.
So it is a good idea to keep the things in memory somehow to prevent heavy (and useless) database access.
Static objects? No – we already have a cool thing for this – the cache!
Our call turns to this:
Snippet created with CBEnhancer [
OperationContract]
[
OperationBehavior(AutoDisposeParameters =
false)]
public List<
PromoManualPage> GetManual(
string strLangCode) {
return (GetCachedManual(strLangCode).PageList);
}
And the method we call looks like this:
Snippet created with CBEnhancer public static PromoManual GetCachedManual(
string strLanguageCode) {
//return (new PromoManual(strLanguageCode)); string strCacheName =
"PM_" + strLanguageCode;
Cache cH =
HttpRuntime.Cache;
PromoManual pM = cH[strCacheName]
as PromoManual;
if(pM ==
null) {
pM =
new PromoManual(strLanguageCode);
//reflect manual changes after 30 minutes - or simply free memory if the game "sleeps" cH.Insert(strCacheName, pM,
null,
DateTime.UtcNow.AddMinutes(30),
Cache.NoSlidingExpiration);
}
return (pM);
}
This means we search for PM_xx (xx==Language Code) in the cache. If we don’t find it we create such an object and place it in the cache. In this case I decided to keep the things 30 Minutes in Cache.
So I can update my manual pages (fix typos or whatever) – and after max. 30 minutes the players will see the new version. And if the game “sleeps” (no one plays) I’ll free the memory.
Have you seen the commented line? – That’s for text editing when I want to see changes immediately.
Someone may have noticed that I added an OperationBehaviour to my WCF Method.
The reason for this (I had to learn it the bitter way) – WCF call Dispose if your class implements IDisposable.
While this is a good idea if the object dies (fast resource release) it turn into an enemy when you want to keep your objects in cache.
Our Access to twitter is also cacheable. But it is handled a bit different.
Snippet created with CBEnhancer public static TwitterAccessor GetCachedTwitterAccessor() {
string strCacheName =
"TWA_Accessor";
Cache cH =
HttpRuntime.Cache;
TwitterAccessor twA = cH[strCacheName]
as TwitterAccessor;
if(twA ==
null) {
twA =
new TwitterAccessor();
cH.Insert(strCacheName, twA,
null,
Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(10));
}
return (twA);
}
The difference is the insert call. Here we don’t use a absolute expiration. Instead we use a sliding expiration.
So as long as someone (within 10 Minutes) calls the accessor from the cache this timer starts again.
There is no need for “absolute” expiration – like with the manual pages.
One last “mystery” about the twitter access. If I search for a new “promotion tweet” I do it like this:
Snippet created with CBEnhancer private static PromoSearchResult GetSearchResult(
string strPromoName,
string strEntryCode) {
PromoList pL = GetCachedPromoList(strPromoName);
if(!pL.DoesEntryExist(strEntryCode)) {
//we didn't find it TwitterAccessor twA = GetCachedTwitterAccessor();
DateTime dtLastTwitterSearch = twA.GetLastSearchForThisPromo(strPromoName);
//30 seconds would mean (if full running) 120 calls - 150 are available //50 means about 72 calls - thats fine for us - as long as we run not more than TWO promos a time (and fully loaded) if(dtLastTwitterSearch.AddSeconds(50) <
DateTime.Now) {
//if older than - try to find it ulong ulLastPost = pL.GetLastPromoID();
PromoInfo pI = GetCachedPromoInfo(strPromoName,
"en");
//always use english List<
PromoEntry> lPE = twA.SearchEntries(strPromoName, ulLastPost, pI.PromoTweetStartText);
if(lPE !=
null && lPE.Count > 0) {
//we found something pL.HandleTwitterData(lPE, twA);
}
}
}
return (
new PromoSearchResult(strPromoName, strEntryCode));
}
Notice that I avoid searches due to the “twitter call limit”. YES there are methods that allow me to check the current limit – and linq2twitter supports these methods. But I was lazy and avoided to implement it.
By the way it is not so easy as it looks (what if I have little calls in the beginning – and later massive access?) to implement a real good solution which brings the best (use almost all 150 calls) result.
Of course I could do this – but laziness is not the only reason to avoid it.
As a part of the game you should answer 3 questions – or in other words “learn more about the product”.
Who do you think would do this if he clicks a button – and at the very moment he gets a result?
By the way – this would never happen – twitter search also has some delay.
Anyhow – you do something in the game – I search for it – but I take time to do this – while you search for answers in the game. The balance is not to bad (boring) with less than a minute searches.
And if you follow the game (as I want you to do) – you initiate the call (tweet promotion) – look for the answers and normally I would have found your tweet before you could find the answers.
Two things also to consider – 50 seconds is “worst case” and even if you hit this – the “new PromoSearchResult” contains your result if the twitter search found it in the call 50 seconds before.
A last word about the Cache – always use it in scenarios like this! Online “things” (if successful) may have a lot of calls per second. And (in most cases) it makes no difference for the client if the data is 1 or 3 seconds old.
But it makes a lot of difference for your infrastructure (database and so on).
What I means is – even in situation where data is very volatile – it’s worth to cache for 2 seconds or even one if the load on your server is heavy.
In the next post I’ll handle the Silverlight parts of these WCF service.
Stay tuned
Manfred