Facebook Pixel

2725. Interval Cancellation

Problem Description

This problem asks you to create a function that executes another function repeatedly at fixed intervals until cancelled.

You need to implement a cancellable function that takes three parameters:

  • fn: a function to be executed
  • args: an array of arguments to pass to fn
  • t: the time interval in milliseconds between each execution

The cancellable function should:

  1. Call fn with the provided args immediately (at time 0)
  2. Continue calling fn with the same args every t milliseconds
  3. Return a cancel function that, when invoked, stops all future executions of fn

The key requirements are:

  • The first execution happens immediately when cancellable is called
  • Subsequent executions happen at intervals of t milliseconds (at times t, 2t, 3t, etc.)
  • The returned cancel function stops the interval when called
  • After cancellation, no more executions of fn should occur

For example, if fn is (x) => x * 2, args is [4], t is 20ms, and the cancel function is called at 110ms:

  • fn(4) executes at 0ms, returning 8
  • fn(4) executes at 20ms, returning 8
  • fn(4) executes at 40ms, returning 8
  • fn(4) executes at 60ms, returning 8
  • fn(4) executes at 80ms, returning 8
  • fn(4) executes at 100ms, returning 8
  • Cancel is called at 110ms, preventing the execution at 120ms

The solution uses setInterval to handle the repeated executions and clearInterval in the returned cancel function to stop them.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to execute a function repeatedly at regular intervals, JavaScript provides the built-in setInterval function which is perfectly suited for this task. However, there's a subtle requirement in this problem - the function must execute immediately at time 0, not after waiting for the first interval.

The natural approach would be to just use setInterval(fn, t), but this would wait t milliseconds before the first execution. To handle the immediate execution requirement, we need to call fn(...args) once before setting up the interval.

For the cancellation mechanism, setInterval returns a timer ID that can be used with clearInterval to stop the repeated execution. This maps directly to our need for a cancel function. We can capture this timer ID and return a function that calls clearInterval with it.

The solution structure becomes clear:

  1. Execute fn(...args) immediately to satisfy the time 0 execution
  2. Set up setInterval(() => fn(...args), t) to handle all subsequent executions at intervals of t
  3. Store the interval ID returned by setInterval
  4. Return a function that calls clearInterval with the stored ID

This approach elegantly handles all the requirements - immediate first execution, repeated executions at fixed intervals, and a clean cancellation mechanism. The closure created by the returned function naturally maintains access to the interval ID, allowing it to cancel the interval when called.

Solution Approach

The implementation follows a straightforward approach using JavaScript's timer functions:

  1. Immediate Execution: First, we call fn(...args) directly to ensure the function executes at time 0. The spread operator ...args unpacks the array of arguments to pass them individually to the function.

  2. Setting Up Repeated Execution: We use setInterval(() => fn(...args), t) to create a timer that executes the function every t milliseconds. The arrow function () => fn(...args) creates a closure that captures both fn and args, ensuring they're available for each execution.

  3. Storing the Timer ID: The setInterval function returns a unique timer ID, which we store in the time variable. This ID is crucial for cancellation later.

  4. Returning the Cancel Function: We return an arrow function () => clearInterval(time) that, when called, will stop the interval. This function forms a closure over the time variable, maintaining access to the timer ID even after the cancellable function has finished executing.

The complete implementation:

function cancellable(fn: Function, args: any[], t: number): Function {
    fn(...args);                           // Execute immediately at time 0
    const time = setInterval(() => fn(...args), t);  // Set up repeated execution
    return () => clearInterval(time);      // Return cancel function
}

This solution leverages JavaScript's closure mechanism - the returned cancel function maintains a reference to the time variable from its parent scope, allowing it to clear the correct interval when invoked. The pattern of returning a cleanup function is common in JavaScript for managing resources and side effects.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a simple example where we want to repeatedly log a counter value.

Setup:

  • fn = (x) => console.log(x * 2)
  • args = [3]
  • t = 30 (milliseconds)
  • Cancel will be called at 95ms

