45

I've given some thought to implementing badges (just like the badges here on Stack Overflow) and think it would be difficult without Windows services, but I'd like to avoid that if possible.

I came up with a plan to implement some examples:

  • Audobiographer: Check if all fields in the profile is filled out.
  • Commentor: When making a comment check if the number of comments equal 10, if so award the badge.
  • Good Answer: When voting up check to see if vote score is 25 or higher.

How could this be implemented in the database? Or would another way be better?

Luke101
  • 63,072
  • 85
  • 231
  • 359
  • 1
    You would want to cache some use reputation value on the web server without having to constantly call the windows services. – Russell Jul 01 '10 at 23:48

4 Answers4

41

A similar-to-Stackoverflow implementation is actually a lot simpler than you have described, based on bits of info dropped by the team every once in awhile.

In the database, you simply store a collection of BadgeID-UserID pairs to track who has what (and a count or a rowID to allow multiple awards for some badges).

In the application, there is a worker object for each badge type. The object is in cache, and when the cache expires, the worker runs its own logic for determining who should get the badge and making the updates, and then it re-inserts itself into the cache:

public abstract class BadgeJob
{
    protected BadgeJob()
    {
        //start cycling on initialization
        Insert();
    }

    //override to provide specific badge logic
    protected abstract void AwardBadges();

    //how long to wait between iterations
    protected abstract TimeSpan Interval { get; }

    private void Callback(string key, object value, CacheItemRemovedReason reason)
    {
        if (reason == CacheItemRemovedReason.Expired)
        {
            this.AwardBadges();
            this.Insert();
        }
    }

    private void Insert()
    {
        HttpRuntime.Cache.Add(this.GetType().ToString(),
            this,
            null,
            Cache.NoAbsoluteExpiration,
            this.Interval,
            CacheItemPriority.Normal,
            this.Callback);
    }
}

And a concrete implementation:

public class CommenterBadge : BadgeJob
{
    public CommenterBadge() : base() { }

    protected override void AwardBadges()
    {
        //select all users who have more than x comments 
        //and dont have the commenter badge
        //add badges
    }

    //run every 10 minutes
    protected override TimeSpan Interval
    {
        get { return new TimeSpan(0,10,0); }
    }
}
Rex M
  • 142,167
  • 33
  • 283
  • 313
  • wow. This is a good idea. How do you know when the cache expires. Is there like a call back function you can call when the cache expires? – Luke101 Jul 01 '10 at 23:58
  • 1
    @Luke101: it's a feature of the .NET caching infrastructure: when the timeout expires (and the object is about to removed from the cache) the runtime calls your callback automatically. – Dean Harding Jul 02 '10 at 00:23
  • This is quite cool, but isn't there a more declarative/explicit way of running periodic jobs in the ASP.NET process? Also, what thread does this run on when it fires? Is this well suited to long-running processes, such as calculating badges that span thousands of database entries? – Drew Noakes Aug 19 '10 at 00:07
  • @Drew ASP.NET is not suited to anything other than, well, serving web requests. That means high turnover for a large number of short-lived threads. This approach is *easy*, but that's all. For very different needs, we have totally different application models - such as Windows Services. To answer the threading question - it runs on a thread the ASP.NET runtime specifically spins up to manage cache. – Rex M Aug 19 '10 at 01:19
  • I think this is a pretty good approach. But I wonder about two things; 1) What happens when you get a very large number of badges (jobs). Would this not use up a lot of worker threads from asp.net? To me, a windows service would seem like a good candidate for this 2) Recalculating each time is good because it means if you change logic you can re-calc badges. But as you begin to get a lot of records, wouldn't this get slower and slower? – Joshua Hayes Aug 19 '10 at 14:05
  • @Joshua it only uses one thread - the cache expiration is managed by one thread. Yes - it does get slower and slower. In fact, Stackoverflow's badge jobs run much less frequently than they did shortly after launch, likely for the same reason. So you are right - in any formal setting, a Windows Service definitely would be more robust and architecturally sound. The point of this approach is that it is easy and keeps everything in the same component. – Rex M Aug 19 '10 at 14:20
  • @RexM: Where would you instantiate the CommenterBadge? I am having a tough time figuring out the best location. Ninject's InSingletonScope? Global.asax's Application_Start? – Shawn Feb 03 '12 at 16:35
  • @Shawn App Start is a very logical place to put that kind of initialization code. ASP.NET MVC uses this approach for registering routes, areas, etc. – Rex M Feb 06 '12 at 02:34
  • @RexM: Thanks. That's where I put it hoping that would be as good a place as any. I realize a windows service may better serve as the number of badges increase, but do you have any recommendations in regard to when its necessary to make that switch? After a certain number of badge objects, or is it all about processing/load time? – Shawn Feb 06 '12 at 15:23
  • @Shawn there's no way to say at the level we're discussing. Things to consider are system complexity (multiple applications making up a single system now have to communicate with each other); intra-application complexity (one app, but that app is becoming too complex to maintain); and any specific performance or scalability issues that you encounter that can be directly tied to the badge system. – Rex M Feb 06 '12 at 15:35
  • @RexM Do you happen to have any links to those "bits of info dropped by the team"? Thanks. – Guido Nov 16 '21 at 15:13
