Search code examples
sql-servert-sqlinfinite-loop

Stored procedure to constantly check table for records and process them


We have a process that is causing dirty read errors. So, I was thinking of redesigning it as a queue with a single process to go through the queue.

My idea was to create a table that various processes could insert into. Then, one process actually processes the records in the table. But, we need real-time results from the processing method. So, while I was thinking of scheduling it to run every couple of seconds, that may not be fast enough. I don't want a user waiting several seconds for a result.

Essentially, I was thinking of using an infinite loop so that one stored procedure is constantly running, and that stored procedure creates transactions to perform updates.

It could be something like:

WHILE 1=1
BEGIN
    --Check for new records
    IF NewRecordsExist
    BEGIN
        --Mark new records as "in process"
        BEGIN TRANSACTION

            --Process records
            --If errors, Rollback
            --Otherwise Commit transaction
    END
END

But, I don't want SQL Server to get overburdened by this one method. Basically, I want this running in the background all the time, not eating up processor power. Unless there is a lot for it to do. Then, I want it doing its work. Is there a better design pattern for this? Are stored procedures with infinite loops thread-safe? I use this pattern all the time in Windows Processes, but this particular task is not appropriate for a Windows Process.

Update: We have transaction locking set. That is not the issue. We are trying to set aside items that are reserved for orders. So, we have a stored procedure start a transaction, check what is available, and then update the table to mark what is reserved.

The problem is that when two different users attempt to reserve the same product at the same time, the first process checks availability, finds product available, then start to reserve it. But, the second process cannot see what the first process is doing (we have transaction locking set), so it has no idea that another process is trying to reserve the items. It sees the items as still available and also goes to reserve them.

We considered application locking, but we are worried about waits, delays, etc. So, another solution we came up with is one process that handles reservations in a queue. It is first come first serve. Only one process will ever be reading the queue at a time. Different processes can add to the queue, but we no longer need to worry about two processes trying to reserve the same product at the same time. Only one process will be doing the reservations. I was hoping to do this all in SQL, but that may not be possible.


Solution

  • Disclaimer: This may be an option, but the recommendations for using a Service Broker to serialize requests are likely the better solutions.

    If you can't use a transaction, but need your your stored procedure to return an immediate result, there are ways to safely update a record in a single statement.

    DECLARE @ProductId INT = 123
    DECLARE @Quantity INT = 5
    
    UPDATE Inventory
    SET Available = Available - @Quantity
    WHERE ProductId = @ProductId
    AND Available >= @Quantity
    
    IF @@ROW_COUNT > 0
    BEGIN
      -- Success
    END
    

    Under the covers, there is still a transaction occurring accompanied by a lock, but it just covers this one statement.

    If you need to update multiple records (reserve multiple product IDs) in one statement, you can use the OUTPUT clause to capture which records were successfully updated, which you can then compare with the original request.

    DECLARE @Request TABLE (ProductId INT, Quantity INT)
    DECLARE @Result TABLE (ProductId INT, Quantity INT)
    INSERT @Request VALUES (123, 5), (456, 1)
    
    UPDATE I
    SET Available = Available - R.Quantity
    OUTPUT R.ProductId, R.Quantity INTO @Result
    FROM @Request R
    JOIN Inventory I
        ON I.ProductId = R.ProductId
        AND I.Available >= R.@Quantity
    
    IF (SELECT COUNT(*) FROM @Request) = (SELECT COUNT(*) FROM @Result)
    BEGIN
        -- Success
    END
    ELSE BEGIN
        -- Partial or fail. Need to undo those that may have been updated.
    END
    

    In any case, you need to thoroughly think through your error handling and undo scenarios.

    If you store reservations separate from inventory and define "Available" as "Inventory.OnHand - SUM(Reserved.Quantity)", this approach is not an option.

    (Comments as to why this a bad approach will likely follow.)