Implement a Callback Signaling Registry with Register, Unregister and Signal
Candid HealthSoftware Engineer · Technical ScreenLast updated
Implement a minimal in-process signaling system with register_callback, unregister_callback and signal functions that all return None. Tests requirement clarification, callback storage and hashability, execution order, isolation of failing callbacks, and prevention of reentrant signal loops.
Implement a small in-process signaling system, essentially a minimal publish/subscribe registry: callbacks subscribe to a signal id, and signaling that id runs them. The system exposes three functions, and each of them must return None:
def register_callback(id, callback) -> None: ...
def unregister_callback(id, callback) -> None: ...
def signal(id) -> None: ...
A callback is any Python callable (Callable). Intended usage:
register_callback(10, fn_foo)
register_callback(10, fn_bar)
signal(10) # runs fn_foo and fn_bar
unregister_callback(10, fn_bar)
signal(10) # runs only fn_foo
signal(200) # nothing happens
The prompt is deliberately open-ended. The code itself is short; what is evaluated is whether you ask clarifying questions before coding, surface the failure modes and edge cases yourself, and defend the behavior you choose for each. The interviewer's follow-ups target exactly those choices.
Constraints and Clarifications
- All three functions return
None. Values returned by callbacks are not passed back to the caller ofsignal. - Signaling an id that has no registered callbacks does nothing.
- Assume a single-threaded Python program unless the interviewer says otherwise.
Clarifying Questions
- What arguments, if any, does
signalpass to the callbacks? - If the same callback is registered twice for the same id, should it run twice, and how many registrations should one
unregister_callbackcall remove? - What should
unregister_callbackdo when the id or the callback was never registered: ignore the call or report an error?
Part 1 — Registry API and storage
Implement the three functions. Explain how you store the callbacks for each id. Could you put them directly into a dict or a set? What would that require of the callbacks, and which kinds of callables would break it?
What This Part Should Cover
- The three functions with the required
Nonereturns and the no-op behavior from the example - A per-id container and the cost of register, unregister and signal with it
- Hashability and equality of callables, and the kinds of callables that break a
setordictdesign - Duplicate registrations, and unregistering something that is not registered
Part 2 — Execution order and failing callbacks
Must the callbacks run in the order in which they were registered? If one callback raises an exception, should the remaining callbacks for that signal still run, and what should the caller of signal observe?
What This Part Should Cover
- A stated ordering guarantee and a data structure that actually provides it
- An exception policy (stop, continue, or continue and report) with its trade-off
- Where failures surface, given that
signalreturnsNone - Which exceptions, if any, should not be caught
Part 3 — Reentrant signals
A callback may itself call signal(), either for the id that is being dispatched or for another id whose callbacks signal back. That can recurse without end. How do you detect and prevent such reentrant loops?
Clarifying Questions for this Part
- Is a nested
signalfor a different id allowed, or is every nested call an error? - When a loop is detected, should the nested call be dropped, deferred until the current dispatch finishes, or reported as an error?
What This Part Should Cover
- How reentrancy is detected, including indirect cycles across ids
- The policy applied when a loop is detected, and why
- An argument that every top-level
signalcall terminates, and tests that show it
Hints
Hint 1
Finding the registration again: `unregister_callback` receives a callable and must find the matching registration. Consider which kinds of callables make that lookup fail with each container you might choose, for example `obj.method` passed once to register and again to unregister.
Hint 2
One broken subscriber: Think about what the other subscribers would expect when an unrelated callback ahead of them fails, and where that failure can become visible when `signal` returns nothing.
Hint 3
What is in flight: Consider what the registry would need to remember so that a nested `signal` call can be told apart from a top-level one.
Hint 4
Not every nested signal is a bug: A callback for one id signaling a different id can be legitimate. Decide which nested calls to allow, and how to bound the rest.
Follow-up questions
- A callback unregisters itself, or registers a new callback for the same id, while `signal` is running the callbacks. What does your implementation do, and what should it do?
- How would you make the registry safe to use from several threads without deadlocking when a callback calls back into it?
- The registry holds strong references to callbacks, which keeps the objects behind bound methods alive. How would you avoid that leak, and what would break?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.