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>
95 lines
2.2 KiB
TypeScript
95 lines
2.2 KiB
TypeScript
import { Injectable, Logger } from "@nestjs/common";
|
|
import type { PrismaClient } from "@prisma/client";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { createAuth, type Auth } from "./auth.config";
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
private readonly logger = new Logger(AuthService.name);
|
|
private readonly auth: Auth;
|
|
|
|
constructor(private readonly prisma: PrismaService) {
|
|
// PrismaService extends PrismaClient and is compatible with BetterAuth's adapter
|
|
// Cast is safe as PrismaService provides all required PrismaClient methods
|
|
this.auth = createAuth(this.prisma as unknown as PrismaClient);
|
|
}
|
|
|
|
/**
|
|
* Get BetterAuth instance
|
|
*/
|
|
getAuth(): Auth {
|
|
return this.auth;
|
|
}
|
|
|
|
/**
|
|
* Get user by ID
|
|
*/
|
|
async getUserById(userId: string): Promise<{
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
authProviderId: string | null;
|
|
} | null> {
|
|
return this.prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
authProviderId: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get user by email
|
|
*/
|
|
async getUserByEmail(email: string): Promise<{
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
authProviderId: string | null;
|
|
} | null> {
|
|
return this.prisma.user.findUnique({
|
|
where: { email },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
authProviderId: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify session token
|
|
* Returns session data if valid, null if invalid or expired
|
|
*/
|
|
async verifySession(
|
|
token: string
|
|
): Promise<{ user: Record<string, unknown>; session: Record<string, unknown> } | null> {
|
|
try {
|
|
const session = await this.auth.api.getSession({
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
user: session.user as Record<string, unknown>,
|
|
session: session.session as Record<string, unknown>,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(
|
|
"Session verification failed",
|
|
error instanceof Error ? error.message : "Unknown error"
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
}
|