fix(#196): fix race condition in job status updates
Implemented optimistic locking with version field and SELECT FOR UPDATE transactions to prevent data corruption from concurrent job status updates. Changes: - Added version field to RunnerJob schema for optimistic locking - Created migration 20260202_add_runner_job_version_for_concurrency - Implemented ConcurrentUpdateException for conflict detection - Updated RunnerJobsService methods with optimistic locking: * updateStatus() - with version checking and retry logic * updateProgress() - with version checking and retry logic * cancel() - with version checking and retry logic - Updated CoordinatorIntegrationService with SELECT FOR UPDATE: * updateJobStatus() - transaction with row locking * completeJob() - transaction with row locking * failJob() - transaction with row locking * updateJobProgress() - optimistic locking - Added retry mechanism (3 attempts) with exponential backoff - Added comprehensive concurrency tests (10 tests, all passing) - Updated existing test mocks to support updateMany Test Results: - All 10 concurrency tests passing ✓ - Tests cover concurrent status updates, progress updates, completions, cancellations, retry logic, and exponential backoff This fix prevents race conditions that could cause: - Lost job results (double completion) - Lost progress updates - Invalid status transitions - Data corruption under concurrent access Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Prisma, UserLayout } from "@prisma/client";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import type { CreateLayoutDto, UpdateLayoutDto } from "./dto";
|
||||
|
||||
@@ -13,7 +13,7 @@ export class LayoutsService {
|
||||
/**
|
||||
* Get all layouts for a user
|
||||
*/
|
||||
async findAll(workspaceId: string, userId: string) {
|
||||
async findAll(workspaceId: string, userId: string): Promise<UserLayout[]> {
|
||||
return this.prisma.userLayout.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
@@ -29,7 +29,7 @@ export class LayoutsService {
|
||||
/**
|
||||
* Get the default layout for a user
|
||||
*/
|
||||
async findDefault(workspaceId: string, userId: string) {
|
||||
async findDefault(workspaceId: string, userId: string): Promise<UserLayout> {
|
||||
const layout = await this.prisma.userLayout.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
@@ -63,7 +63,7 @@ export class LayoutsService {
|
||||
/**
|
||||
* Get a single layout by ID
|
||||
*/
|
||||
async findOne(id: string, workspaceId: string, userId: string) {
|
||||
async findOne(id: string, workspaceId: string, userId: string): Promise<UserLayout> {
|
||||
const layout = await this.prisma.userLayout.findUnique({
|
||||
where: {
|
||||
id,
|
||||
@@ -82,7 +82,11 @@ export class LayoutsService {
|
||||
/**
|
||||
* Create a new layout
|
||||
*/
|
||||
async create(workspaceId: string, userId: string, createLayoutDto: CreateLayoutDto) {
|
||||
async create(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
createLayoutDto: CreateLayoutDto
|
||||
): Promise<UserLayout> {
|
||||
// Use transaction to ensure atomicity when setting default
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// If setting as default, unset other defaults first
|
||||
@@ -114,7 +118,12 @@ export class LayoutsService {
|
||||
/**
|
||||
* Update a layout
|
||||
*/
|
||||
async update(id: string, workspaceId: string, userId: string, updateLayoutDto: UpdateLayoutDto) {
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
updateLayoutDto: UpdateLayoutDto
|
||||
): Promise<UserLayout> {
|
||||
// Use transaction to ensure atomicity when setting default
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// Verify layout exists
|
||||
@@ -163,7 +172,7 @@ export class LayoutsService {
|
||||
/**
|
||||
* Delete a layout
|
||||
*/
|
||||
async remove(id: string, workspaceId: string, userId: string) {
|
||||
async remove(id: string, workspaceId: string, userId: string): Promise<void> {
|
||||
// Verify layout exists
|
||||
const layout = await this.prisma.userLayout.findUnique({
|
||||
where: { id, workspaceId, userId },
|
||||
|
||||
Reference in New Issue
Block a user