12

Say you have an Action in ASP.NET MVC in a multi-instance environment that looks something like this*:

public void AddLolCat(int userId)
{
    var user = _Db.Users.ById(userId);

    user.LolCats.Add( new LolCat() );

    user.LolCatCount = user.LolCats.Count();

    _Db.SaveChanges();
}

When a user repeatedly presses a button or refreshes, race conditions will occur, making it possible that LolCatCount is not similar to the amount of LolCats.

Question

What is the common way to fix these issues? You could fix it client side in JavaScript, but that might not always be possible. I.e. when something happens on a page refresh, or because someone is screwing around in Fiddler.

  • I guess you have to make some kind of a network based lock?
  • Do you really have to suffer the extra latency per call?
  • Can you tell an Action that it is only allowed to be executed once per User?
  • Is there any common pattern already in place that you can use? Like a Filter or attribute?
  • Do you return early, or do you really lock the process?
  • When you return early, is there an 'established' response / response code I should return?
  • When you use a lock, how do you prevent thread starvation with (semi) long running processes?

* just a stupid example shown for brevity. Real world examples are a lot more complicated.

Dirk Boer
  • 8,522
  • 13
  • 63
  • 111

5 Answers5

5

Answer 1: (The general approach)
If the data store supports transactions you could do the following:

using(var trans = new TransactionScope(.., ..Serializable..)) {
    var user = _Db.Users.ById(userId);
    user.LolCats.Add( new LolCat() );
    user.LolCatCount = user.LolCats.Count();
    _Db.SaveChanges();
    trans.Complete();
}

this will lock the user record in the database making other requests wait until the transaction has been committed.

Answer 2: (Only possible with single process)
Enabling sessions and using session will cause implicit locking between requests from the same user (session).

Session["TRIGGER_LOCKING"] = true;

Answer 3: (Example specific)
Deduce the number of LolCats from the collection instead of keeping track of it in a separate field and thus avoid inconsistency issues.

Answers to your specific questsions:

  • I guess you have to make some kind of a network based lock?
    yes, database locks are common

  • Do you really have to suffer the extra latency per call?
    say what?

  • Can you tell an Action that it is only allowed to be executed once per User
    You could implement an attribute that uses the implicit session locking or some custom variant of it but that won't work between processes.

  • Is there any common pattern already in place that you can use? Like a Filter or attribute?
    Common practice is to use locks in the database to solve the multi instance issue. No filter or attribute that I know of.

  • Do you return early, or do you really lock the process?
    Depends on your use case. Commonly you wait ("lock the process"). However if your database store supports the async/await pattern you would do something like

    var user = await _Db.Users.ByIdAsync(userId);
    

    this will free the thread to do other work while waiting for the lock.

  • When you return early, is there an 'established' response / response code I should return?
    I don't think so, pick something that fits your use case.

  • When you use a lock, how do you prevent thread starvation with (semi) long running processes?
    I guess you should consider using queues.

Henrik Cooke
  • 775
  • 5
  • 8
  • Thanks for your answer. Just: what makes `TransactionScope` lock the `User`? – Dirk Boer Sep 25 '14 at 09:43
  • TransactionScope makes the db queries run in a transaction and within a transaction all locks are kept until the transaction completes. You take locks by selecting, inserting etc. What type of locks are specified by the isolation level which in this case is Serializable and means that you take exclusive locks on the "data" you "touch". So when you select a user you take an exclusive lock on the user and any other transaction will need to wait for your transaction to complete. – Henrik Cooke Sep 25 '14 at 19:38
  • I'm trying to use your "answer 1" but when I launch multiple threads in the same millisecond to call the method I get "Transaction (Process ID 58) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction" – user441365 Jul 27 '17 at 10:37
2

By "multi-instance" you're obviously referring to a web farm or maybe a web garden situation where just using a mutex or monitor isn't going to be sufficient to serialize requests.

So... do you you have just one database on the back end? Why not just use a database transaction?

It sounds like you probably don't want to force serialized access to this one section of code for all user id's, right? You want to serialize requests per user id?

It seems to me that the right thinking about this is to serialize access to the source data, which is the LolCats records in the database.

I do like the idea of disabling the button or link in the browser for the duration of a request, to prevent the user from hammering away on the button over and over again before previous requests finish processing and return. That seems like an easy enough step with a lot of benefit.

