2637. Promise Time Limit


Problem Description

The task is to create a wrapper function that controls how long an asynchronous function is allowed to run before it's considered to have taken too long. An asynchronous function, which we'll call fn, is given to us along with a timeout threshold t in milliseconds. We need to return a new function that behaves in the following way:

  • When called, it lets fn run with any arguments passed to it.
  • If fn finishes its task within t milliseconds, the new function should complete with the same result as fn.
  • If fn does not finish within the allotted t milliseconds, the new function should stop waiting for fn and instead return a rejection with the message "Time Limit Exceeded".

This problem combines asynchronous programming with timing control, requiring knowledge of promises and the race condition in asynchronous flows.

Intuition

The intuition behind the solution is to use concurrency in JavaScript Promise operations. We can race two promises against each other: one promise represents the completion of the input function fn, and the other represents the time limit as a timeout. Here's the thinking process:

  1. Start by invoking the fn with its arguments, wrapped in a promise.
  2. Create a timeout promise that will reject with "Time Limit Exceeded" after t milliseconds.
  3. Use Promise.race to run both promises (the function promise and the timeout promise) against each other.
  4. If fn completes first, the race is won by the function promise, and its result is returned.
  5. If fn takes too long and the timeout elapses first, the race is won by the timeout promise, and "Time Limit Exceeded" is returned.

This approach ensures that regardless of what fn is doing, we're not waiting for it longer than t milliseconds, enforcing a strict time limit on its execution.

Solution Approach

The solution makes use of Promises and the race method provided by the Promise API. The idea is to have two promises: one that represents the asynchronous operation fn and another that acts as a timer. Whichever promise settles first determines the outcome. To implement this:

  1. We define a function timeLimit that takes an asynchronous function fn and a timeout value t.
  2. timeLimit returns a new function that accepts any number of arguments (...args).
  3. Inside this new function, we set up a race condition between two promises:
    • The first promise is the result of calling fn(...args). Since fn is asynchronous and returns a promise, it will either resolve with the result of fn or reject if fn fails.
    • The second promise is created with a call to new Promise((_, reject) => setTimeout(() => reject('Time Limit Exceeded'), t)). This promise waits for t milliseconds and then rejects with "Time Limit Exceeded".
  4. We use Promise.race to run these two promises. This method returns a new promise that resolves or rejects as soon as one of the promises in the array resolves or rejects, with the value or reason from that promise.
  5. When calling the race, if fn completes before the timeout, the resulting promise will resolve with the value provided by fn.
  6. If fn does not complete within t milliseconds, the timer promise will reject first, causing the race to end with a rejection.

By using Promise.race, we ensure that our wrapped function never waits longer than t milliseconds to settle. This effectively creates a timeout behavior for any asynchronous operation.

Here's a detailed handling scenario:

  • Assume the function call fn(...args) finishes in less time than t. In this case, Promise.race will resolve to whatever fn(...args) resolves to.
  • On the other hand, if fn(...args) takes longer than t milliseconds, the promise from setTimeout will reject first, causing Promise.race to reject with "Time Limit Exceeded".

This pattern is a common solution in scenarios where a timeout needs to be enforced on asynchronous operations, ensuring that a system remains responsive and does not wait indefinitely for an action to complete.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's say we have an asynchronous function asyncOperation that resolves after a random amount of time, which sometimes might exceed our threshold. We want to ensure that if asyncOperation takes more than 2000 milliseconds (2 seconds), it should be considered as failed due to taking too long. We will use the timeLimit function to enforce this rule.

// Simulate an asynchronous operation that takes a random amount of time to complete,
// sometimes more than 2000 milliseconds.
function asyncOperation(resolveValue) {
  return new Promise((resolve) => {
    const delay = Math.floor(Math.random() * 3000); // Random delay from 0 to 2999 ms
    setTimeout(() => resolve(resolveValue), delay);
  });
}

// The `timeLimit` function wraps around our `asyncOperation`.
const timeLimit = (fn, t) => (...args) => 
  Promise.race([
    fn(...args),
    new Promise((_, reject) => setTimeout(() => reject('Time Limit Exceeded'), t))
  ]);

// Let's create a time-limited version of our `asyncOperation` with a 2000 ms limit.
const limitedAsyncOperation = timeLimit(asyncOperation, 2000);

// Call the time-limited asynchronous function with a sample resolve value.
limitedAsyncOperation('Sample resolve value')
  .then(result => console.log(`Operation successful: ${result}`))
  .catch(error => console.log(`Operation failed: ${error}`));

In this walkthrough:

  1. We define an asynchronous function asyncOperation that would typically represent a more complex async process, such as an API call or a database transaction.
  2. We set up a timeLimit function according to the solution approach.
  3. We create limitedAsyncOperation by passing asyncOperation and the desired timeout of 2000 milliseconds to timeLimit.
  4. When we call limitedAsyncOperation, it initiates two parallel promises:
    • The original asyncOperation promise that resolves after a random delay.
    • A new promise that will reject with "Time Limit Exceeded" after 2000 milliseconds.
  5. Promise.race is used to return the outcome of whichever promise settles first.
    • If asyncOperation completes in less than 2 seconds, its resolve value will be logged to the console.
    • If asyncOperation takes more than 2 seconds, the console will log "Operation failed: Time Limit Exceeded".

