У меня есть таблица, которая обрабатывается одновременно N потоками.
CREATE TABLE [dbo].[Jobs]
(
[Id] BIGINT NOT NULL CONSTRAINT [PK_Jobs] PRIMARY KEY IDENTITY,
[Data] VARBINARY(MAX) NOT NULL,
[CreationTimestamp] DATETIME2(7) NOT NULL,
[Type] INT NOT NULL,
[ModificationTimestamp] DATETIME2(7) NOT NULL,
[State] INT NOT NULL,
[RowVersion] ROWVERSION NOT NULL,
[Activity] INT NULL,
[Parent_Id] BIGINT NULL
)
GO
CREATE NONCLUSTERED INDEX [IX_Jobs_Type_State_RowVersion] ON [dbo].[Jobs]([Type], [State], [RowVersion] ASC) WHERE ([State] <> 100)
GO
CREATE NONCLUSTERED INDEX [IX_Jobs_Parent_Id_State] ON [dbo].[Jobs]([Parent_Id], [State] ASC)
GO
Работа добавляется к таблице с помощью State=0 (New)
- ее может потреблять любой рабочий в этом состоянии. Когда рабочий получает этот элемент очереди, State
изменен на 50 (Processing)
, и задание становится недоступным для других потребителей (рабочие звонят [dbo].[Jobs_GetFirstByType]
с аргументами: Type=any, @CurrentState=0, @NewState=50
).
CREATE PROCEDURE [dbo].[Jobs_GetFirstByType]
@Type INT,
@CurrentState INT,
@NewState INT
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @JobId BIGINT;
BEGIN TRAN
SELECT TOP(1)
@JobId = Id
FROM [dbo].[Jobs] WITH (UPDLOCK, READPAST)
WHERE [Type] = @Type AND [State] = @CurrentState
ORDER BY [RowVersion];
UPDATE [dbo].[Jobs]
SET [State] = @NewState,
[ModificationTimestamp] = SYSUTCDATETIME()
OUTPUT INSERTED.[Id]
,INSERTED.[RowVersion]
,INSERTED.[Data]
,INSERTED.[Type]
,INSERTED.[State]
,INSERTED.[Activity]
WHERE [Id] = @JobId;
COMMIT TRAN
END
После обработки задание State
можно снова изменить на 0 (New)
или его можно установить как 100 (Completed)
.
CREATE PROCEDURE [dbo].[Jobs_UpdateStatus]
@Id BIGINT,
@State INT,
@Activity INT
AS
BEGIN
UPDATE j
SET j.[State] = @State,
j.[Activity] = @Activity,
j.[ModificationTimestamp] = SYSUTCDATETIME()
OUTPUT INSERTED.[Id], INSERTED.[RowVersion]
FROM [dbo].[Jobs] j
WHERE j.[Id] = @Id;
END
Работа имеет иерархическую структуру, родительское задание получает State=100 (Completed)
только тогда, когда все дочерние элементы завершены.
Некоторые рабочие называют хранимые процедуры ([dbo].[Jobs_GetCountWithExcludedState]
с @ExcludedState=100
), который возвращает количество незавершенных заданий, когда он возвращает 0, родительское задание State
может быть установлено на 100 (Completed)
.
CREATE PROCEDURE [dbo].[Jobs_GetCountWithExcludedState]
@ParentId INT,
@ExcludedState INT
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT COUNT(1)
FROM [dbo].[Jobs]
WHERE [Parent_Id] = @ParentId
AND [State] <> @ExcludedState
END
Основная проблема - странное поведение этой хранимой процедуры. Иногда он возвращает 0 для родительского задания, но у него есть неполные задания. Я попытался включить отслеживание данных изменений и некоторую отладочную информацию (включая профилирование) - дочерние задания на 100% не имеют State=100
, когда SP возвращается 0.
Кажется, что SP пропускает записи, которые не находятся в состоянии 100 (Completed)
, но почему это происходит и как мы можем это предотвратить?
UPD:
Вызов [dbo].[Jobs_GetCountWithExcludedState]
начинается, когда родительское задание имеет дочерние элементы. Там не может быть ситуации, когда работник начнет проверять детские задания без их существования, поскольку создает дочерние элементы и устанавливает на работу проверки родительского задания, заключенную в транзакцию:
using (var ts = new TransactionScope())
{
_jobManager.AddChilds(parentJob);
parentJob.State = 0;
parentJob.Activity = 30; // in this activity worker starts checking child jobs
ts.Complete();
}