Unlike every earlier sequence diagram, the main actor here isn't a client call — it's the
Proctor process sending itself delayed messages (Process.send_after/3) and reacting
to them in handle_info/2. The GenServer reply timeout ({:reply, :ok, state, timeout})
is what wakes it up to start the next queued quiz.
sequenceDiagram
actor Client
participant Mastery
participant Proctor as Proctor (GenServer)
participant QuizManager
participant QuizSession
participant Registry as Registry.QuizSession
Client->>Mastery: schedule_quiz(quiz_fields, templates, start_at, end_at)
Mastery->>Mastery: QuizValidator.errors / TemplateValidator.errors (per template)
Mastery->>Proctor: call {:schedule_quiz, quiz}
Proctor->>Proctor: prepend to quizzes, start_quizzes(quizzes, now)
alt quiz.start_at <= now (starts immediately)
Proctor->>QuizManager: build_quiz(quiz.fields)
Proctor->>QuizManager: add_template(title, fields) per template
Proctor->>Proctor: Process.send_after(self(), {:end_quiz, title}, end_at - now)
else start_at is in the future
Note over Proctor: quiz stays queued, list re-sorted by start_at
end
Proctor->>Proctor: sort remaining quizzes by start_at
Proctor-->>Mastery: {:reply, :ok, quizzes, timeout}
Note over Proctor: reply timeout = next quiz's start_at - now\n(no timeout arg at all if queue is empty)
Mastery-->>Client: :ok
Note over Proctor: ... time passes, no other messages arrive ...
Proctor->>Proctor: handle_info(:timeout)
Proctor->>Proctor: start_quizzes(quizzes, now)
loop for each quiz whose start_at has arrived
Proctor->>QuizManager: build_quiz(quiz.fields)
Proctor->>QuizManager: add_template(title, fields) per template
Proctor->>Proctor: Process.send_after(self(), {:end_quiz, title}, end_at - now)
end
Proctor-->>Proctor: {:noreply, remaining_quizzes, next_timeout}
Note over Proctor: ... end_at for a running quiz arrives ...
Proctor->>Proctor: handle_info({:end_quiz, title})
Proctor->>QuizManager: remove_quiz(title)
Proctor->>QuizSession: active_sessions_for(title)
QuizSession->>Registry: which_children / keys lookup for this title
Registry-->>QuizSession: matching {title, email} names
QuizSession-->>Proctor: [names]
Proctor->>QuizSession: end_sessions(names)
loop each active session
QuizSession->>QuizSession: GenServer.stop(via(name))
end
Proctor->>Proctor: Logger.info("Stopped quiz #{title}")
Proctor->>Proctor: handle_info(:timeout) (re-check queue, reschedule)
Notes:
- The GenServer reply-timeout mechanism (4th element of
{:reply, ..., timeout}) is doing double duty as a priority-queue scheduler: the only thing it ever waits for is "when does the next queued quiz need to start," recomputed every time state changes. handle_info({:end_quiz, title})ends by callinghandle_info(:timeout)itself, so ending one quiz and starting the next queued one happen as part of the same message — the process never idles longer than it has to.- If a learner is mid-quiz when
end_athits, theirQuizSessionis force-stopped (GenServer.stop/1) — there's no "let them finish the current question" grace period.