Skip to content

Commit e6461b4

Browse files
authored
Merge pull request #3064 from CarnegieLearningWeb/hotfix/prevent-deadlock-on-condition-edit
remove select queries in experiment update transaction
2 parents efcf74b + 33a3913 commit e6461b4

3 files changed

Lines changed: 43 additions & 28 deletions

File tree

packages/backend/src/api/models/ConditionPayload.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export class ConditionPayload extends BaseModel {
2222
@ManyToOne(() => ExperimentCondition, (condition) => condition.conditionPayloads, { onDelete: 'CASCADE' })
2323
public parentCondition: ExperimentCondition;
2424

25+
@Column({ name: 'parentConditionId', nullable: true })
26+
parentConditionId?: string;
27+
2528
@ManyToOne(() => DecisionPoint, (decisionPoint) => decisionPoint.conditionPayloads, { onDelete: 'CASCADE' })
2629
public decisionPoint: DecisionPoint;
30+
31+
@Column({ name: 'decisionPointId', nullable: true })
32+
decisionPointId?: string;
2733
}

packages/backend/src/api/services/ExperimentService.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ import {
8383
ValidatedExperimentError,
8484
ConditionPayloadValidator,
8585
} from '../DTO/ExperimentDTO';
86-
import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO';
8786
import { FactorDTO } from '../DTO/FactorDTO';
8887
import { LevelDTO } from '../DTO/LevelDTO';
8988
import { CacheService } from './CacheService';
@@ -760,10 +759,10 @@ export class ExperimentService {
760759
this.experimentSchedulerService.updateExperimentSchedules(experiment as any, logger);
761760
}
762761

762+
let uniqueIdentifiers = await this.getAllUniqueIdentifiers(logger);
763763
return entityManager
764764
.transaction(async (transactionalEntityManager) => {
765765
experiment.context = experiment.context.map((context) => context.toLocaleLowerCase());
766-
let uniqueIdentifiers = await this.getAllUniqueIdentifiers(logger);
767766
if (experiment.conditions.length) {
768767
const response = this.setConditionOrPartitionIdentifiers(experiment.conditions, uniqueIdentifiers);
769768
experiment.conditions = response[0];
@@ -861,16 +860,16 @@ export class ExperimentService {
861860
});
862861
});
863862

