2715. Timeout Cancellation
Problem Description
This problem asks you to create a function that can schedule another function to run after a delay, but with the ability to cancel that scheduled execution.
You need to implement a function cancellable
that takes three parameters:
fn
: A function to be executedargs
: An array of arguments to pass tofn
t
: A delay in milliseconds beforefn
should be executed
The cancellable
function should return a cancel function (cancelFn
). Here's how it works:
- When
cancellable
is called, it schedulesfn
to be executed with the providedargs
aftert
milliseconds - The returned
cancelFn
can be called to cancel the scheduled execution - If
cancelFn
is called before the delayt
expires, the execution offn
is cancelled andfn
never runs - If
cancelFn
is not called within the delayt
, thenfn
executes normally with the provided arguments
For example, if you schedule a function to run after 20ms but call the cancel function after 10ms, the original function never executes. However, if you schedule a function to run after 20ms and call the cancel function after 50ms, the original function will have already executed at the 20ms mark.
The solution uses JavaScript's setTimeout
to schedule the delayed execution and clearTimeout
to cancel it if needed. When cancellable
is called, it creates a timer that will execute fn(...args)
after t
milliseconds and returns a function that, when called, clears this timer using clearTimeout(timer)
.
Intuition
The key insight here is recognizing that JavaScript provides built-in timer functions that perfectly match our requirements. When we need to delay execution of a function, setTimeout
is the natural choice - it schedules a function to run after a specified delay and returns a timer ID.
The challenge is creating a cancellation mechanism. Fortunately, JavaScript's clearTimeout
function allows us to cancel a scheduled timer using its ID. This creates a clean pattern: store the timer ID when scheduling the function, then provide a way to call clearTimeout
with that ID.
The solution emerges from understanding closures in JavaScript. When we create the cancel function inside cancellable
, it has access to the timer
variable even after cancellable
returns. This is crucial because:
- We need to schedule the function immediately when
cancellable
is called - We need to return a cancel function that can be invoked later
- That cancel function needs to know which timer to cancel
By storing the timer ID in a variable and returning a function that references it, we create a closure that preserves the connection between the scheduled execution and its cancellation mechanism. The returned function simply calls clearTimeout(timer)
when invoked, which either cancels the pending execution (if called before t
milliseconds) or does nothing (if the function has already executed).
This approach is elegant because it leverages JavaScript's event loop and timer system rather than trying to implement custom timing logic. The entire cancellation mechanism is just two lines: creating the timer with setTimeout
and returning a function that clears it.
Solution Approach
The implementation is straightforward and leverages JavaScript's timer API:
-
Create a Timer: When
cancellable
is called, we immediately create a timer usingsetTimeout
:const timer = setTimeout(() => fn(...args), t);
This schedules the function
fn
to be executed with the spread arguments...args
aftert
milliseconds. ThesetTimeout
function returns a timer ID that we store in thetimer
variable. -
Return a Cancel Function: We return an arrow function that can cancel the scheduled execution:
return () => { clearTimeout(timer); };
This returned function captures the
timer
variable through closure. When called, it invokesclearTimeout(timer)
to cancel the scheduled execution.
The solution uses the following key patterns:
-
Closure Pattern: The returned cancel function maintains access to the
timer
variable even aftercancellable
finishes executing. This allows the cancel function to reference the correct timer ID when it's called later. -
Arrow Function with Spread Operator: The expression
() => fn(...args)
creates a function that, when executed by the timer, callsfn
with all the arguments from theargs
array spread out as individual parameters. -
Timer Management: The solution relies on the browser/Node.js event loop to handle the timing.
setTimeout
adds the function execution to the event queue after the specified delay, whileclearTimeout
removes it from the queue if it hasn't executed yet.
The beauty of this solution is its simplicity - it directly maps the problem requirements to JavaScript's built-in timer functions without any additional complexity. If clearTimeout
is called before the timer expires, the scheduled function never runs. If the timer has already expired and the function has executed, calling clearTimeout
has no effect (it's a no-op for expired or invalid timer IDs).
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works:
Scenario: We want to log a message after 100ms, but we might need to cancel it.
// Define our function to be delayed
const logMessage = (name, age) => {
console.log(`${name} is ${age} years old`);
};
// Schedule the function to run after 100ms
const cancelFn = cancellable(logMessage, ["Alice", 25], 100);
What happens internally:
cancellable
is called with our function, arguments, and delay- Inside
cancellable
, a timer is created:const timer = setTimeout(() => logMessage("Alice", 25), 100); // timer might be assigned ID: 123
- A cancel function is returned that knows about timer ID 123
Case 1: Cancelling before execution (at 50ms)
// After 50ms, we decide to cancel
setTimeout(() => {
cancelFn(); // Calls clearTimeout(123)
}, 50);
- At 50ms:
cancelFn()
executes, callingclearTimeout(123)
- The scheduled function is removed from the event queue
- At 100ms: Nothing happens - the message is never logged
Case 2: Cancelling after execution (at 150ms)
// After 150ms, we try to cancel (too late)
setTimeout(() => {
cancelFn(); // Calls clearTimeout(123) but timer already fired
}, 150);
- At 100ms: Timer expires,
logMessage("Alice", 25)
executes - Console shows: "Alice is 25 years old"
- At 150ms:
cancelFn()
executes but has no effect (timer already completed)
The key insight is that the cancel function maintains a reference to the timer ID through closure, allowing it to cancel the exact timer that was created, even when called much later.
Solution Implementation
1import time
2import threading
3from typing import Callable, List, Any, Dict
4
5def cancellable(fn: Callable, args: List[Any], t: int) -> Callable:
6 """
7 Creates a cancellable delayed function execution
8
9 Args:
10 fn: The function to be executed after the delay
11 args: Arguments to be passed to the function
12 t: Delay in milliseconds before executing the function
13
14 Returns:
15 A cancel function that when called, prevents the delayed execution
16 """
17 # Convert milliseconds to seconds for Python's threading.Timer
18 delay_seconds = t / 1000.0
19
20 # Create a timer to execute the function after the specified delay
21 timer = threading.Timer(delay_seconds, fn, args)
22 timer.start()
23
24 # Return a cancel function that stops the timer
25 def cancel():
26 timer.cancel()
27
28 return cancel
29
30
31# Example usage:
32if __name__ == "__main__":
33 result = []
34
35 # Define a simple function that multiplies input by 5
36 def fn(x: int) -> int:
37 return x * 5
38
39 args = [2]
40 delay_time = 20 # Delay before function execution (in milliseconds)
41 cancel_time = 50 # Time when cancel is called (in milliseconds)
42
43 # Record the start time for measuring elapsed time
44 start_time = time.perf_counter() * 1000 # Convert to milliseconds
45
46 # Wrapper function that logs the execution time and result
47 def log(*args_arr):
48 elapsed_time = int(time.perf_counter() * 1000 - start_time)
49 result.append({"time": elapsed_time, "returned": fn(*args_arr)})
50
51 # Create a cancellable execution of the log function
52 cancel = cancellable(log, args, delay_time)
53
54 # Determine the maximum time to wait before checking results
55 max_time = max(delay_time, cancel_time)
56
57 # Schedule the cancel function to be called after cancel_time
58 cancel_timer = threading.Timer(cancel_time / 1000.0, cancel)
59 cancel_timer.start()
60
61 # Check and log the results after ensuring all operations are complete
62 time.sleep((max_time + 15) / 1000.0)
63 print(result) # [{"time": 20, "returned": 10}]
64
1import java.util.*;
2import java.util.concurrent.*;
3import java.util.function.Function;
4
5/**
6 * Solution class for creating cancellable delayed function execution
7 */
8class Solution {
9
10 /**
11 * Creates a cancellable delayed function execution
12 * @param fn - The function to be executed after the delay
13 * @param args - Arguments to be passed to the function
14 * @param t - Delay in milliseconds before executing the function
15 * @return A Runnable that when called, prevents the delayed execution
16 */
17 public static Runnable cancellable(Runnable fn, Object[] args, int t) {
18 // Create a scheduled executor service for delayed execution
19 ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
20
21 // Schedule the function to execute after the specified delay
22 ScheduledFuture<?> future = executor.schedule(() -> {
23 // Execute the function with provided arguments
24 fn.run();
25 }, t, TimeUnit.MILLISECONDS);
26
27 // Return a cancel function that cancels the scheduled task
28 return () -> {
29 future.cancel(true);
30 executor.shutdown();
31 };
32 }
33
34 /**
35 * Example usage demonstration
36 */
37 public static void main(String[] args) throws InterruptedException {
38 // List to store results with time and returned value
39 List<Map<String, Object>> result = new ArrayList<>();
40
41 // Define a simple function that multiplies input by 5
42 Function<Integer, Integer> fn = x -> x * 5;
43 Object[] fnArgs = {2};
44 int delayTime = 20; // Delay before function execution in milliseconds
45 int cancelTime = 50; // Time when cancel is called in milliseconds
46
47 // Record the start time for measuring elapsed time
48 long startTime = System.currentTimeMillis();
49
50 // Wrapper runnable that logs the execution time and result
51 Runnable log = () -> {
52 // Calculate elapsed time since start
53 long elapsedTime = System.currentTimeMillis() - startTime;
54
55 // Execute the function and store result
56 int returnedValue = fn.apply((Integer) fnArgs[0]);
57
58 // Create a map to store time and returned value
59 Map<String, Object> entry = new HashMap<>();
60 entry.put("time", elapsedTime);
61 entry.put("returned", returnedValue);
62
63 // Add the entry to results list
64 result.add(entry);
65 };
66
67 // Create a cancellable execution of the log function
68 Runnable cancel = cancellable(log, fnArgs, delayTime);
69
70 // Determine the maximum time to wait before checking results
71 int maxTime = Math.max(delayTime, cancelTime);
72
73 // Schedule the cancel function to be called after cancelTime
74 ScheduledExecutorService cancelExecutor = Executors.newSingleThreadScheduledExecutor();
75 cancelExecutor.schedule(() -> {
76 cancel.run();
77 }, cancelTime, TimeUnit.MILLISECONDS);
78
79 // Wait and check the results after ensuring all operations are complete
80 Thread.sleep(maxTime + 15);
81
82 // Print the results
83 System.out.println(result); // [{time=20, returned=10}]
84
85 // Shutdown the cancel executor
86 cancelExecutor.shutdown();
87 }
88}
89
1#include <iostream>
2#include <functional>
3#include <thread>
4#include <chrono>
5#include <atomic>
6#include <vector>
7#include <any>
8#include <mutex>
9
10/**
11 * A class that provides cancellable delayed function execution
12 */
13class Cancellable {
14private:
15 std::atomic<bool> cancelled{false};
16 std::thread timerThread;
17
18public:
19 /**
20 * Creates a cancellable delayed function execution
21 * @param fn - The function to be executed after the delay
22 * @param delayMs - Delay in milliseconds before executing the function
23 * @returns A cancel function that when called, prevents the delayed execution
24 */
25 template<typename Func, typename... Args>
26 std::function<void()> cancellable(Func&& fn, int delayMs, Args&&... args) {
27 // Create a lambda that captures the function and arguments
28 auto task = [this, fn, args...]() {
29 // Sleep for the specified delay
30 std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
31
32 // Check if the execution was cancelled
33 if (!cancelled.load()) {
34 // Execute the function with the provided arguments
35 fn(args...);
36 }
37 };
38
39 // Start the timer thread
40 timerThread = std::thread(task);
41
42 // Return a cancel function that sets the cancelled flag
43 return [this]() {
44 cancelled.store(true);
45 if (timerThread.joinable()) {
46 timerThread.join();
47 }
48 };
49 }
50
51 ~Cancellable() {
52 // Ensure thread is properly joined on destruction
53 if (timerThread.joinable()) {
54 timerThread.join();
55 }
56 }
57};
58
59/**
60 * Example usage demonstrating the cancellable function
61 */
62void exampleUsage() {
63 // Structure to store execution results
64 struct Result {
65 int time;
66 int returned;
67 };
68
69 std::vector<Result> result;
70 std::mutex resultMutex;
71
72 // Define a simple function that multiplies input by 5
73 auto fn = [](int x) -> int {
74 return x * 5;
75 };
76
77 int args = 2;
78 int delayTime = 20; // Delay before function execution in milliseconds
79 int cancelTime = 50; // Time when cancel is called in milliseconds
80
81 // Record the start time for measuring elapsed time
82 auto startTime = std::chrono::steady_clock::now();
83
84 // Wrapper function that logs the execution time and result
85 auto log = [&result, &resultMutex, &fn, &startTime](int x) -> void {
86 // Calculate elapsed time since start
87 auto currentTime = std::chrono::steady_clock::now();
88 auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(
89 currentTime - startTime).count();
90
91 // Store the result with thread safety
92 std::lock_guard<std::mutex> lock(resultMutex);
93 result.push_back({static_cast<int>(elapsedTime), fn(x)});
94 };
95
96 // Create a cancellable execution of the log function
97 Cancellable cancellableObj;
98 auto cancel = cancellableObj.cancellable(log, delayTime, args);
99
100 // Determine the maximum time to wait before checking results
101 int maxTime = std::max(delayTime, cancelTime);
102
103 // Schedule the cancel function to be called after cancelTime
104 std::thread cancelThread([&cancel, cancelTime]() {
105 std::this_thread::sleep_for(std::chrono::milliseconds(cancelTime));
106 cancel();
107 });
108
109 // Check and log the results after ensuring all operations are complete
110 std::this_thread::sleep_for(std::chrono::milliseconds(maxTime + 15));
111
112 // Print the results
113 std::cout << "[";
114 for (size_t i = 0; i < result.size(); ++i) {
115 std::cout << "{\"time\":" << result[i].time
116 << ",\"returned\":" << result[i].returned << "}";
117 if (i < result.size() - 1) {
118 std::cout << ",";
119 }
120 }
121 std::cout << "]" << std::endl; // Expected: [{"time":20,"returned":10}]
122
123 // Clean up threads
124 if (cancelThread.joinable()) {
125 cancelThread.join();
126 }
127}
128
129int main() {
130 exampleUsage();
131 return 0;
132}
133
1/**
2 * Creates a cancellable delayed function execution
3 * @param fn - The function to be executed after the delay
4 * @param args - Arguments to be passed to the function
5 * @param t - Delay in milliseconds before executing the function
6 * @returns A cancel function that when called, prevents the delayed execution
7 */
8function cancellable(fn: Function, args: any[], t: number): Function {
9 // Set a timer to execute the function after the specified delay
10 const timerId = setTimeout(() => fn(...args), t);
11
12 // Return a cancel function that clears the timer
13 return () => {
14 clearTimeout(timerId);
15 };
16}
17
18/**
19 * Example usage:
20 *
21 * const result: Array<{time: number, returned: any}> = [];
22 *
23 * // Define a simple function that multiplies input by 5
24 * const fn = (x: number): number => x * 5;
25 * const args = [2];
26 * const delayTime = 20; // Delay before function execution
27 * const cancelTime = 50; // Time when cancel is called
28 *
29 * // Record the start time for measuring elapsed time
30 * const startTime = performance.now();
31 *
32 * // Wrapper function that logs the execution time and result
33 * const log = (...argsArr: any[]): void => {
34 * const elapsedTime = Math.floor(performance.now() - startTime);
35 * result.push({"time": elapsedTime, "returned": fn(...argsArr)});
36 * };
37 *
38 * // Create a cancellable execution of the log function
39 * const cancel = cancellable(log, args, delayTime);
40 *
41 * // Determine the maximum time to wait before checking results
42 * const maxTime = Math.max(delayTime, cancelTime);
43 *
44 * // Schedule the cancel function to be called after cancelTime
45 * setTimeout(() => {
46 * cancel();
47 * }, cancelTime);
48 *
49 * // Check and log the results after ensuring all operations are complete
50 * setTimeout(() => {
51 * console.log(result); // [{"time":20,"returned":10}]
52 * }, maxTime + 15);
53 */
54
Time and Space Complexity
Time Complexity: O(1)
The cancellable
function performs a constant number of operations:
- One call to
setTimeout
which schedules a function execution - Returns an arrow function that contains a single
clearTimeout
call
All these operations execute in constant time regardless of input size. The setTimeout
itself only schedules the function and doesn't execute it immediately, so its scheduling operation is O(1)
.
Space Complexity: O(1)
The space usage is constant:
- One timer ID stored in the
timer
variable - One closure created for the returned cancel function that captures the
timer
variable - The function reference and arguments are stored but not copied (they're passed by reference)
The space doesn't grow with the size of the input. Even though args
could be an array of any size, the function only stores a reference to it rather than creating a copy, so the space complexity remains constant with respect to the function itself.
Common Pitfalls
1. Thread Safety and Race Conditions
The most significant pitfall in the Python implementation is the lack of thread safety when multiple threads might access shared resources. If the fn
function modifies shared state or if multiple cancellable functions are created and managed simultaneously, race conditions can occur.
Problem Example:
shared_counter = 0
def increment():
global shared_counter
temp = shared_counter
time.sleep(0.001) # Simulate some processing
shared_counter = temp + 1
# Creating multiple cancellable functions
cancels = []
for i in range(10):
cancel = cancellable(increment, [], 10)
cancels.append(cancel)
Solution: Use thread-safe mechanisms like locks when dealing with shared resources:
import threading
lock = threading.Lock()
shared_counter = 0
def increment():
global shared_counter
with lock:
temp = shared_counter
time.sleep(0.001)
shared_counter = temp + 1
2. Timer Already Expired Before Cancel
Calling cancel()
after the timer has already executed doesn't raise an error, but developers might incorrectly assume the function didn't execute. This silent failure can lead to unexpected behavior.
Problem Example:
def important_operation():
print("Operation executed!")
# Perform critical operation
cancel = cancellable(important_operation, [], 100)
time.sleep(0.2) # Wait 200ms
cancel() # This does nothing - operation already executed at 100ms
Solution: Track execution state and provide feedback:
def cancellable_with_status(fn, args, t):
delay_seconds = t / 1000.0
executed = {'status': False}
def wrapper():
executed['status'] = True
fn(*args)
timer = threading.Timer(delay_seconds, wrapper)
timer.start()
def cancel():
success = timer.cancel()
if not success and executed['status']:
print("Warning: Function already executed")
return success
return cancel
3. Resource Cleanup and Memory Leaks
Creating many timers without proper cleanup can lead to resource leaks, especially in long-running applications. Even cancelled timers consume resources until they're garbage collected.
Problem Example:
# Creating thousands of timers in a loop
for i in range(10000):
cancel = cancellable(lambda x: x * 2, [i], 60000) # 60 second delay
# Forgetting to cancel unused timers
Solution: Implement a context manager or ensure proper cleanup:
class CancellableManager:
def __init__(self):
self.active_timers = []
def create_cancellable(self, fn, args, t):
delay_seconds = t / 1000.0
timer = threading.Timer(delay_seconds, fn, args)
timer.start()
self.active_timers.append(timer)
def cancel():
if timer in self.active_timers:
timer.cancel()
self.active_timers.remove(timer)
return cancel
def cleanup_all(self):
for timer in self.active_timers:
timer.cancel()
self.active_timers.clear()
4. Precision Issues with Small Delays
Python's threading.Timer
isn't designed for high-precision timing. For very small delays (< 10ms), the actual execution time might vary significantly from the requested delay.
Problem Example:
# Expecting precise 1ms delay cancel = cancellable(print_time, [], 1) # 1ms delay # Actual execution might happen after 5-15ms due to thread scheduling
Solution: For high-precision timing requirements, consider using alternative approaches:
import asyncio
async def cancellable_async(fn, args, t):
task = asyncio.create_task(delayed_execution(fn, args, t))
async def delayed_execution(fn, args, t):
await asyncio.sleep(t / 1000.0)
fn(*args)
def cancel():
task.cancel()
return cancel
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!