fix(scheduler): eliminate read/write concurrency hazzard while updating predicate status

This commit is contained in:
Karol Sójko 2022-07-26 09:23:45 +02:00
parent 049e66770a
commit 4ab0d24d24
No known key found for this signature in database
GPG key ID: A50543BF560BDEB0
3 changed files with 8 additions and 70 deletions

View file

@ -1,8 +1,7 @@
import { PredicateAuthority, PredicateName, PredicateVerificationResult } from '@standardnotes/predicates'
import 'reflect-metadata' import 'reflect-metadata'
import { JobDoneInterpreterInterface } from '../../Job/JobDoneInterpreterInterface' import { PredicateAuthority, PredicateName, PredicateVerificationResult } from '@standardnotes/predicates'
import { JobRepositoryInterface } from '../../Job/JobRepositoryInterface'
import { Predicate } from '../../Predicate/Predicate' import { Predicate } from '../../Predicate/Predicate'
import { PredicateRepositoryInterface } from '../../Predicate/PredicateRepositoryInterface' import { PredicateRepositoryInterface } from '../../Predicate/PredicateRepositoryInterface'
import { PredicateStatus } from '../../Predicate/PredicateStatus' import { PredicateStatus } from '../../Predicate/PredicateStatus'
@ -11,12 +10,10 @@ import { UpdatePredicateStatus } from './UpdatePredicateStatus'
describe('UpdatePredicateStatus', () => { describe('UpdatePredicateStatus', () => {
let predicateRepository: PredicateRepositoryInterface let predicateRepository: PredicateRepositoryInterface
let jobRepository: JobRepositoryInterface
let jobDoneInterpreter: JobDoneInterpreterInterface
let predicateComplete: Predicate let predicateComplete: Predicate
let predicateIncomplete: Predicate let predicateIncomplete: Predicate
const createUseCase = () => new UpdatePredicateStatus(predicateRepository, jobRepository, jobDoneInterpreter) const createUseCase = () => new UpdatePredicateStatus(predicateRepository)
beforeEach(() => { beforeEach(() => {
predicateComplete = { predicateComplete = {
@ -33,15 +30,9 @@ describe('UpdatePredicateStatus', () => {
predicateRepository = {} as jest.Mocked<PredicateRepositoryInterface> predicateRepository = {} as jest.Mocked<PredicateRepositoryInterface>
predicateRepository.findByJobUuid = jest.fn().mockReturnValue([predicateComplete, predicateIncomplete]) predicateRepository.findByJobUuid = jest.fn().mockReturnValue([predicateComplete, predicateIncomplete])
predicateRepository.save = jest.fn() predicateRepository.save = jest.fn()
jobRepository = {} as jest.Mocked<JobRepositoryInterface>
jobRepository.markJobAsDone = jest.fn()
jobDoneInterpreter = {} as jest.Mocked<JobDoneInterpreterInterface>
jobDoneInterpreter.interpret = jest.fn()
}) })
it('should mark a predicate as complete and update job as done', async () => { it('should mark a predicate as complete', async () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
predicate: { name: PredicateName.EmailBackupsEnabled, jobUuid: '1-2-3', authority: PredicateAuthority.Auth }, predicate: { name: PredicateName.EmailBackupsEnabled, jobUuid: '1-2-3', authority: PredicateAuthority.Auth },
@ -49,7 +40,6 @@ describe('UpdatePredicateStatus', () => {
}), }),
).toEqual({ ).toEqual({
success: true, success: true,
allPredicatesChecked: true,
}) })
expect(predicateRepository.save).toHaveBeenCalledWith({ expect(predicateRepository.save).toHaveBeenCalledWith({
@ -57,37 +47,5 @@ describe('UpdatePredicateStatus', () => {
name: 'email-backups-enabled', name: 'email-backups-enabled',
status: 'denied', status: 'denied',
}) })
expect(jobRepository.markJobAsDone).toHaveBeenCalled()
expect(jobDoneInterpreter.interpret).toHaveBeenCalled()
})
it('should mark a predicate as complete and not update job as done if there are still incomplete predicates', async () => {
predicateRepository.findByJobUuid = jest
.fn()
.mockReturnValue([
predicateComplete,
predicateIncomplete,
{ uuid: '3-4-5', status: PredicateStatus.Pending } as jest.Mocked<Predicate>,
])
expect(
await createUseCase().execute({
predicate: { name: PredicateName.EmailBackupsEnabled, jobUuid: '1-2-3', authority: PredicateAuthority.Auth },
predicateVerificationResult: PredicateVerificationResult.Denied,
}),
).toEqual({
success: true,
allPredicatesChecked: false,
})
expect(predicateRepository.save).toHaveBeenCalledWith({
uuid: '2-3-4',
name: 'email-backups-enabled',
status: 'denied',
})
expect(jobRepository.markJobAsDone).not.toHaveBeenCalled()
expect(jobDoneInterpreter.interpret).not.toHaveBeenCalled()
}) })
}) })

View file

@ -1,8 +1,6 @@
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { JobDoneInterpreterInterface } from '../../Job/JobDoneInterpreterInterface'
import { JobRepositoryInterface } from '../../Job/JobRepositoryInterface'
import { PredicateRepositoryInterface } from '../../Predicate/PredicateRepositoryInterface' import { PredicateRepositoryInterface } from '../../Predicate/PredicateRepositoryInterface'
import { PredicateStatus } from '../../Predicate/PredicateStatus' import { PredicateStatus } from '../../Predicate/PredicateStatus'
import { UseCaseInterface } from '../UseCaseInterface' import { UseCaseInterface } from '../UseCaseInterface'
@ -12,35 +10,20 @@ import { UpdatePredicateStatusResponse } from './UpdatePredicateStatusResponse'
@injectable() @injectable()
export class UpdatePredicateStatus implements UseCaseInterface { export class UpdatePredicateStatus implements UseCaseInterface {
constructor( constructor(@inject(TYPES.PredicateRepository) private predicateRepository: PredicateRepositoryInterface) {}
@inject(TYPES.PredicateRepository) private predicateRepository: PredicateRepositoryInterface,
@inject(TYPES.JobRepository) private jobRepository: JobRepositoryInterface,
@inject(TYPES.JobDoneInterpreter) private jobDoneInterpreter: JobDoneInterpreterInterface,
) {}
async execute(dto: UpdatePredicateStatusDTO): Promise<UpdatePredicateStatusResponse> { async execute(dto: UpdatePredicateStatusDTO): Promise<UpdatePredicateStatusResponse> {
const predicates = await this.predicateRepository.findByJobUuid(dto.predicate.jobUuid) const predicates = await this.predicateRepository.findByJobUuid(dto.predicate.jobUuid)
let allPredicatesChecked = true
for (const predicate of predicates) { for (const predicate of predicates) {
if (predicate.name === dto.predicate.name) { if (predicate.name === dto.predicate.name) {
predicate.status = dto.predicateVerificationResult as unknown as PredicateStatus predicate.status = dto.predicateVerificationResult as unknown as PredicateStatus
await this.predicateRepository.save(predicate) await this.predicateRepository.save(predicate)
} }
if (predicate.status === PredicateStatus.Pending) {
allPredicatesChecked = false
}
}
if (allPredicatesChecked) {
await this.jobDoneInterpreter.interpret(dto.predicate.jobUuid)
await this.jobRepository.markJobAsDone(dto.predicate.jobUuid)
} }
return { return {
success: true, success: true,
allPredicatesChecked,
} }
} }
} }

View file

@ -1,6 +1,3 @@
export type UpdatePredicateStatusResponse = export type UpdatePredicateStatusResponse = {
| { success: boolean
success: true }
allPredicatesChecked: boolean
}
| { success: false }