Skip to content

Latest commit

 

History

History
68 lines (60 loc) · 3.29 KB

File metadata and controls

68 lines (60 loc) · 3.29 KB

Chapter 8 — Timed quiz lifecycle via the Proctor

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)
Loading

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 calling handle_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_at hits, their QuizSession is force-stopped (GenServer.stop/1) — there's no "let them finish the current question" grace period.