Question

How would you solve the concurrency issue with the following code? In this example, we'd like to know why the user failed authentication. The problem is that this code makes two separate calls to the database, but we'd like the whole method to happen inside of a conceptual transaction. Specifically we're interested in isolation. We don't want concurrent writes during the execution of this method to affect our reads before we've determined the reason for the authentication failure.

A few solutions come to mind: Thread Locking, TransactionScope and Optimistic Locking. I really like the idea of Optimistic Locking, since I think conflicts are likely to be rare, but there's nothing built into .NET to do this, right?

Also - is this something to REALLY be concerned about in this case? When are concurrency issues like this important to consider and when aren't they? What needs to be considered in implementing a solution? Performance? The duration of the lock? How likely conflicts are to occur?

Edit: After reviewing Aristos' answer, I think what I'm really after is some sort of "snapshot" isolation level for the Authenticate method.

public MembershipStatus Authenticate(string username, string password)
    {
        MembershipUser user = Membership.GetUser(username);
        if (user == null)
        {
            // user did not exist as of Membership.GetUser
            return MembershipStatus.InvalidUsername;
        }

        if (user.IsLockedOut)
        {
            // user was locked out as of Membership.GetUser
            return MembershipStatus.AccountLockedOut;
        }

        if (Membership.ValidateUser(username, password))
        {
            // user was valid as of Membership.ValidateUser
            return MembershipStatus.Valid;
        }

        // user was not valid as of Membership.ValidateUser BUT we don't really
        // know why because we don't have ISOLATION.  The user's status may have changed
        // between the call to Membership.GetUser and Membership.ValidateUser.
        return MembershipStatus.InvalidPassword;
    }
Était-ce utile?

La solution

Based on my reading here and here, it seems like a System.Transactions.TransactionScope wrapping your whole method should automatically enlist your database calls in a common transaction, resulting in transactional safety across the whole Transaction scope.

You'd want to do something like this:

public MembershipStatus Authenticate(string username, string password)
{
    using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.Snapshot }))
    {
    MembershipUser user = Membership.GetUser(username);
        if (user == null)
        {
            // user did not exist as of Membership.GetUser
            return MembershipStatus.InvalidUsername;
        }

        if (user.IsLockedOut)
        {
            // user was locked out as of Membership.GetUser
            return MembershipStatus.AccountLockedOut;
        }

        if (Membership.ValidateUser(username, password))
        {
            // user was valid as of Membership.ValidateUser
            return MembershipStatus.Valid;
        }

        // user was not valid as of Membership.ValidateUser BUT we don't really
        // know why because we don't have ISOLATION.  The user's status may have changed
        // between the call to Membership.GetUser and Membership.ValidateUser.
        return MembershipStatus.InvalidPassword;
    }
}

Autres conseils

I will use mutex using the name as locking parametre, so only the same user may can lock out for awhile. This for me is more safe for one computer because with mutex I can capture all possible threads from different pools, or web calls.

public MembershipStatus AuthenticateLock(string username, string password)
{
    if(string.IsNullOrEmpty(username))
      return MembershipStatus.InvalidUsername;

    // TODO: Here you must check and clear for non valid characters on mutex name
    using (var mutex = new Mutex (false, username))
    {
         // possible lock and wait, more than 16 seconds and the user can go...
         mutex.WaitOne (TimeSpan.FromSeconds(16), false);

         // here I call your function anyway ! and what ever done...
         //  at least I get a result
         return Authenticate(username, password)
    }
}

More comments: Both Membership.ValidateUser and Membership.GetUser make call to the database.

But if you use the standard asp.net session for the pages that make this calls and affect this parameters, then the page all ready lock the one the other so I think that there is no chance to need this mutex call. Because the lock of the session is enough to synchronize and this part. I remind that the session is locking the page from the start to the end for all users.

About session lock: Replacing ASP.Net's session entirely

jQuery Ajax calls to web service seem to be synchronous

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top