The above code demonstrates how the timeLimit function effectively imposes a timeout constraint on an asynchronous operation.

Solution Implementation

1from typing import Callable, Any
2import asyncio
3
4# Define a generic asynchronous function type that returns a Future.
5AsyncFunction = Callable[..., Any]
6
7async def time_limit(async_function: AsyncFunction, time_limit_millis: int) -> AsyncFunction:
8    """
9    Wraps an asynchronous function with a time limit, enforcing it to either
10    resolve or reject within a set timeframe.
11  
12    :param async_function: The asynchronous function to wrap.
13    :param time_limit_millis: The maximum amount of time (in milliseconds) to wait before cancelling.
14    :returns: A function that behaves like the original async function but with a time limit.
15    """
16    async def wrapper(*args, **kwargs):
17        # Use asyncio.wait_for to apply a timeout to the async function
18        try:
19            return await asyncio.wait_for(async_function(*args, **kwargs), time_limit_millis / 1000.0)
20        except asyncio.TimeoutError:
21            # Raise a custom exception if the function times out
22            raise TimeoutError('Time Limit Exceeded')
23
24    return wrapper
25
26# Usage example:
27# Define an asynchronous operation that may take longer than the allocated time limit.
28async def example_async_function(duration):
29    await asyncio.sleep(duration)
30
31# Wrap the async function with a time limit.
32limited_function = time_limit(example_async_function, 100)
33
34# Use the wrapped function with a time-out that exceeds the time limit and handle exceptions.
35async def main():
36    try:
37        # This should raise a TimeoutError after 100ms
38        await limited_function(0.150)
39    except TimeoutError as e:
40        print(e)
41
42# Run the main function to demonstrate usage.
43if __name__ == '__main__':
44    asyncio.run(main())
45
1import java.util.concurrent.*;
2import java.util.function.*;
3
4/**
5 * Represents a generic function that returns a Future.
6 */
7@FunctionalInterface
8interface AsyncFunction<T> {
9    Future<T> apply(Object... params);
10}
11
12/**
13 * Wraps an asynchronous function with a time limit, enforcing it to either
14 * complete or cancel within a set timeframe.
15 *
16 * @param asyncFunction The asynchronous function to wrap.
17 * @param timeLimitMillis The maximum amount of time (in milliseconds) to wait before cancelling.
18 * @param <T> The type of the result provided by the asynchronous function.
19 * @return A function that behaves like the original async function but with a time limit.
20 */
21public static <T> AsyncFunction<T> timeLimit(AsyncFunction<T> asyncFunction, long timeLimitMillis) {
22    // Return a new function that upon invocation, submits the original task to an executor
23    // and applies the timeout.
24    return (Object... args) -> {
25        // Create a new executor to run the asynchronous function
26        ExecutorService executor = Executors.newSingleThreadExecutor();
27      
28        // Submit the original asynchronous function as a callable task to the executor
29        Callable<T> task = () -> {
30            try {
31                return asyncFunction.apply(args).get();
32            } catch (ExecutionException | InterruptedException e) {
33                throw new RuntimeException(e);
34            }
35        };
36      
37        Future<T> future = executor.submit(task);
38
39        // Schedule a task to cancel the future after the time limit
40        Executors.newSingleThreadScheduledExecutor()
41                 .schedule(() -> future.cancel(true), timeLimitMillis, TimeUnit.MILLISECONDS);
42      
43        try {
44            // Return the result of the future, awaiting termination with the given time limit
45            return CompletableFuture.completedFuture(future.get(timeLimitMillis, TimeUnit.MILLISECONDS));
46        } catch (TimeoutException | InterruptedException | ExecutionException e) {
47            // Cancel the future if it times out or encounters an issue
48            future.cancel(true);
49            throw new RuntimeException("Time Limit Exceeded", e);
50        } finally {
51            // Shutdown the executor service to prevent lingering threads
52            executor.shutdown();
53        }
54    };
55}
56
57// Usage example (uncomment to use within a main method or other appropriate context):
58/*
59AsyncFunction<Void> limited = timeLimit(duration -> {
60    CompletableFuture<Void> future = new CompletableFuture<>();
61    new Thread(() -> {
62        try {
63            Thread.sleep((Long) duration);
64            future.complete(null); // Resolve the future upon successful completion
65        } catch (InterruptedException e) {
66            future.completeExceptionally(e);
67        }
68    }).start();
69    return future;
70}, 100);
71
72// Call the wrapped function with a duration that exceeds the limit, catching any exceptions
73try {
74    limited.apply(150).get();
75} catch (Exception e) {
76    System.out.println(e.getMessage());  // Expected output: "Time Limit Exceeded"
77}
78 */
79
1#include <iostream> // For std::cout and std::endl
2#include <future>   // For std::async, std::future, and std::chrono
3#include <functional> // For std::function
4#include <stdexcept> // For std::runtime_error
5#include <chrono> // For std::chrono
6#include <thread> // For std::this_thread::sleep_for
7
8// Define a type alias for a generic function that returns a std::future.
9using AsyncFunction = std::function<std::future<void>(std::vector<int>)>;
10
11/**
12 * Wraps an asynchronous function with a time limit, enforcing it to either
13 * resolve or fail within a set timeframe.
14 *
15 * @param asyncFunction The asynchronous function to wrap.
16 * @param timeLimitMillis The maximum amount of time (in milliseconds) to wait before canceling.
17 * @return A function that behaves like the original async function but with a time limit.
18 */
19auto timeLimit(AsyncFunction asyncFunction, int timeLimitMillis) -> AsyncFunction {
20    // Return a new function which will race the original function against a timeout
21    return [asyncFunction, timeLimitMillis](std::vector<int> args) -> std::future<void> {
22        // Start the async function with the given arguments
23        auto result = asyncFunction(args);
24      
25        // Wait for the result for the given time limit
26        if (result.wait_for(std::chrono::milliseconds(timeLimitMillis)) == std::future_status::timeout) {
27            // If it times out, throw a runtime_error
28            throw std::runtime_error("Time Limit Exceeded");
29        }
30
31        // Otherwise, return the original function's result
32        return result;
33    };
34}
35
36// Usage example:
37// The following lines of code provide a basic example of how the timeLimit function
38// could be used in practice. It defines an asynchronous operation that could take
39// longer than the specified time limit and handles timeout errors.
40
41// Define the actual async operation function
42AsyncFunction myAsyncOperation = [](std::vector<int> duration) -> std::future<void> {
43    return std::async(std::launch::async, [duration]() {
44        // Simulate a long running operation
45        std::this_thread::sleep_for(std::chrono::milliseconds(duration[0]));
46        std::cout << "Operation finished" << std::endl;
47    });
48};
49
50// Use the timeLimit wrapper with the async operation with a timeout of 100ms
51AsyncFunction limitedAsyncOperation = timeLimit(myAsyncOperation, 100);
52
53// Run and catch any timeout errors
54try {
55    limitedAsyncOperation({150}).get(); // Using 150ms for the operation
56} catch (std::runtime_error &e) {
57    // Print out the error message if there's a timeout
58    std::cout << e.what() << std::endl; // Expected output: "Time Limit Exceeded" after t=100ms
59}
60
1// Define a generic function type that returns a Promise.
2type AsyncFunction = (...params: any[]) => Promise<any>;
3
4/**
5 * Wraps an asynchronous function with a time limit, enforcing it to either
6 * resolve or reject within a set timeframe.
7 *
8 * @param asyncFunction The asynchronous function to wrap.
9 * @param timeLimitMillis The maximum amount of time (in milliseconds) to wait before rejecting.
10 * @returns A function that behaves like the original async function but with a time limit.
11 */
12function timeLimit(asyncFunction: AsyncFunction, timeLimitMillis: number): AsyncFunction {
13    // Return a new function which will race the original function against a timeout
14    return async function (...args) {
15        // Use Promise.race to compete the async function call against a timeout
16        return Promise.race([
17            asyncFunction(...args),
18            // Create a new Promise that automatically rejects after timeLimitMillis
19            new Promise((_, reject) => setTimeout(() => reject(new Error('Time Limit Exceeded')), timeLimitMillis)),
20        ]);
21    };
22}
23
24// Usage example:
25// Define an asynchronous operation that may take longer than the allocated time limit.
26// const limited = timeLimit((duration) => new Promise(resolve => setTimeout(resolve, duration)), 100);
27// Use the wrapped operation with a timeout that exceeds the time limit and catch any errors.
28// limited(150).catch(error => console.log(error.message)); // Expected output: "Time Limit Exceeded" at t=100ms
29

