Katkat

Implement a Bounded Per-Key Ordered Task Executor

MicrosoftSoftware Engineer · Technical Screen

Last updated

Design and implement a thread-safe, bounded task executor that guarantees sequential execution per key while enforcing a global concurrency limit across all keys.

Problem Statement

Design and implement a thread-safe executor, KeyedTaskExecutor, that executes submitted tasks asynchronously subject to strict concurrency and ordering invariants:

  1. Per-Key Mutual Exclusion & Ordering: Tasks associated with the same key must never run concurrently. Tasks for a given key must execute strictly in the order they were admitted.
  2. Global Concurrency Bound: At most max_workers (e.g., 4) tasks across all keys may execute concurrently at any point in time.
  3. Bounded Capacity: The executor accepts at most max_capacity total pending tasks across all keys. Submissions exceeding this capacity must be rejected immediately.
  4. Task Cancellation: Pending (non-running) tasks can be cancelled via a task handle or identifier.
  5. Graceful Shutdown: The executor must support a shutdown(wait=True, timeout=None) method. Once shut down, new submissions are rejected. If wait=True, the executor waits up to timeout seconds for in-flight tasks to complete.

Interface

from typing import Callable, Any, Optional

class TaskRejectedException(Exception):
    pass

class TaskHandle:
    def result(self, timeout: Optional[float] = None) -> Any:
        """Blocks until completion and returns value or raises task exception."""
        pass

    def cancel(self) -> bool:
        """Attempts to cancel task. Returns True if cancelled before execution started."""
        pass

class KeyedTaskExecutor:
    def __init__(self, max_workers: int = 4, max_capacity: int = 100):
        pass

    def submit(self, key: str, fn: Callable[[], Any]) -> TaskHandle:
        """Atomically admits a task for `key`. Raises TaskRejectedException if capacity is reached or executor is shut down."""
        pass

    def shutdown(self, wait: bool = True, timeout: Optional[float] = None) -> bool:
        """Stops accepting tasks and waits for active/queued tasks up to `timeout`. Returns True if all finished."""
        pass

Example

executor = KeyedTaskExecutor(max_workers=2, max_capacity=10)

# Submissions
h1 = executor.submit("user_1", task_a) # Starts immediately (worker 1)
h2 = executor.submit("user_2", task_b) # Starts immediately (worker 2)
h3 = executor.submit("user_1", task_c) # Queued: user_1 already running
h4 = executor.submit("user_3", task_d) # Queued: max_workers reached

# When task_a finishes, user_1 becomes eligible again.
# task_c runs next on user_1, maintaining FIFO order for user_1.

Constraints & Edge Cases

  • 1 <= max_workers <= 64
  • 1 <= max_capacity <= 100,000
  • Task execution may raise arbitrary exceptions; failure of one task must not stall subsequent tasks for that key or leak worker threads.
  • Empty keys or keys whose queues drain completely must clean up their internal state to prevent memory leaks.
  • Submissions and cancellations must be thread-safe.

Hints

Hint 1

Separate the per-key waiting queues from a global 'ready queue' that holds keys currently eligible to run but waiting for an available global worker slot.

Hint 2

When a worker finishes executing a task for key K, it should inspect key K's queue: if more tasks remain, K can either stay active or re-enter the ready queue depending on worker availability.

Hint 3

Protect queue transitions and admission checks with a single lock or dedicated state machine, and use condition variables to coordinate worker threads and shutdown deadlines.

Follow-up questions

  • How would you prevent a key with an enormous task queue from starving other keys of global worker threads (fairness)?
  • How would you support dynamic scaling of `max_workers` and `max_capacity` at runtime without restarting the executor?
  • If this executor were distributed across multiple nodes instead of threads, what synchronization primitives would you use to enforce per-key serialization and global bounds?