Katkat

Implement sequential and parallel URL requests

AtlassianSoftware Engineer · Technical Screen

Last updated

Implement an asynchronous batch URL fetcher that executes requests with bounded concurrency, deduplicates network calls, preserves input order, and handles per-request errors gracefully.

Problem Statement

Design and implement an asynchronous URL fetching utility that downloads content from a list of URLs with a specified concurrency limit.

Your implementation must meet the following requirements:

  1. Concurrency Control: At most max_concurrency network requests may be active at any given time. If max_concurrency == 1, requests run sequentially.
  2. Deduplication: If a URL appears multiple times in the input list, fetch_fn must be invoked only once for that URL. Subsequent occurrences must share the same result (whether success or failure) without triggering additional requests.
  3. Order Preservation: The returned list must match the exact index ordering of the input urls list.
  4. Isolated Error Handling: If a request to a URL throws an exception, it should not fail the entire batch. Instead, the error must be captured in the output object for that URL, allowing remaining requests to proceed.

Interface

Implement the function fetch_urls:

from typing import Any, Awaitable, Callable, Dict, List

async def fetch_urls(
    urls: List[str],
    fetch_fn: Callable[[str], Awaitable[str]],
    max_concurrency: int = 3
) -> List[Dict[str, Any]]:
    """
    Fetches content for a list of URLs with concurrency control and deduplication.

    Returns a list of dicts corresponding to each URL in input order:
    {
        "url": str,
        "status": "success" | "error",
        "data": str | None,
        "error": str | None
    }
    """
    pass

Constraints

  • 0 <= len(urls) <= 10,000
  • 1 <= max_concurrency <= 100
  • fetch_fn is an asynchronous function that takes a str URL and returns a str payload or raises an Exception.
  • Memory usage should be proportional to the number of unique URLs and results, not unbounded.

Examples

Example 1

# Input
urls = ["https://api.example.com/1", "https://api.example.com/2", "https://api.example.com/1"]
max_concurrency = 2
# fetch_fn("https://api.example.com/1") -> "data 1"
# fetch_fn("https://api.example.com/2") -> raises ConnectionError("Timeout")

# Output
[
    {"url": "https://api.example.com/1", "status": "success", "data": "data 1", "error": None},
    {"url": "https://api.example.com/2", "status": "error", "data": None, "error": "Timeout"},
    {"url": "https://api.example.com/1", "status": "success", "data": "data 1", "error": None}
]
# Explanation: fetch_fn is invoked only twice (once for url 1, once for url 2).

Edge Cases to Consider

  • Empty urls list should return an empty list immediately.
  • urls where all elements are identical.
  • max_concurrency greater than the number of unique URLs.
  • All requests fail or all requests succeed.

Hints

Hint 1

Identify the unique URLs first, or use an in-flight promise/task cache to avoid duplicate calls to fetch_fn.

Hint 2

Use an asyncio.Semaphore (or equivalent language primitive) to limit the number of simultaneous active requests to max_concurrency.

Hint 3

Map the results of unique URLs back to the original indices in the input list to guarantee preserved order.

Follow-up questions

  • How would you add per-request retry logic with exponential backoff for transient HTTP status codes (e.g., 429, 503)?
  • How would you support an overall batch timeout or cancel outstanding tasks if the caller aborts?
  • If results can be large binary streams instead of strings, how would you design this to avoid excessive memory pressure?