TL;DR
SQL Server race condition on a round-robin process queue despite adding ROWLOCK, READPAST, and UPDLOCK hints in conjunction with application locks.
I have an application where multiple app servers and multiple processes run "jobs" that are stored in a queue-like table in our SQL Server 2014 database. Every x number of seconds, each process queries the table for new jobs to run (if the process is available to take jobs). New jobs need to be assigned in something like a round-robin fashion on a user-to-user basis. The problem is that my current query is running into a race condition so that multiple processes are acting on the same job, resulting in duplicate data sets in our output tables. This is what the current query looks like:
EXEC sp_getapplock @Resource = 'GetJobAssignments', @LockMode = 'Exclusive', @LockOwner = 'Session';
BEGIN TRANSACTION;
    WITH cte AS
    (
    SELECT TOP (@nMaxJobs) [JobId]
        ,[Owner]
        ,[Timeout]
        ,[StartTime]
        ,[Status]
        ,[userID] 
        , ROW_NUMBER() OVER (PARTITION BY [userID] ORDER BY [JobID]) AS [recID] 
    FROM [Jobs]
    WITH (ROWLOCK, READPAST, UPDLOCK)
    WHERE [Status] = 0 AND [Owner] is null
    ORDER BY [recID], [userID]
    )
    UPDATE cte SET [Owner] = @owner, [Status] = 1;
COMMIT;
EXEC sp_releaseapplock @Resource = 'GetJobAssignments', @LockOwner = 'Session';
I have tried various combinations of ROWLOCK, READPAST, UPDLOCK, and XLOCK with no luck. Also used with and without the applock on both Session and Transaction settings. In other iterations of the query I messed around with putting the transaction isolation level to REPEATABLE READ and SERIALIZABLE with equal measures of no success.
Bearing in mind that I am neither an SQL nor a SQLServer expert, this next part might not mean anything...I have tried doing this within a stored procedure (as it is now), but in the past it has also been called from EF5 using dbContext.Database.ExecuteSqlCommand() (it is now just a stored procedure that is part of the dbContext).
This current query is the result of messing around with a couple known working answers to questions similar to this:
SQL Server Process Queue Race Condition
https://dba.stackexchange.com/questions/81791/strategies-for-checking-out-records-for-processing
Outside of basic research on the things I have mentioned here, I am pretty unfamiliar with strategies in SQL Server to prevent deadlocks/race conditions. Are there any other options that I could try in my query, or are there any known preconditions that might exist that I could check for that would prevent this current strategy from working?
 
    