LeetCode 2715 - Timeout Cancellation
This problem asks us to implement a cancellable delayed function execution mechanism. Essentially, you are given a function fn, an array of arguments args, and a timeout t in milliseconds.
Difficulty: 🟢 Easy
Topics: —
Solution
Problem Understanding
This problem asks us to implement a cancellable delayed function execution mechanism. Essentially, you are given a function fn, an array of arguments args, and a timeout t in milliseconds. Your task is to return a cancel function cancelFn that, when invoked, can prevent the execution of fn if it has not already occurred.
The function fn is initially scheduled to execute after a delay of t milliseconds. If cancelFn is called before that delay elapses, the execution of fn must be canceled. If cancelFn is called after fn has already executed, it should have no effect. The inputs guarantee that fn is a valid function and args is an array with 1 to 10 elements, while t and cancelTimeMs are within practical ranges, so the function will only need to handle small delays.
Important edge cases include calling cancelFn exactly at the moment of execution, or cancelFn being scheduled with a delay greater than or less than t. These cases test that the mechanism correctly handles timing without prematurely invoking fn or failing to cancel it.
The output is technically the result of executing fn at the proper time, or nothing if canceled. This can be represented by returning the cancel function and allowing external logic (like setTimeout) to invoke it.
Approaches
A brute-force approach would involve repeatedly checking the time to see if t has elapsed and whether cancelFn has been called. This could involve busy-waiting in a loop, checking timestamps continuously, and then executing fn if the cancel flag is not set. While it is correct in theory, it is inefficient and unreliable in practice due to CPU usage and timing inaccuracy.
The optimal approach leverages JavaScript or equivalent asynchronous scheduling mechanisms like setTimeout. The insight is that JavaScript already provides delayed execution through timers, so we can simply schedule fn to execute after t milliseconds and store the timeout handle. If cancelFn is called before the timeout fires, we clear the timer to prevent execution. This approach is precise, efficient, and leverages built-in event loop mechanics.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(t) | O(1) | Repeatedly check time until t or cancel, inefficient |
| Optimal | O(1) | O(1) | Use a timer to schedule fn, cancel via clear timeout |
Algorithm Walkthrough
- Define a function
cancellablethat takesfn,args, andtas inputs. This will return the cancel function. - Inside
cancellable, schedulefnto execute aftertmilliseconds using a timer (setTimeoutin JS,threading.Timerin Python,time.AfterFuncin Go). - Store the handle or reference to the timer. This allows us to cancel it later.
- Define
cancelFnas a function that clears the scheduled timer if it exists. If the timer has already fired,fnhas already executed, so the cancellation does nothing. - Return
cancelFnfromcancellableso that external code can invoke it according tocancelTimeMs.
Why it works: The invariant is that the timer is only active until either t milliseconds elapse or cancelFn is invoked. By leveraging the timer abstraction, we guarantee that fn executes exactly once if not canceled and never executes if canceled in time.
Python Solution
import threading
from typing import Any, Callable
def cancellable(fn: Callable, args: list[Any], t: int) -> Callable[[], None]:
# Schedule the function to execute after t milliseconds
timer = threading.Timer(t / 1000, lambda: fn(*args))
timer.start()
# Define the cancel function
def cancel_fn() -> None:
timer.cancel() # Cancel the timer if it hasn't executed yet
return cancel_fn
This implementation creates a threading.Timer that executes fn after t / 1000 seconds (Python timers use seconds). cancel_fn calls timer.cancel() to prevent the scheduled execution if it is invoked before the timer fires.
Go Solution
package main
import (
"time"
)
func Cancellable(fn func(...any), args []any, t int) func() {
timer := time.AfterFunc(time.Duration(t)*time.Millisecond, func() {
fn(args...)
})
cancelFn := func() {
timer.Stop() // Stop the timer if it hasn't executed
}
return cancelFn
}
In Go, time.AfterFunc schedules the execution of fn after t milliseconds. The returned cancelFn calls timer.Stop(), which prevents execution if the timer has not yet fired. Go requires args to be expanded using ... when calling a variadic function.
Worked Examples
Example 1: fn = x => x * 5, args = [2], t = 20, cancelTimeMs = 50
| Time (ms) | Action |
|---|---|
| 0 | Schedule fn(2) after 20ms |
| 20 | fn(2) executes, result = 10 |
| 50 | cancelFn called, timer already fired, no effect |
Example 2: fn = x => x**2, args = [2], t = 100, cancelTimeMs = 50
| Time (ms) | Action |
|---|---|
| 0 | Schedule fn(2) after 100ms |
| 50 | cancelFn called, timer stopped |
| 100 | fn does not execute |
Example 3: fn = (x1, x2) => x1 * x2, args = [2, 4], t = 30, cancelTimeMs = 100
| Time (ms) | Action |
|---|---|
| 0 | Schedule fn(2,4) after 30ms |
| 30 | fn(2,4) executes, result = 8 |
| 100 | cancelFn called, no effect |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) | Scheduling the timer and defining cancelFn is constant time |
| Space | O(1) | Only a single timer object and function closure are stored |
The complexity is minimal because we leverage built-in timer mechanisms. There is no loop or additional data structure usage.
Test Cases
# Provided examples
cancel_fn1 = cancellable(lambda x: x*5, [2], 20)
threading.Timer(0.05, cancel_fn1).start() # cancel after 50ms
cancel_fn2 = cancellable(lambda x: x**2, [2], 100)
threading.Timer(0.05, cancel_fn2).start() # cancel after 50ms
cancel_fn3 = cancellable(lambda x1, x2: x1*x2, [2,4], 30)
threading.Timer(0.1, cancel_fn3).start() # cancel after 100ms
# Boundary case: immediate cancel
cancel_fn4 = cancellable(lambda x: x+1, [5], 50)
cancel_fn4() # should cancel immediately
# Edge case: no arguments
cancel_fn5 = cancellable(lambda: 42, [], 50)
threading.Timer(0.02, cancel_fn5).start()
| Test | Why |
|---|---|
| Example 1 | Cancel occurs after execution, fn runs |
| Example 2 | Cancel occurs before execution, fn does not run |
| Example 3 | Cancel occurs after execution, fn runs |
| Immediate cancel | Tests zero-delay cancellation |
| No arguments | Tests function without parameters |
Edge Cases
- Immediate cancellation: If
cancelFnis called immediately after creation,fnmust never execute. Usingtimer.cancel()ensures that any scheduled timer is cleared, so this case is handled. - Cancel exactly at execution time: If
cancelFnis invoked at the exact momentfnis scheduled to execute, there is a potential race condition. The timer implementations in Python and Go guarantee that either the cancel will prevent execution if called slightly before the firing, orfnexecutes if the timer has already fired. This aligns with the problem requirements. - Multiple arguments: Functions with multiple arguments need correct expansion. In Python, we use
*args; in Go, we useargs.... This ensures the function is invoked with all arguments provided, preventing type or argument errors.
This implementation is robust for all scenarios within the provided constraints.