But I doubt that is enough to guarantee the serialized access you want to enforce.

You could also implement shared session state and implement some kind of a lock on a session-based object, but it would probably need to be a collection (of user id's) in order to enforce the serializable-per-user paradigm.

I'd vote for using a database transaction.

Craig Tullis
  • 9,939
  • 2
  • 21
  • 21
1

I suggest, and personally use mutex on this case.

I have write here : Mutex release issues in ASP.NET C# code , a class that handle mutex but you can make your own.

So base on the class from this answer your code will be look like:

public void AddLolCat(int userId)
{
    // I add here some text in front of the number, because I see its an integer
    //  so its better to make it a little more complex to avoid conflicts
    var gl = new MyNamedLock("SiteName." + userId.ToString());
    try
    {
        //Enter lock
        if (gl.enterLockWithTimeout())
        {
            var user = _Db.Users.ById(userId);    
            user.LolCats.Add( new LolCat() );    
            user.LolCatCount = user.LolCats.Count();    
            _Db.SaveChanges();
        }
        else
        {
            // log the error
            throw new Exception("Failed to enter lock");
        }
    }
    finally
    {
        //Leave lock
        gl.leaveLock();
    }
}

Here the lock is base on the user, so different users will not block each other.

About Session Lock

If you use the asp.net session on your call then you may win a free lock "ticket" from the session. The session is lock each call until the page is return.

Read about that on this q/a:
Web app blocked while processing another web app on sharing same session
Does ASP.NET Web Forms prevent a double click submission?
jQuery Ajax calls to web service seem to be synchronous

Community
  • 1
  • 1
Aristos
  • 66,005
  • 16
  • 114
  • 150
  • The only problem with the upvote on this answer is the requirement that the lock work across multiple servers. Even a named mutex will only provide interprocess synchronization on one machine, not across a server farm. – Craig Tullis Sep 25 '14 at 23:09
  • @Craig Didn't realize that ask for server farm. There the lock must be done via a common resource that is stays in one of the servers, ether file, either using common database lock. I am not sure but you can also use mutex, and set as name a common/shared file. – Aristos Sep 26 '14 at 07:26
  • I figured as much, and was only pointing out that in the specific scenario the OP laid out, a named mutex alone would not be enough. You could create a server (WCF would make it fairly easy) and use a mutex (or a monitor) to synchronize access to the members of a collection of user id's in that server/service, but you might be hard-pressed to realize any gains over just using a database transaction. So I'd start with the database transaction, then escalate from there if it isn't working out. :-) – Craig Tullis Sep 26 '14 at 20:12
0

Well MVC is stateless meaning that you'll have to handle with yourself manually. From a purist perspective I would recommend preventing the multiple presses by using a client-side lock, although my preference is to disable the button and apply an appropriate CSSClass to demonstrate its disabled state. I guess my reasoning is we cannot fully determine the consumer of the action so while you provide the example of Fiddler, there is no way to truly determine whether multiple clicks are applicable or not.

However, if you wanted to pursue a server-side locking mechanism, this article provides an example storing the requester's information in the server-side cache and returns an appropriate response depending on the timeout / actions you would want to implement.

HTH

Community
  • 1
  • 1
SeanCocteau
  • 1,838
  • 19
  • 25
  • Thanks for your answer! About the server side locking though: the article that you point too doesn't seem to take into account a multi-instance environment. – Dirk Boer Sep 18 '14 at 10:16
  • Good point - I guess the client-side option still comes into play unless you're after locking mechanism across application instances. This means that the server-side mechanism article still provides a suitable mechanism, but you'll need an alternative to the server-cache. Azure Table storage / Database mechanism is one way - however, you can use the AppFabric cache that should run across your multi-instance application. Can't say I've ever used it, but here's a good starting place -> http://msdn.microsoft.com/en-us/library/ff383731(v=azure.10).aspx – SeanCocteau Sep 18 '14 at 13:32
0

One possible solution is to avoid the redundancy which can lead to inconsistent data. i.e. If LolCatCount can be determined at runtime, then determine it at runtime instead of persisting this redundant information.

Vikas Gupta
  • 4,455
  • 1
  • 20
  • 40