Time and Space Complexity

The code defines a function timeLimit that takes another function fn and a time limit t, and returns a new function that will reject the promise if it doesn’t resolve within time t. The computational complexities are as follow:

Time Complexity

The time complexity of the timeLimit function itself is O(1) (constant time), as it simply sets up a Promise.race() construct without any loops or recursive calls.

However, the time complexity of the resulting function when called is determined by fn, which is an input parameter to timeLimit. Since fn could be any function, its complexity can vary. When this resulting function is called, it will execute fn(...args) and setTimeout(..., t) concurrently, and the Promise.race() will settle as soon as the first promise settles.

Thus, the overall time complexity of the resulting function is O(f(n)) where O(f(n)) represents the time complexity of the function fn that is passed to timeLimit.

Space Complexity

The space complexity of the timeLimit function is O(1). It does not utilize any additional space that grows with the input size, so it uses constant space.

The space complexity of the resulting function when it is called with a specific fn is determined by the space that fn uses. If fn uses space that grows with the input, then the resulting function will also have a space complexity that reflects that growth. However, since we do not have specifics on what fn does, we denote the space complexity of the function as O(g(n)), where O(g(n)) represents the space complexity of fn.

In addition to the space used by fn, the resulting function uses space for the Promise.race() and the setTimeout. However, this additional space does not grow with the input and is thus considered constant, not affecting the overall space complexity which remains O(g(n)).


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!