4

Jobs. That is the key. Out of process jobs that run at set intervals to check the criteria that you mention. I don't think you even need to have a windows service unless it requires some external resources to set the levels. I actually think StackOverflow uses jobs as well for their calculations.

spinon
  • 10,760
  • 5
  • 41
  • 59
  • 2
    I should also add that to avoid manually checking every user every time you would probably want to keep a table of activity so that you could have your list of people that you would want to check. You obviously don't want to waste resources processing users that haven't had any recent activity. – spinon Jul 01 '10 at 23:52
  • Do jobs run automatically? Where can I find more information on jobs? – Luke101 Jul 01 '10 at 23:55
  • Jobs can be scheduled to run on the database. You build them and then set their interval for which they should execute. You will notice on SO that once you complete an action it doesn't necessarily reward you right away. So I would imagine they have things set to run every few minutes or so. But I really have no idea. But a little delay is not a big deal but I would wait till the end of the day for processing. – spinon Jul 01 '10 at 23:58
  • 1
    This would work, but I would argue putting this out-of-process adds unnecessary maintenance overhead long-term. – Rex M Jul 02 '10 at 00:07
0

You could use triggers and check upon update or insert, then if your conditions are met add badge. That would handle it pretty seem less. Commence the trigger bashing in 3, 2, 1...

Dustin Laine
  • 37,935
  • 10
  • 86
  • 125
  • 6
    This is exactly the kind of (valid) reason triggers are bashed. – Rex M Jul 01 '10 at 23:53
  • I agree there are many disadvantages of triggers, but IMHO they mostly revolve around not including business logic in a data layer/database. Is there any performance or validity issues that arise from triggers, if they are written and implemented properly? – Dustin Laine Jul 02 '10 at 05:39
  • performance and validity issues tend to arise from triggers because they are, relatively speaking, very difficult to manage. They are side-effect-oriented, whereas in general it's commonly accepted that code with side effects is a recipe for disaster. It's hard to make a strong case for a single operation (e.g. insert) to silently have - from the caller's perspective - an unknown amount of additional side-effects to the system. – Rex M Jul 02 '10 at 14:03
  • @Rex M - Thanks for the explanation. I agree with you on this, also nice answer! – Dustin Laine Jul 02 '10 at 16:48
0

comments must be stored within the database right? then i think there are two main ways to do this.

1) when a user logs in you get a count of the comments. this is obvisously not the desired approach as the count could take a lot of time

2) when a user posts a comment you could either do a count then and store the count with the use details or you could do a trigger which executes when a comment is added. the trigger would then get the details of the newly created comment, grab the user id, get a count and store that against the user in a table of some sort.

i like the idea of a trigger as your program can return w/out waiting for sql server to do its stuff.

griegs
  • 22,624
  • 33
  • 128
  • 205