OCaml Module 12
Effects, concurrency and parallelism (OCaml 5) interview questions
Interview questions on effects, concurrency and parallelism (OCaml 5): warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
Both kinds of handler catch something thrown from deep inside a call. What can an effect handler do that an exception handler cannot?
Both are found the same way: the nearest enclosing handler with a matching case runs, and one without a case lets the exception or the effect go on outward. The difference is what the handler receives. An exception handler gets the exception's value, and the code that raised it has already been abandoned, so the handler can only take over. An effect handler also gets the continuation, the suspended rest of the computation from the perform onwards, and can resume it with continue k v, which makes the perform return v. That makes an effect a question the caller answers, such as a setting or a log line. It also brings duties an exception lacks: a continuation can be resumed only once, or Continuation_already_resumed is raised, and one that will not be continued should be ended with discontinue so its cleanup runs.
From the lesson Effect handlers
-
Fresher to mid
Why would a program turn a push-style iterator into a Yield-based generator?
Answered in the lesson Generators and schedulers with effects
-
Fresher to mid
What is an Eio fiber, and how does Eio.Fiber.both differ from spawning two threads?
-
Fresher to mid
What does Domain.recommended_domain_count () return, and how should it decide how many domains to spawn?
It returns the ceiling the OCaml 5 runtime recommends for parallel work on this machine: on the lesson's machine, 18. The figure counts hardware threads the runtime can reasonably back with a domain, not tasks a program would like to run. Spawn one domain per core at most and feed work into it; each spawn allocates a stack and a minor heap, so a long task list is better served by a pool that keeps a handful of long-lived domains and hands them chunks of the work.
From the lesson Parallelism with domains and atomics
-
Fresher to mid
What does Domainslib's parallel_for_reduce do, and why must its reducer be associative?
It runs a body on every number of a range on a task pool's domains and combines the results with a reducer. The library combines the parts in an arbitrary grouping, and its interface asks for an associative reducer to get a deterministic result: ( + ) let pool.ml sum 1 to 1,000,000 to 500000500000 on 18 domains, as one domain does, while ( - ) would give an answer that depends on the split.
From the lesson Parallel patterns and race detection
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A service installs one settings handler around its main function, and its code reads limits with perform (Ask "max_rows"). After request work moves into Domain.spawn, every request fails with Effect.Unhandled (Ask "max_rows"), although the handler is still in place. Why, and how do you fix it?
- What it tests
- Knowing that a deep handler covers only the stack of the computation it runs, that a new domain or thread starts without handlers, and choosing a fix that keeps the effect design.
- First step
- Find where the handler sits relative to Domain.spawn, then reproduce the failure in a few lines: a handler around Domain.join (Domain.spawn f), where f performs Ask. Before changing anything, ask on whose stack that perform runs, and which handlers that stack holds.
- Follow-ups
-
- Where exactly is the handler needed, and what are the two ways to give the spawned work its settings?
- Does the same happen with a systhread created by Thread.create inside the handler?
- Why did nothing warn about this at compile time, and how would you catch it in tests?
From the lesson Effect handlers
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A pricing function reads settings through an Ask effect and writes progress lines through a Log effect. You want unit tests that run it with no files, no globals and no printing, and that check both its result and what it logged. How do you write the test handler, and what should each test assert?
- What it tests
- Using one handler as a test double for several effects, collecting what the code reports, and making missing or unexpected effects fail loudly instead of going unnoticed.
- First step
- Write one function, run_test settings f, that runs f () in a match with an effect case for each effect: Ask answered from the test's own table, Log appended to a list created inside run_test. Return the value and the collected lines together, so every test can assert on both.
- Follow-ups
-
- How do you make a setting the test forgot fail with a message that names it?
- What happens when the code under test performs an effect the test handler has no case for, and is that good?
- How does this compare with passing a logger and a settings record as parameters?
From the lesson Effect handlers
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Senior to staff
A CSV importer meets rows with no price. One caller wants such rows skipped and counted, another wants a default price, a third wants the import stopped at the first bad row, and the importer must not know which. Design this with an effect, and say how stopping should work.
- What it tests
- Designing a resumable error as an effect whose answer carries the caller's decision, and ending a suspended computation with discontinue, compared with exceptions and result types.
- First step
- Give the effect an answer type that can express every decision: Missing_price : int -> float option Effect.t, where Some p means use p and None means skip the row. Each caller installs its own handler around the same import; skipping and the default are both continue, while stopping must end the suspended import.
- Follow-ups
-
- Why not have the importer return a result type, or raise an exception, instead?
- What goes wrong if the stopping handler simply raises its own exception in the effect case?
- How would the skipping caller count the rows it skipped, and report which ones?
From the lesson Effect handlers
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
Walk through the first six prints of a round-robin scheduler on three tasks A, B and C that each print their name twice and perform Yield between them. The program forks B, then forks C, then runs A inline, each with no other work. What order comes out, and why?
- What it tests
- Reading a run queue of continuations as a FIFO, telling what Fork and Yield each put in the queue and what runs next, and tracing a run on paper.
- First step
- Draw the queue after every effect and write down which task is running. Before tracing, settle two rules: what Fork does with the task that performs it and with the new task, and what Yield does with the current task. Those two rules fix the whole order; then step until every task has returned.
- Follow-ups
-
- What if the scheduler were LIFO (a stack) instead of FIFO: which lines would change and in what order?
- How does Fork differ from Yield in terms of what runs next after each one?
- A task that returns is not enqueued again. What runs the next task after a return, and why does the queue size go down by one?
- Give an invariant the handler must preserve for the scheduler to be correct, and name one bug that violates it.
From the lesson Generators and schedulers with effects
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Senior to staff
You are porting a toplevel-style interpreter from Effect.Deep to Effect.Shallow because its rules change between performs: each step reads a token and chooses the next handler from it. What has to change in each effc branch, and when would you keep the Deep version instead?
- What it tests
- Distinguishing a continuation that keeps its handler from one that does not, and reaching for the right API for a state-machine handler against a scheduler-style handler.
- First step
- Start from what each API hands the branch: a deep continuation still carries its handler, while a shallow one comes back with none. List, branch by branch, what the next perform must find in place, and only then decide where the handler for that next perform is built.
- Follow-ups
-
- Give one specific risk of Effect.Shallow that Effect.Deep does not have, from the behaviour recorded on OCaml 5.5.1.
- How many handler records do you create for a fiber that performs N effects under each API?
- Would you reach for Effect.Shallow for a scheduler that runs thousands of yields a second, and why or why not?
- A tiny program performs Ping three times under each API. What does each version print, and which handler prints the last line?
From the lesson Generators and schedulers with effects
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Senior to staff
A colleague wants their effect scheduler to resume a task after a GTK button-click signal arrives, through caml_callback. Why does OCaml 5.5 not support performing an effect from the signal handler, and what alternative design would you suggest?
- What it tests
- Knowing the official boundary (OCaml 5.2 Changes #12456) between effects and C callbacks, and designing round it rather than fighting it.
- First step
- Trace how the click reaches OCaml: GTK calls a C function, which enters OCaml through caml_callback rather than from inside a fiber of the scheduler. Ask whether a perform made there can reach the scheduler's handler, read what the manual says about the limits of effects, and design from that answer.
- Follow-ups
-
- Which three run-time paths does the Changes entry list for the same reason, besides caml_callback itself?
- How would an existing scheduler such as Eio handle the same GTK signal, and what makes that different?
- If a C library already calls back into OCaml and the callback must trigger a wake-up, what is the minimum the callback does on the C side?
- What would happen on OCaml 5.5 if the callback did perform an effect anyway, and which page of the manual says so?
From the lesson Generators and schedulers with effects
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A service that calls one slow upstream has p99 latency 3 s when the upstream slows down, and p50 under 50 ms when it is healthy. The team writes the call under Eio. How would you keep p99 near the SLO without changing the upstream?
- What it tests
- Reaching for Eio.Time.with_timeout as the first lever, understanding what cancellation does to the slow fiber, and picking a budget that fits the SLO rather than one picked by the slow upstream.
- First step
- Separate what the service controls from what it does not: it cannot make the upstream faster, but it decides how long it waits for it. Work out this hop's share of the latency target, and decide what the caller should get when that share runs out, before choosing the Eio call that enforces it.
- Follow-ups
-
- What has to happen in the slow fiber for cancellation to take effect quickly?
- How would the budget propagate to nested calls the handler also makes?
- What should the fall-back answer look like, and when do you prefer a stale cached value over a 503?
- How would you measure the number of timeouts to tell whether the budget is right?
From the lesson Concurrent I/O with Eio and Lwt
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Mid to senior
A line-based Eio server reads one request a connection with Eio.Buf_read.line, no cap. A client opens a connection and sends 1 MB of bytes with no newline. The process memory climbs. What would you change, and why?
- What it tests
- Spotting that an unbounded line read is an input-validation gap, reaching for Buf_read.of_flow ~max_size:N and knowing where N comes from, and understanding that the handler must catch Buffer_limit_exceeded and reply.
- First step
- Ask what bounds the memory one connection can take while the reader waits for a newline. Find where the buffered reader is created and what limit it was given, decide the longest line the protocol really needs, and plan what the client should receive when a line is longer.
- Follow-ups
-
- How do you choose N for a service whose protocol accepts a few short commands and one long upload?
- What does a load balancer need to see to keep the connection from reaching the service at all?
- How would a timeout help here, in addition to the cap?
- What test would make a weak reader fail and the fixed one pass?
From the lesson Concurrent I/O with Eio and Lwt
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 5 minutes
-
Senior to staff
A team is writing a new HTTP service in OCaml. One engineer wants Eio for direct style; another wants Lwt, because the shared logging library and the client for the next service are both cohttp-lwt. What would you propose?
- What it tests
- Weighing OCaml 5 direct style against an existing Lwt ecosystem rather than choosing by novelty, and knowing what a bridge between the two libraries can and cannot do.
- First step
- List the integration points: logging, the HTTP client to the next service, metrics, the database. For each, note which concurrency library it is built on and whether a maintained version for the other one exists. Then decide which library most of the service's code will live in, and where the rest would have to cross over.
- Follow-ups
-
- What does a bridge between the two libraries cost, and where would you cross it?
- How would you expose a sync-looking API to the service's callers in each case?
- When does rewriting the shared library in Eio pay off, and when does it not?
- What migration path would you plan if you pick Lwt now?
From the lesson Concurrent I/O with Eio and Lwt
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 6 minutes
-
Mid to senior
Your team wrapped the shared counter in Atomic and the lost updates stopped. A later refactor made it a record { mutable count : int [@atomic]; mutable total : int [@atomic] } where every bump increments count and adds the request's amount into total. Under load the two fields disagree again. Where is the race?
- What it tests
- Reading what an atomic word or atomic field actually makes indivisible, and why a step across several fields needs a mutex that covers the whole body.
- First step
- State exactly what one atomic operation makes indivisible, then write one bump out as the separate steps a second domain could observe, including the moment after count changed and before total did. Decide from that what has to cover both fields as a single step.
- Follow-ups
-
- Would combining count and total into one Atomic.t of (int * int) solve it, and why not?
- If the mutex is too heavy, what would a lock-free Saturn queue of events let the design do instead?
- How would you test the race deterministically, so a CI job catches a reintroduction?
From the lesson Parallelism with domains and atomics
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A background worker runs on a domain, and a bug makes it raise Failure "db connection refused" on start-up. The parent program keeps running and never prints the failure; its logs show only the worker never produced any output. Why, and how do you fix it on OCaml 5.5?
- What it tests
- Knowing that a domain propagates an exception only through Domain.join and that 5.5 keeps the worker's backtrace intact; recognising the pattern of a spawn with no corresponding join.
- First step
- Look at where the worker domain is spawned and whether anything ever joins it. Then ask where an exception that ends a domain's function goes, and which call is the only one that hands it to another domain.
- Follow-ups
-
- What would Domain.join print on OCaml 5.5 after the Failure, and what did pre-5.5 miss?
- If the parent must not wait on the worker, how do you still get its failure out?
- How does Domain.join compose with a background logger that depends on the main domain still running?
From the lesson Parallelism with domains and atomics
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A parallel_for_reduce over 10,000,000 numbers runs slower on 16 domains than a plain for loop on one domain, and the call passes ~chunk_size:1. What is going on, and how do you choose a better chunk size?
- What it tests
- Whether the candidate links the chunk size to the number of tasks the pool makes, knows what goes wrong at both ends, and chooses a size by measuring instead of guessing.
- First step
- Count the tasks the call makes. With chunk_size 1 the loop halves the range down to single numbers, so ten million numbers make about ten million tasks; compare what making, queueing and taking one task costs with one iteration of a cheap body. Then plan how you will time each size you try.
- Follow-ups
-
- What goes wrong at the other end, with a chunk so large that the range makes only two chunks?
- How does Domainslib 0.5.2 compute the default chunk size, and when would you override it?
- The body costs more for larger numbers. How does that change your choice?
From the lesson Parallel patterns and race detection
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A team compiles the tests of its OCaml 5 service in a ThreadSanitizer switch and runs them ten times with no report. A colleague says this proves the service has no data race. Is the colleague right, and what would you do next?
- What it tests
- Knowing that ThreadSanitizer reports only the unordered, conflicting accesses a run performs, the other blind spots the manual names, and how to make a hidden race likely to show.
- First step
- Start from what a ThreadSanitizer report is about: the unordered, conflicting accesses one run performed. Ask what ten clean runs say about an interleaving they never produced, and about shared state no test touched, before deciding what the next runs must do differently.
- Follow-ups
-
- What does a ThreadSanitizer report contain, and how do you read its two stack traces?
- What does the instrumentation cost in time and memory, and where would you run it?
- A race sits in a library you cannot change. How do you keep the reports usable?
From the lesson Parallel patterns and race detection
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Senior to staff
A teammate proposes replacing every Mutex-protected Queue in an OCaml 5 service with Saturn's lock-free queue, because lock-free is faster. What would you check before agreeing?
- What it tests
- Whether the candidate weighs contention, progress when a domain stalls, composability, waiting for items and the measured cost on the real workload, instead of assuming that lock-free means faster.
- First step
- Ask what each queue does, and measure both versions on the real workload before changing anything: a lock-free queue never waits for a lock, which is not the same thing as being faster. Then list what each queue's users rely on: two operations that must happen as one, how consumers wait for an item, and whether the queue needs to be shared at all.
- Follow-ups
-
- Where does a lock-free queue clearly win over a mutex?
- The consumers start only after every producer has finished. Is a shared queue needed at all?
- How would you benchmark the two on this service's real workload?
From the lesson Parallel patterns and race detection
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes