0

In my application I have a BackgroundService which also includes the following method:

  • GetNumberAsync() gets the "oldest" record from database and updates the properties of the record.

This method is called from API controller async method like:

[HttpGet]
[Route("GetNumber")]
public async Task<ActionResult<string>> GetNumber()
{
    return Ok(await _numberService.GetNumberAsync());
}

I'm afraid that if two or more calls of GetNumberAsync() at the same time occurs then some of them may get the same record. How to avoid it, because every call should return a unique record?

Method code:

    internal async Task<string> GetNumberAsync()
    {
        var num = await _smsDbContext.ZadarmaPhoneNumbers
            .OrderBy(x => x.LockedTime)
            .FirstOrDefaultAsync();

        if(num != null)
        {
            num.LockedTime = DateTime.Now;
            num.SmsCode = "";

            _smsDbContext.ZadarmaPhoneNumbers.Update(num);

            await _smsDbContext.SaveChangesAsync();

            return num.Number;
        }

        return "";
    }

Thanks.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Sergey
  • 11
  • 4
  • You could add a lock, or use one of the existing synchronization classes like [SemaphoreSlim](https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=netframework-4.8) or [Monitor](https://learn.microsoft.com/en-us/dotnet/api/system.threading.monitor?view=netframework-4.8) – Chronicle Feb 29 '20 at 21:42
  • 2
    @Chronicle: such synchronization mechanisms won't work if two or more servers in a farm handle requests. – Wiktor Zychla Feb 29 '20 at 21:48
  • @user3547759: I would try to SELECT with TABLOCKX, wrapped in a TRANSACTION. – Wiktor Zychla Feb 29 '20 at 21:58
  • @WiktorZychla like this [https://stackoverflow.com/a/22721849/3547759] ? – Sergey Feb 29 '20 at 22:13
  • @user3547759, locking the entire table is not necessary though it solve the problem and it is bad in general from the performance point of view. There're better solutions by using a proper sql query but to suggest them you need to share what you want to archive from the business function view. – Sergey L Feb 29 '20 at 22:18
  • @SergeyL the business need: there is a pool of phone numbers in and users of the API should get the one number per user to send SMS there. In other words, every user should receive a unique phone number. I've simplified the code a bit in my question by excluding the situation when no numbers available ar the moment. – Sergey Feb 29 '20 at 22:33
  • @user3547759 What happens when sms is sent? Will this number returned into the pool of numbers? If it is returned to the pool it can be reused and thus it will not be unique. – Sergey L Feb 29 '20 at 22:49
  • @SergeyL you are right. after we read an SMS or waiting time of SMS is over we return number in the pool. thus, at every moment we know the user who received the number. – Sergey Feb 29 '20 at 22:55
  • Based on earlier comments, will this be running in a multi-server environment? Or will there be only one web server accessing the database? If it is a simple, single server environment then you could register NumberServer as a singleton and use the SemaphoreSlim or Monitor mentioned earlier. If it is in a multi-server environment then you will probably need to use SQL locking to handle the concurrency. – Simply Ged Feb 29 '20 at 23:04

1 Answers1

0

Here's the idea how you can implement it.

Assuming you have phones table like this. You might add LockedTime to add a time criteria.

create table phones (
  used bit default 0 not null,
  phone varchar(24) not null);

Execute this query to extract just one phone and it will not be returned twice as it marked as used.

update top(1) phones
set used = 1
output inserted.phone
where used = 0
;

Note that it works on MS SQL server. Some other SQL servers might not have output clause.

If you want to return a number to the free number pool you just need to clear the used flag. For example, you may do it checking the time criteria or any other deciding that a phone becomes free

update phones set used = 0 where LockedTime < @somedatetime

Sergey L
  • 1,402
  • 1
  • 9
  • 11