Skip to content

Commit 6c6bc85

Browse files
committed
Handling possible race condition in exam creation by retry mechanism.
1 parent 82c9b2b commit 6c6bc85

1 file changed

Lines changed: 45 additions & 11 deletions

File tree

app/model/repository/GroupExams.php

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Model\Entity\Group;
66
use App\Model\Entity\GroupExam;
77
use Doctrine\ORM\EntityManagerInterface;
8+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
89
use DateTime;
910
use Exception;
1011

@@ -38,6 +39,44 @@ public function findPendingForGroup(Group $group): ?GroupExam
3839
return $exam ? reset($exam) : null;
3940
}
4041

42+
/**
43+
* Internal helper that tries to find the exam or create it if not present.
44+
* It returns null if creation failed due to race condition (expects retry by caller).
45+
* @param Group $group
46+
* @param DateTime $begin
47+
* @param DateTime $end
48+
* @param bool $strict
49+
* @return GroupExam|null
50+
*/
51+
private function tryFindOrCreate(Group $group, DateTime $begin, DateTime $end, bool $strict): ?GroupExam
52+
{
53+
$exam = $this->findBy(["group" => $group, "begin" => $begin]);
54+
if (count($exam) > 1) {
55+
throw new Exception("Data corruption, there is more than one group exam starting at the same time.");
56+
}
57+
58+
if (!$exam) {
59+
try {
60+
$this->em->getConnection()->executeQuery(
61+
"INSERT INTO group_exams (group_id, begin, end, lock_strict) VALUES (:gid, :begin, :end, :strict)",
62+
[
63+
'gid' => $group->getId(),
64+
'begin' => $begin->format('Y-m-d H:i:s'),
65+
'end' => $end->format('Y-m-d H:i:s'),
66+
'strict' => $strict ? 1 : 0
67+
]
68+
);
69+
} catch (UniqueConstraintViolationException) {
70+
// race condition, another transaction created the entity meanwhile
71+
}
72+
return null; // signal caller to retry
73+
} else {
74+
$exam = reset($exam);
75+
}
76+
77+
return $exam;
78+
}
79+
4180
/**
4281
* Fetch group exam entity by group-begin index. If not present, new entity is created.
4382
* @param Group $group
@@ -56,18 +95,13 @@ public function findOrCreate(
5695
$end = $end ?? $group->getExamEnd();
5796
$strict = $strict === null ? $group->isExamLockStrict() : $strict;
5897

59-
$exam = $this->findBy(["group" => $group, "begin" => $begin]);
60-
if (count($exam) > 1) {
61-
throw new Exception("Data corruption, there is more than one group exam starting at the same time.");
98+
for ($retries = 0; $retries < 3; $retries++) {
99+
$exam = $this->tryFindOrCreate($group, $begin, $end, $strict);
100+
if ($exam !== null) {
101+
return $exam;
102+
}
62103
}
63104

64-
if (!$exam) {
65-
$exam = new GroupExam($group, $begin, $end, $strict);
66-
$this->persist($exam);
67-
} else {
68-
$exam = reset($exam);
69-
}
70-
71-
return $exam;
105+
throw new Exception("Failed to find or create group exam after multiple attempts.");
72106
}
73107
}

0 commit comments

Comments
 (0)