Katkat

Implement a Callback Signaling Registry with Register, Unregister and Signal

Candid HealthSoftware Engineer · Technical Screen

Last 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 of signal.
  • 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 signal pass 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_callback call remove?
  • What should unregister_callback do 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 None returns 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 set or dict design
  • 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 signal returns None
  • 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 signal for 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 signal call 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?