1

I inherited an app which has a service class which is called from asp.net application like this:

class PeopleService : IPeopleService
{
    //...
    public void SavePerson(Person person)
    {
        person.UniqueId = _idGenerationService.GetNextId(person);
        //...
        _dbContext.InsertOrUpdate(person);
        _dbContext.Commit();
    }
}

class IdGenerationService : IIdGenerationService
{
    //...
    public string GetNextId(Person person) 
    {
        int nextId = _dbContext.People.Count(x => x.Gender == person.Gender) + 1;
        return string.Format("AB{0}", nextId);
    }
}

The implementation of GetNextId(Person) is not thread safe, and we had a lot of Person objects with duplicated ids, like AB1, AB2, AB2, etc...

The solution was to apply lock to SavePerson(Person) like this:

class PeopleService : IPeopleService
{
    private static readonly Object ReadIdLock = new Object();
    //...
    public void SavePerson(Person person)
    {
        lock(ReadIdLock) 
        {
            person.UniqueId = _idGenerationService.GetNextId(person);
            //...
            _dbContext.InsertOrUpdate(person);
            _dbContext.Commit();
        }
    }
}

Now we tried to test the code by hitting the Save button simultaneously in asp.net app. But the fix didn't work! Interestingly, only the first records seem to be duplicates, like AB1, AB1, AB2, AB3...

How it's possible that multiple requests have accessed the locked statements? Should I use Mutex instead?

(Please note this example is not a production code, it's a simplified code to convey the problem that we are having. Also, small point, we are using StructureMap as DI).

Oluwafemi
  • 14,243
  • 11
  • 43
  • 59
mai
  • 464
  • 4
  • 23

1 Answers1

0

Since the lock is a static variable there's only one such lock object per appdomain which excludes a lot of possible bugs. Here's the only reason for this problem I can think of:

Your lock only works in one appdomain. ASP.NET can run apps in multiple app domains at the same time for example when recycling, deploying or in web garden mode. Also, you might have multiple servers.

Best fix is to make _idGenerationService.GetNextId thread-safe. Probably, you need to apply proper locking to the database query that underlies this method.

Also, you lock region is too long. You also cover the insert into the database. This starves concurrency and can cause distributed deadlocks.

usr
  • 168,620
  • 35
  • 240
  • 369
  • There's a lot I learned from your answer, just a quick comment. "Best fix is to make `_idGenerationService.GetNextId` thread-safe". But that method is just reading from the database so it won't return a different value until `InsertOrUpdate()` is called later. Hence I locked the whole method. – mai Aug 26 '15 at 11:55
  • 1
    I see. Then maybe you should move the ID generation into the transaction that acts on that ID. That's probably what is intended from a business perspective. – usr Aug 26 '15 at 12:14