Step-by-step execution:

  1. Time 0ms: When cancellable(fn, [3], 30) is called:

    • fn(...args) executes immediately → fn(3) → logs 6
    • setInterval(() => fn(3), 30) is set up and returns timer ID (let's say ID = 123)
    • Returns cancel function that captures timer ID 123
  2. Time 30ms: First interval triggers:

    • The interval callback () => fn(...args) executes
    • fn(3) runs → logs 6
  3. Time 60ms: Second interval triggers:

    • The interval callback executes again
    • fn(3) runs → logs 6
  4. Time 90ms: Third interval triggers:

    • The interval callback executes
    • fn(3) runs → logs 6
  5. Time 95ms: Cancel function is called:

    • clearInterval(123) executes
    • The interval is cleared, preventing future executions
  6. Time 120ms: No execution occurs (interval was cancelled)

Output timeline:

0ms:  6 (immediate execution)
30ms: 6 (first interval)
60ms: 6 (second interval)  
90ms: 6 (third interval)
95ms: [cancel called]
120ms: (nothing - execution stopped)

The key insight is that we get both the immediate execution at time 0 (from the direct fn(...args) call) and the repeated executions (from setInterval), with clean cancellation when needed.

Solution Implementation

1import time
2import threading
3from typing import Callable, List, Any
4
5
6def cancellable(fn: Callable, args: List[Any], t: float) -> Callable:
7    """
8    Creates a cancellable function that executes periodically
9  
10    Args:
11        fn: The function to be executed periodically
12        args: The arguments to pass to the function
13        t: The time interval in milliseconds between executions
14      
15    Returns:
16        A cancel function that stops the periodic execution
17    """
18    # Convert milliseconds to seconds for Python's threading.Timer
19    interval_seconds = t / 1000.0
20  
21    # Flag to control the execution loop
22    should_continue = [True]  # Using list to make it mutable in nested function
23  
24    # Execute the function immediately
25    fn(*args)
26  
27    def run_periodically():
28        """Helper function to execute fn periodically"""
29        if should_continue[0]:
30            fn(*args)
31            # Schedule the next execution
32            timer = threading.Timer(interval_seconds, run_periodically)
33            timer.daemon = True  # Make thread daemon so it doesn't prevent program exit
34            timer.start()
35            # Store timer reference for cancellation
36            should_continue.append(timer)
37  
38    # Start the first scheduled execution after the interval
39    timer = threading.Timer(interval_seconds, run_periodically)
40    timer.daemon = True
41    timer.start()
42    should_continue.append(timer)
43  
44    def cancel():
45        """Cancel function that stops the periodic execution"""
46        should_continue[0] = False
47        # Cancel all pending timers
48        for item in should_continue[1:]:
49            if isinstance(item, threading.Timer):
50                item.cancel()
51  
52    return cancel
53
54
55# Example usage demonstration
56if __name__ == "__main__":
57    result = []
58  
59    # Define a simple function that doubles the input
60    def fn(x: int) -> int:
61        """Function that doubles the input value"""
62        return x * 2
63  
64    args = [4]
65    t = 20  # Interval time in milliseconds
66    cancel_t = 110  # Time to cancel in milliseconds
67  
68    # Record the start time
69    start_time = time.time() * 1000  # Convert to milliseconds
70  
71    # Wrapper function to log results with timestamp
72    def log(*args_arr):
73        """Log function execution results with timestamp"""
74        current_time = time.time() * 1000  # Convert to milliseconds
75        result.append({
76            "time": int(current_time - start_time),
77            "returned": fn(*args_arr)
78        })
79  
80    # Start the cancellable periodic execution
81    cancel = cancellable(log, args, t)
82  
83    # Cancel the execution after cancel_t milliseconds
84    def delayed_cancel():
85        """Function to cancel after specified delay"""
86        time.sleep(cancel_t / 1000.0)  # Convert to seconds
87        cancel()
88        print(result)
89        # Expected output:
90        # [
91        #     {"time": 0, "returned": 8},
92        #     {"time": 20, "returned": 8},
93        #     {"time": 40, "returned": 8},
94        #     {"time": 60, "returned": 8},
95        #     {"time": 80, "returned": 8},
96        #     {"time": 100, "returned": 8}
97        # ]
98  
99    # Run the cancellation in a separate thread
100    cancel_thread = threading.Thread(target=delayed_cancel)
101    cancel_thread.start()
102    cancel_thread.join()  # Wait for the thread to complete
103
1import java.util.*;
2import java.util.concurrent.*;
3import java.util.function.*;
4
5/**
6 * Creates a cancellable function that executes periodically
7 * @param fn - The function to be executed periodically
8 * @param args - The arguments to pass to the function
9 * @param t - The time interval in milliseconds between executions
10 * @return A Runnable that when executed stops the periodic execution
11 */
12public class CancellableFunction {
13  
14    public static Runnable cancellable(Consumer<Object[]> fn, Object[] args, long t) {
15        // Execute the function immediately
16        fn.accept(args);
17      
18        // Create a scheduled executor for periodic execution
19        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
20      
21        // Set up periodic execution of the function
22        ScheduledFuture<?> scheduledFuture = executor.scheduleAtFixedRate(
23            () -> fn.accept(args),
24            t,  // Initial delay
25            t,  // Period between executions
26            TimeUnit.MILLISECONDS
27        );
28      
29        // Return a cancel function that stops the scheduled execution
30        return () -> {
31            scheduledFuture.cancel(false);
32            executor.shutdown();
33        };
34    }
35  
36    /**
37     * Example usage demonstration
38     */
39    public static void main(String[] args) {
40        // List to store results with timestamp
41        List<Map<String, Object>> result = new ArrayList<>();
42      
43        // Define a simple function that doubles the input
44        Function<Integer, Integer> fn = x -> x * 2;
45        Object[] fnArgs = {4};
46        long t = 20; // Interval time in milliseconds
47        long cancelT = 110; // Time to cancel in milliseconds
48      
49        // Record start time for calculating relative timestamps
50        long startTime = System.currentTimeMillis();
51      
52        // Wrapper function to log results with timestamp
53        Consumer<Object[]> log = (argsArr) -> {
54            Map<String, Object> entry = new HashMap<>();
55            entry.put("time", System.currentTimeMillis() - startTime);
56            entry.put("returned", fn.apply((Integer) argsArr[0]));
57            result.add(entry);
58        };
59      
60        // Start the cancellable periodic execution
61        Runnable cancel = cancellable(log, fnArgs, t);
62      
63        // Schedule cancellation after cancelT milliseconds
64        ScheduledExecutorService cancelExecutor = Executors.newSingleThreadScheduledExecutor();
65        cancelExecutor.schedule(() -> {
66            // Cancel the periodic execution
67            cancel.run();
68          
69            // Print the results
70            System.out.println(result);
71            // Expected output format:
72            // [
73            //     {time=0, returned=8},
74            //     {time=20, returned=8},
75            //     {time=40, returned=8},
76            //     {time=60, returned=8},
77            //     {time=80, returned=8},
78            //     {time=100, returned=8}
79            // ]
80          
81            // Shutdown the cancel executor
82            cancelExecutor.shutdown();
83        }, cancelT, TimeUnit.MILLISECONDS);
84    }
85}
86
1#include <iostream>
2#include <vector>
3#include <functional>
4#include <thread>
5#include <chrono>
6#include <atomic>
7#include <memory>
8
9/**
10 * Class to handle cancellable periodic function execution
11 */
12class CancellableTimer {
13private:
14    std::atomic<bool> running;
15    std::thread workerThread;
16  
17public:
18    /**
19     * Creates a cancellable function that executes periodically
20     * @param fn - The function to be executed periodically
21     * @param t - The time interval in milliseconds between executions
22     */
23    CancellableTimer(std::function<void()> fn, int t) : running(true) {
24        // Execute the function immediately
25        fn();
26      
27        // Set up periodic execution of the function in a separate thread
28        workerThread = std::thread([this, fn, t]() {
29            while (running) {
30                std::this_thread::sleep_for(std::chrono::milliseconds(t));
31                if (running) {
32                    fn();
33                }
34            }
35        });
36    }
37  
38    /**
39     * Cancels the periodic execution
40     */
41    void cancel() {
42        running = false;
43        if (workerThread.joinable()) {
44            workerThread.join();
45        }
46    }
47  
48    // Destructor to ensure thread cleanup
49    ~CancellableTimer() {
50        cancel();
51    }
52};
53
54/**
55 * Creates a cancellable function that executes periodically
56 * @param fn - The function to be executed periodically
57 * @param t - The time interval in milliseconds between executions
58 * @returns A shared pointer to CancellableTimer that can be used to cancel execution
59 */
60std::shared_ptr<CancellableTimer> cancellable(std::function<void()> fn, int t) {
61    return std::make_shared<CancellableTimer>(fn, t);
62}
63
64/**
65 * Example usage demonstrating the cancellable function
66 */
67void exampleUsage() {
68    // Structure to store results with timestamp
69    struct Result {
70        long long time;  // Time in milliseconds
71        int returned;    // Returned value
72    };
73  
74    std::vector<Result> result;
75  
76    // Define a simple function that doubles the input
77    auto fn = [](int x) -> int { return x * 2; };
78    int args = 4;
79    int t = 20;        // Interval time in milliseconds
80    int cancelT = 110; // Time to cancel in milliseconds
81  
82    // Record start time
83    auto startTime = std::chrono::steady_clock::now();
84  
85    // Wrapper function to log results with timestamp
86    auto log = [&result, &fn, &args, &startTime]() {
87        auto currentTime = std::chrono::steady_clock::now();
88        auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(
89            currentTime - startTime).count();
90      
91        result.push_back({
92            elapsedTime,
93            fn(args)
94        });
95    };
96  
97    // Start the cancellable periodic execution
98    auto cancelHandle = cancellable(log, t);
99  
100    // Cancel the execution after cancelT milliseconds
101    std::this_thread::sleep_for(std::chrono::milliseconds(cancelT));
102    cancelHandle->cancel();
103  
104    // Output the results
105    std::cout << "[\n";
106    for (size_t i = 0; i < result.size(); ++i) {
107        std::cout << "    {\"time\":" << result[i].time 
108                  << ",\"returned\":" << result[i].returned << "}";
109        if (i < result.size() - 1) {
110            std::cout << ",";
111        }
112        std::cout << "\n";
113    }
114    std::cout << "]\n";
115    // Expected Output:
116    // [
117    //     {"time":0,"returned":8},
118    //     {"time":20,"returned":8},
119    //     {"time":40,"returned":8},
120    //     {"time":60,"returned":8},
121    //     {"time":80,"returned":8},
122    //     {"time":100,"returned":8}
123    // ]
124}
125
126int main() {
127    exampleUsage();
128    return 0;
129}
130
1/**
2 * Creates a cancellable function that executes periodically
3 * @param fn - The function to be executed periodically
4 * @param args - The arguments to pass to the function
5 * @param t - The time interval in milliseconds between executions
6 * @returns A cancel function that stops the periodic execution
7 */
8function cancellable(fn: Function, args: any[], t: number): Function {
9    // Execute the function immediately
10    fn(...args);
11  
12    // Set up periodic execution of the function
13    const intervalId = setInterval(() => fn(...args), t);
14  
15    // Return a cancel function that clears the interval
16    return () => clearInterval(intervalId);
17}
18
19/**
20 * Example usage:
21 * 
22 * const result: Array<{time: number, returned: any}> = [];
23 * 
24 * // Define a simple function that doubles the input
25 * const fn = (x: number): number => x * 2;
26 * const args = [4];
27 * const t = 20; // Interval time in milliseconds
28 * const cancelT = 110; // Time to cancel in milliseconds
29 * 
30 * // Wrapper function to log results with timestamp
31 * const log = (...argsArr: any[]): void => {
32 *     result.push({
33 *         time: Date.now() - startTime,
34 *         returned: fn(...argsArr)
35 *     });
36 * };
37 * 
38 * const startTime = Date.now();
39 * 
40 * // Start the cancellable periodic execution
41 * const cancel = cancellable(log, args, t);
42 * 
43 * // Cancel the execution after cancelT milliseconds
44 * setTimeout(() => {
45 *     cancel();
46 *     console.log(result);
47 *     // Output: [
48 *     //     {"time":0,"returned":8},
49 *     //     {"time":20,"returned":8},
50 *     //     {"time":40,"returned":8},
51 *     //     {"time":60,"returned":8},
52 *     //     {"time":80,"returned":8},
53 *     //     {"time":100,"returned":8}
54 *     // ]
55 * }, cancelT);
56 */
57

Time and Space Complexity

Time Complexity: O(1) for the cancellable function itself. The function executes in constant time as it only performs three operations: calling fn once, setting up an interval, and returning a cancel function. However, the total time complexity over the lifetime of the interval is O(n) where n = floor(cancelT / t) represents the number of times fn is executed before cancellation.

Space Complexity: O(1). The function allocates constant space for:

  • The time variable to store the interval ID
  • The returned cancel function closure which captures the time variable
  • The interval timer in the JavaScript event loop

The space complexity doesn't depend on the size of args since they're passed by reference and not copied. The function fn and its arguments are not duplicated in memory.

Common Pitfalls

1. Forgetting the Initial Immediate Execution

A frequent mistake is only setting up the interval without executing the function immediately at time 0. This would cause the first execution to happen at time t instead of time 0.

Incorrect Implementation:

def cancellable(fn, args, t):
    interval_seconds = t / 1000.0
  
    # Missing immediate execution!
    def run_periodically():
        fn(*args)
        # ... rest of the code
  
    timer = threading.Timer(interval_seconds, run_periodically)
    timer.start()
    # First execution happens at time t, not 0

Solution: Always call fn(*args) immediately before setting up the periodic execution.

2. Memory Leaks from Uncancelled Timers

Failing to properly clean up timers can lead to memory leaks and threads continuing to run even after they should have stopped. This is especially problematic when creating multiple cancellable functions without cancelling previous ones.

Problematic Pattern:

def cancellable(fn, args, t):
    should_continue = [True]
  
    def run_periodically():
        if should_continue[0]:
            fn(*args)
            # Creating new timer without storing reference
            threading.Timer(t/1000.0, run_periodically).start()
            # Can't cancel these timers later!
  
    def cancel():
        should_continue[0] = False
        # Timers still exist in memory and may execute

Solution: Store references to all created timers and explicitly cancel them in the cancel function.

3. Race Conditions in Cancellation

The cancellation might happen while a function execution is in progress or just about to start, leading to unexpected behavior or one extra execution after cancellation.

Potential Issue:

def cancel():
    should_continue[0] = False  # Set flag
    # But a timer might fire between these two lines!
    for timer in timers:
        timer.cancel()  # Too late, function already executing

Solution: Use thread-safe mechanisms and cancel timers before setting flags:

def cancel():
    # Cancel timers first to prevent new executions
    for item in should_continue[1:]:
        if isinstance(item, threading.Timer):
            item.cancel()
    # Then set the flag
    should_continue[0] = False

4. Incorrect Time Unit Conversion

JavaScript uses milliseconds for setInterval, but Python's threading.Timer uses seconds. Forgetting to convert can cause functions to execute 1000x too frequently or too slowly.

Wrong:

timer = threading.Timer(t, run_periodically)  # t is in milliseconds!

Correct:

timer = threading.Timer(t / 1000.0, run_periodically)  # Convert to seconds

5. Non-Daemon Threads Preventing Program Exit

Creating non-daemon timer threads can prevent the Python program from exiting naturally, causing it to hang.

Problematic:

timer = threading.Timer(interval_seconds, run_periodically)
timer.start()  # Non-daemon thread keeps program alive

Solution:

timer = threading.Timer(interval_seconds, run_periodically)
timer.daemon = True  # Thread will terminate when main program exits
timer.start()
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More