Implement sequential and parallel URL requests
AtlassianSoftware Engineer · Technical ScreenLast 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:
- Concurrency Control: At most
max_concurrencynetwork requests may be active at any given time. Ifmax_concurrency == 1, requests run sequentially. - Deduplication: If a URL appears multiple times in the input list,
fetch_fnmust be invoked only once for that URL. Subsequent occurrences must share the same result (whether success or failure) without triggering additional requests. - Order Preservation: The returned list must match the exact index ordering of the input
urlslist. - 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,0001 <= max_concurrency <= 100fetch_fnis an asynchronous function that takes astrURL and returns astrpayload or raises anException.- 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
urlslist should return an empty list immediately. urlswhere all elements are identical.max_concurrencygreater 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?
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.