864-
const conditionPayloadDocToSave: Array<Partial<Omit<ConditionPayload, 'parentCondition' | 'decisionPoint'>>> =
863+
const conditionPayloadDocToSave: Array<Partial<ConditionPayload>> =
865864
(newPayloads &&
866865
newPayloads.length > 0 &&
867866
newPayloads.map((conditionPayload) => {
868867
const conditionPayloadToReturn = {
869868
id: conditionPayload.id,
870869
payloadType: conditionPayload.payload.type,
871870
payloadValue: conditionPayload.payload.value,
872-
parentCondition: conditionPayload.parentCondition,
873-
decisionPoint: conditionPayload.decisionPoint,
871+
parentConditionId: conditionPayload.parentCondition,
872+
decisionPointId: conditionPayload.decisionPoint,
874873
};
875874
return conditionPayloadToReturn;
876875
})) ||
@@ -993,7 +992,6 @@ export class ExperimentService {
993992
let conditionDocs: ExperimentCondition[];
994993
let decisionPointDocs: DecisionPoint[];
995994
let queryDocs: Query[];
996-
let conditionPayloadDocs: ConditionPayloadDTO[];
997995
try {
998996
[conditionDocs, decisionPointDocs, queryDocs] = await Promise.all([
999997
Promise.all(
@@ -1026,16 +1024,14 @@ export class ExperimentService {
10261024
}
10271025

10281026
try {
1029-
[conditionPayloadDocs] = await Promise.all([
1030-
Promise.all(
1031-
conditionPayloadDocToSave.map(async (conditionPayload) => {
1032-
return this.conditionPayloadRepository.upsertConditionPayload(
1033-
conditionPayload,
1034-
transactionalEntityManager
1035-
);
1036-
})
1037-
) as any,
1038-
]);
1027+
await Promise.all(
1028+
conditionPayloadDocToSave.map(async (conditionPayload) => {
1029+
return this.conditionPayloadRepository.upsertConditionPayload(
1030+
conditionPayload,
1031+
transactionalEntityManager
1032+
);
1033+
})
1034+
);
10391035
} catch (err) {
10401036
const error = err as Error;
10411037
error.message = `Error in creating conditionPayloads "updateExperimentInDB"`;
@@ -1051,11 +1047,15 @@ export class ExperimentService {
10511047
return { ...decisionPointDoc, experiment: decisionPointDoc.experiment };
10521048
});
10531049

1054-
const conditionPayloadDocToReturn = await transactionalEntityManager.getRepository(ConditionPayload).find({
1055-
relations: ['parentCondition', 'decisionPoint'],
1056-
where: { id: In(conditionPayloadDocs.map((conditionPayload) => conditionPayload.id)) },
1050+
const conditionPayloadDocToReturn = conditionPayloadDocToSave.map((conditionPayload) => {
1051+
const parentCondition = conditionDocs.find((c) => c.id === conditionPayload.parentConditionId);
1052+
const decisionPoint = decisionPointDocs.find((dp) => dp.id === conditionPayload.decisionPointId);
1053+
return {
1054+
...conditionPayload,
1055+
parentCondition: parentCondition,
1056+
decisionPoint: decisionPoint,
1057+
};
10571058
});
1058-
10591059
// sort the payloads by decision point order and condition order
10601060
conditionPayloadDocToReturn.sort((a, b) => {
10611061
if (a.decisionPoint.order === b.decisionPoint.order) {

packages/backend/test/unit/services/ExperimentService.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ describe('ExperimentService Testing', () => {
8383
order: 1,
8484
};
8585

86+
const mockCondition3: Partial<ExperimentCondition> = {
87+
id: 'condition-3',
88+
conditionCode: 'new-treatment',
89+
assignmentWeight: 40,
90+
order: 2,
91+
};
92+
8693
const createMockDecisionPoint1 = () => ({
8794
id: 'partition-1',
8895
site: 'test-site',
@@ -115,7 +122,9 @@ describe('ExperimentService Testing', () => {
115122
payloadType: PAYLOAD_TYPE.STRING,
116123
payloadValue: 'control',
117124
parentCondition: mockCondition1 as ExperimentCondition,
125+
parentConditionId: mockCondition1.id,
118126
decisionPoint: mockDecisionPoint1 as DecisionPoint,
127+
decisionPointId: mockDecisionPoint1.id,
119128
};
120129

121130
mockExperiment = {
@@ -170,14 +179,14 @@ describe('ExperimentService Testing', () => {
170179
conditionPayloads: [
171180
{
172181
id: 'payload-1',
173-
parentCondition: 'condition-1',
174-
decisionPoint: 'partition-1',
182+
parentCondition: mockCondition1,
183+
decisionPoint: mockDecisionPoint1,
175184
payload: { type: PAYLOAD_TYPE.STRING, value: 'control' },
176185
} as any,
177186
{
178187
id: 'payload-2',
179-
parentCondition: 'condition-3',
180-
decisionPoint: 'partition-1',
188+
parentCondition: mockCondition3,
189+
decisionPoint: mockDecisionPoint1,
181190
payload: { type: PAYLOAD_TYPE.STRING, value: 'new-treatment' },
182191
} as any,
183192
],
@@ -267,7 +276,7 @@ describe('ExperimentService Testing', () => {
267276
useValue: {
268277
find: jest.fn().mockResolvedValue([mockCondition1]),
269278
save: jest.fn().mockResolvedValue(mockCondition1),
270-
upsertExperimentCondition: jest.fn().mockResolvedValue(mockCondition1),
279+
upsertExperimentCondition: jest.fn().mockImplementation((value) => Promise.resolve(value)),
271280
deleteCondition: jest.fn().mockResolvedValue({ affected: 1 }),
272281
getAllUniqueIdentifier: jest.fn().mockResolvedValue(['C1', 'C2']),
273282
insertConditions: jest.fn().mockResolvedValue([mockCondition1]),
@@ -279,7 +288,7 @@ describe('ExperimentService Testing', () => {
279288
find: jest.fn().mockResolvedValue([mockDecisionPoint1]),
280289
save: jest.fn().mockResolvedValue(mockDecisionPoint1),
281290
findOne: jest.fn().mockResolvedValue(null),
282-
upsertDecisionPoint: jest.fn().mockResolvedValue(mockDecisionPoint1),
291+
upsertDecisionPoint: jest.fn().mockImplementation((value) => Promise.resolve(value)),
283292
deleteDecisionPoint: jest.fn().mockResolvedValue({ affected: 1 }),
284293
deleteByIds: jest.fn().mockResolvedValue({ affected: 1 }),
285294
getAllUniqueIdentifier: jest.fn().mockResolvedValue(['C1', 'C2']),
@@ -693,8 +702,8 @@ describe('ExperimentService Testing', () => {
693702
// Should create payloads for partition-2 with both conditions
694703
expect(conditionPayloadRepo.upsertConditionPayload).toHaveBeenCalledWith(
695704
expect.objectContaining({
696-
parentCondition: 'condition-1',
697-
decisionPoint: 'partition-2',
705+
parentConditionId: 'condition-1',
706+
decisionPointId: 'partition-2',
698707
payloadValue: 'control',
699708
}),
700709
entityManager

0 commit comments

Comments
 (0)