Skip to content

Commit f46a9cd

Browse files
authored
Fix DB crash due to timeout (#1031)
* Create index on assessment ID in submissions table Improves read performance. * Create separate view layer for grading summary * Move views under AdminGradingView * Migrate to more optimised query * Use subquery instead of join for answers * Split main model query and view model generation * Split queries into 3 * Use ORM where possible * Fix typing * Restore group filtering functionality * Update Avenger backlog notification workers Fixes compatibilty with new function signature. * Remove old query * Remove comment
1 parent ee70c48 commit f46a9cd

File tree

7 files changed

+194
-112
lines changed

7 files changed

+194
-112
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 86 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,114 +1238,106 @@ defmodule Cadet.Assessments do
12381238
{:forbidden, "Forbidden."}}
12391239
"""
12401240
@spec all_submissions_by_grader_for_index(CourseRegistration.t()) ::
1241-
{:ok, String.t()}
1241+
{:ok, %{:assessments => [any()], :submissions => [any()], :users => [any()]}}
12421242
def all_submissions_by_grader_for_index(
12431243
grader = %CourseRegistration{course_id: course_id},
12441244
group_only \\ false,
1245-
ungraded_only \\ false
1245+
_ungraded_only \\ false
12461246
) do
12471247
show_all = not group_only
12481248

1249-
group_where =
1249+
group_filter =
12501250
if show_all,
12511251
do: "",
12521252
else:
1253-
"where s.student_id in (select cr.id from course_registrations cr inner join groups g on cr.group_id = g.id where g.leader_id = $2) or s.student_id = $2"
1253+
"AND s.student_id IN (SELECT cr.id FROM course_registrations AS cr INNER JOIN groups AS g ON cr.group_id = g.id WHERE g.leader_id = #{grader.id}) OR s.student_id = #{grader.id}"
12541254

1255-
ungraded_where =
1256-
if ungraded_only,
1257-
do: "where s.\"gradedCount\" < assts.\"questionCount\"",
1258-
else: ""
1259-
1260-
params = if show_all, do: [course_id], else: [course_id, grader.id]
1255+
# TODO: Restore ungraded filtering
1256+
# ... or more likely, decouple email logic from this function
1257+
# ungraded_where =
1258+
# if ungraded_only,
1259+
# do: "where s.\"gradedCount\" < assts.\"questionCount\"",
1260+
# else: ""
12611261

12621262
# We bypass Ecto here and use a raw query to generate JSON directly from
12631263
# PostgreSQL, because doing it in Elixir/Erlang is too inefficient.
12641264

1265-
case Repo.query(
1266-
"""
1267-
select json_agg(q)::TEXT from
1268-
(
1269-
select
1270-
s.id,
1271-
s.status,
1272-
s."unsubmittedAt",
1273-
s.xp,
1274-
s."xpAdjustment",
1275-
s."xpBonus",
1276-
s."gradedCount",
1277-
assts.jsn as assessment,
1278-
students.jsn as student,
1279-
unsubmitters.jsn as "unsubmittedBy"
1280-
from
1281-
(select
1282-
s.id,
1283-
s.student_id,
1284-
s.assessment_id,
1285-
s.status,
1286-
s.unsubmitted_at as "unsubmittedAt",
1287-
s.unsubmitted_by_id,
1288-
sum(ans.xp) as xp,
1289-
sum(ans.xp_adjustment) as "xpAdjustment",
1290-
s.xp_bonus as "xpBonus",
1291-
count(ans.id) filter (where ans.grader_id is not null) as "gradedCount"
1292-
from submissions s
1293-
left join
1294-
answers ans on s.id = ans.submission_id
1295-
#{group_where}
1296-
group by s.id) s
1297-
inner join
1298-
(select
1299-
a.id, a."questionCount", to_json(a) as jsn
1300-
from
1301-
(select
1302-
a.id,
1303-
a.title,
1304-
a.number as "assessmentNumber",
1305-
bool_or(ac.is_manually_graded) as "isManuallyGraded",
1306-
max(ac.type) as "type",
1307-
sum(q.max_xp) as "maxXp",
1308-
count(q.id) as "questionCount"
1309-
from assessments a
1310-
left join
1311-
questions q on a.id = q.assessment_id
1312-
inner join
1313-
assessment_configs ac on ac.id = a.config_id
1314-
where a.course_id = $1
1315-
group by a.id) a) assts on assts.id = s.assessment_id
1316-
inner join
1317-
(select
1318-
cr.id, to_json(cr) as jsn
1319-
from
1320-
(select
1321-
cr.id,
1322-
u.name as "name",
1323-
u.username as "username",
1324-
g.name as "groupName",
1325-
g.leader_id as "groupLeaderId"
1326-
from course_registrations cr
1327-
left join
1328-
groups g on g.id = cr.group_id
1329-
inner join
1330-
users u on u.id = cr.user_id) cr) students on students.id = s.student_id
1331-
left join
1332-
(select
1333-
cr.id, to_json(cr) as jsn
1334-
from
1335-
(select
1336-
cr.id,
1337-
u.name
1338-
from course_registrations cr
1339-
inner join
1340-
users u on u.id = cr.user_id) cr) unsubmitters on s.unsubmitted_by_id = unsubmitters.id
1341-
#{ungraded_where}
1342-
) q
1343-
""",
1344-
params
1345-
) do
1346-
{:ok, %{rows: [[nil]]}} -> {:ok, "[]"}
1347-
{:ok, %{rows: [[json]]}} -> {:ok, json}
1348-
end
1265+
submissions =
1266+
case Repo.query("""
1267+
SELECT
1268+
s.id,
1269+
s.status,
1270+
s.unsubmitted_at,
1271+
s.unsubmitted_by_id,
1272+
s_ans.xp,
1273+
s_ans.xp_adjustment,
1274+
s.xp_bonus,
1275+
s_ans.graded_count,
1276+
s.student_id,
1277+
s.assessment_id
1278+
FROM
1279+
submissions AS s
1280+
LEFT JOIN (
1281+
SELECT
1282+
ans.submission_id,
1283+
SUM(ans.xp) AS xp,
1284+
SUM(ans.xp_adjustment) AS xp_adjustment,
1285+
COUNT(ans.id) FILTER (
1286+
WHERE
1287+
ans.grader_id IS NOT NULL
1288+
) AS graded_count
1289+
FROM
1290+
answers AS ans
1291+
GROUP BY
1292+
ans.submission_id
1293+
) AS s_ans ON s_ans.submission_id = s.id
1294+
WHERE
1295+
s.assessment_id IN (
1296+
SELECT
1297+
id
1298+
FROM
1299+
assessments
1300+
WHERE
1301+
assessments.course_id = #{course_id}
1302+
) #{group_filter};
1303+
""") do
1304+
{:ok, %{columns: columns, rows: result}} ->
1305+
result
1306+
|> Enum.map(
1307+
&(columns
1308+
|> Enum.map(fn c -> String.to_atom(c) end)
1309+
|> Enum.zip(&1)
1310+
|> Enum.into(%{}))
1311+
)
1312+
end
1313+
1314+
{:ok, generate_grading_summary_view_model(submissions, course_id)}
1315+
end
1316+
1317+
defp generate_grading_summary_view_model(submissions, course_id) do
1318+
users =
1319+
CourseRegistration
1320+
|> where([cr], cr.course_id == ^course_id)
1321+
|> join(:inner, [cr], u in assoc(cr, :user))
1322+
|> join(:left, [cr, u], g in assoc(cr, :group))
1323+
|> preload([cr, u, g], user: u, group: g)
1324+
|> Repo.all()
1325+
1326+
assessment_ids = submissions |> Enum.map(& &1.assessment_id) |> Enum.uniq()
1327+
1328+
assessments =
1329+
Assessment
1330+
|> where([a], a.id in ^assessment_ids)
1331+
|> join(:left, [a], q in assoc(a, :questions))
1332+
|> join(:inner, [a], ac in assoc(a, :config))
1333+
|> preload([a, q, ac], questions: q, config: ac)
1334+
|> Repo.all()
1335+
1336+
%{
1337+
users: users,
1338+
assessments: assessments,
1339+
submissions: submissions
1340+
}
13491341
end
13501342

13511343
@spec get_answers_in_submission(integer() | String.t()) ::

lib/cadet/workers/NotificationWorker.ex

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,10 @@ defmodule Cadet.Workers.NotificationWorker do
7373
for avenger_cr <- avengers_crs do
7474
avenger = Cadet.Accounts.get_user(avenger_cr.user_id)
7575

76-
ungraded_submissions =
77-
Jason.decode!(
78-
elem(
79-
Cadet.Assessments.all_submissions_by_grader_for_index(avenger_cr, true, true),
80-
1
81-
)
82-
)
76+
{:ok, %{submissions: ungraded_submissions}} =
77+
Cadet.Assessments.all_submissions_by_grader_for_index(avenger_cr, true, true)
8378

84-
if length(ungraded_submissions) < ungraded_threshold do
79+
if Enum.count(ungraded_submissions) < ungraded_threshold do
8580
IO.puts("[AVENGER_BACKLOG] below threshold!")
8681
else
8782
IO.puts("[AVENGER_BACKLOG] SENDING_OUT")

lib/cadet_web/admin_controllers/admin_grading_controller.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ defmodule CadetWeb.AdminGradingController do
1010
group = String.to_atom(group)
1111

1212
case Assessments.all_submissions_by_grader_for_index(course_reg, group) do
13-
{:ok, submissions} ->
13+
{:ok, view_model} ->
1414
conn
1515
|> put_status(:ok)
1616
|> put_resp_content_type("application/json")
17-
|> text(submissions)
17+
|> render("gradingsummaries.json", view_model)
1818
end
1919
end
2020

lib/cadet_web/admin_views/admin_grading_view.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,94 @@ defmodule CadetWeb.AdminGradingView do
77
render_many(answers, CadetWeb.AdminGradingView, "grading_info.json", as: :answer)
88
end
99

10+
def render("gradingsummaries.json", %{
11+
users: users,
12+
assessments: assessments,
13+
submissions: submissions
14+
}) do
15+
for submission <- submissions do
16+
user = users |> Enum.find(&(&1.id == submission.student_id))
17+
assessment = assessments |> Enum.find(&(&1.id == submission.assessment_id))
18+
19+
render(
20+
CadetWeb.AdminGradingView,
21+
"gradingsummary.json",
22+
%{
23+
user: user,
24+
assessment: assessment,
25+
submission: submission,
26+
unsubmitter:
27+
case submission.unsubmitted_by_id do
28+
nil -> nil
29+
_ -> users |> Enum.find(&(&1.id == submission.unsubmitted_by_id))
30+
end
31+
}
32+
)
33+
end
34+
end
35+
36+
def render("gradingsummary.json", %{
37+
user: user,
38+
assessment: a,
39+
submission: s,
40+
unsubmitter: unsubmitter
41+
}) do
42+
s
43+
|> transform_map_for_view(%{
44+
id: :id,
45+
status: :status,
46+
unsubmittedAt: :unsubmitted_at,
47+
xp: :xp,
48+
xpAdjustment: :xp_adjustment,
49+
xpBonus: :xp_bonus,
50+
gradedCount:
51+
&case &1.graded_count do
52+
nil -> 0
53+
x -> x
54+
end
55+
})
56+
|> Map.merge(%{
57+
assessment:
58+
render_one(a, CadetWeb.AdminGradingView, "gradingsummaryassessment.json", as: :assessment),
59+
student: render_one(user, CadetWeb.AdminGradingView, "gradingsummaryuser.json", as: :cr),
60+
unsubmittedBy:
61+
case unsubmitter do
62+
nil -> nil
63+
cr -> transform_map_for_view(cr, %{id: :id, name: & &1.user.name})
64+
end
65+
})
66+
end
67+
68+
def render("gradingsummaryassessment.json", %{assessment: a}) do
69+
%{
70+
id: a.id,
71+
title: a.title,
72+
assessmentNumber: a.number,
73+
isManuallyGraded: a.config.is_manually_graded,
74+
type: a.config.type,
75+
maxXp: a.questions |> Enum.map(& &1.max_xp) |> Enum.sum(),
76+
questionCount: a.questions |> Enum.count()
77+
}
78+
end
79+
80+
def render("gradingsummaryuser.json", %{cr: cr}) do
81+
%{
82+
id: cr.id,
83+
name: cr.user.name,
84+
username: cr.user.username,
85+
groupName:
86+
case cr.group do
87+
nil -> nil
88+
_ -> cr.group.name
89+
end,
90+
groupLeaderId:
91+
case cr.group do
92+
nil -> nil
93+
_ -> cr.group.leader_id
94+
end
95+
}
96+
end
97+
1098
def render("grading_info.json", %{answer: answer}) do
1199
transform_map_for_view(answer, %{
12100
student:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Cadet.Repo.Migrations.CreateSubmissionsAssessmentIndex do
2+
use Ecto.Migration
3+
4+
def up do
5+
create(index(:submissions, [:assessment_id]))
6+
end
7+
8+
def down do
9+
drop(index(:submissions, [:assessment_id]))
10+
end
11+
end

test/cadet/email_test.exs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ defmodule Cadet.EmailTest do
2424
avenger_user = insert(:user, %{email: "[email protected]"})
2525
avenger = insert(:course_registration, %{user: avenger_user, role: :staff})
2626

27-
ungraded_submissions =
28-
Jason.decode!(
29-
elem(Cadet.Assessments.all_submissions_by_grader_for_index(avenger, true, true), 1)
30-
)
27+
{:ok, %{submissions: ungraded_submissions}} =
28+
Cadet.Assessments.all_submissions_by_grader_for_index(avenger, true, true)
3129

3230
email = Email.avenger_backlog_email("avenger_backlog", avenger_user, ungraded_submissions)
3331

test/cadet/jobs/notification_worker/notification_worker_test.exs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ defmodule Cadet.NotificationWorker.NotificationWorkerTest do
1818
submission = List.first(List.first(data.mcq_answers)).submission
1919

2020
# setup for avenger backlog
21-
ungraded_submissions =
22-
Jason.decode!(
23-
elem(Cadet.Assessments.all_submissions_by_grader_for_index(avenger_cr, true, true), 1)
24-
)
21+
{:ok, %{submissions: ungraded_submissions}} =
22+
Cadet.Assessments.all_submissions_by_grader_for_index(avenger_cr, true, true)
2523

2624
Repo.update_all(NotificationType, set: [is_enabled: true])
2725
Repo.update_all(NotificationConfig, set: [is_enabled: true])

0 commit comments

Comments
 (0)