Files
MosisService/docs/SANDBOX_MILESTONE_5.md

8.1 KiB

Milestone 5: Timer & Callback System

Status: Complete Goal: Safe timer APIs managed by kernel.


Overview

This milestone implements JavaScript-style timer APIs (setTimeout, setInterval) that are:

  • Managed by the kernel (not Lua coroutines)
  • Subject to per-app limits
  • Properly cleaned up when apps stop
  • Integrated with sandbox instruction counting

Key Deliverables

  1. TimerManager class - Central timer management
  2. Lua timer APIs - setTimeout, setInterval, clearTimeout, clearInterval
  3. Kernel integration - Fire timers from main loop

File Structure

src/main/cpp/sandbox/
├── lua_sandbox.h           # (existing)
├── lua_sandbox.cpp         # (existing)
├── timer_manager.h         # NEW - Timer management
└── timer_manager.cpp       # NEW - Implementation

Implementation Details

1. TimerManager Class

// timer_manager.h
#pragma once

#include <string>
#include <functional>
#include <queue>
#include <unordered_map>
#include <chrono>
#include <mutex>

struct lua_State;

namespace mosis {

using TimerId = uint64_t;
using TimePoint = std::chrono::steady_clock::time_point;
using Duration = std::chrono::milliseconds;

struct Timer {
    TimerId id;
    std::string app_id;
    TimePoint fire_time;
    Duration interval;      // 0 for setTimeout, >0 for setInterval
    int callback_ref;       // Lua registry reference
    lua_State* L;           // Lua state that owns the callback
    bool cancelled = false;
};

class TimerManager {
public:
    TimerManager();
    ~TimerManager();

    // Create timers (returns timer ID)
    TimerId SetTimeout(lua_State* L, const std::string& app_id,
                       int callback_ref, int delay_ms);
    TimerId SetInterval(lua_State* L, const std::string& app_id,
                        int callback_ref, int interval_ms);

    // Cancel timers
    bool ClearTimer(const std::string& app_id, TimerId id);

    // Cancel all timers for an app
    void ClearAppTimers(const std::string& app_id);

    // Process timers (call from main loop)
    // Returns number of timers fired
    int ProcessTimers();

    // Get timer count for an app
    size_t GetTimerCount(const std::string& app_id) const;

    // Configuration
    static constexpr size_t MAX_TIMERS_PER_APP = 100;
    static constexpr int MIN_INTERVAL_MS = 10;
    static constexpr int MIN_TIMEOUT_MS = 0;

private:
    struct TimerCompare {
        bool operator()(const Timer& a, const Timer& b) const {
            return a.fire_time > b.fire_time;  // Min-heap
        }
    };

    TimerId m_next_id = 1;
    std::priority_queue<Timer, std::vector<Timer>, TimerCompare> m_timers;
    std::unordered_map<std::string, size_t> m_app_timer_counts;
    mutable std::mutex m_mutex;

    void FireTimer(Timer& timer);
};

// Lua API registration
void RegisterTimerAPI(lua_State* L, TimerManager* manager, const std::string& app_id);

} // namespace mosis

2. Lua Timer APIs

-- setTimeout: fire callback once after delay
local id = setTimeout(function()
    print("Fired after 1000ms")
end, 1000)

-- clearTimeout: cancel a pending timeout
clearTimeout(id)

-- setInterval: fire callback repeatedly
local id = setInterval(function()
    print("Fires every 500ms")
end, 500)

-- clearInterval: stop an interval
clearInterval(id)

3. Timer Limits

Limit Value Reason
Max timers per app 100 Prevent resource exhaustion
Min interval 10ms Prevent CPU spinning
Min timeout 0ms Allow immediate callbacks

Test Cases

Test 1: SetTimeout Fires

bool Test_SetTimeoutFires(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    // Register timer API
    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    // Set a timeout
    sandbox.LoadString(R"(
        fired = false
        setTimeout(function() fired = true end, 50)
    )", "timeout_test");

    // Process timers after delay
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    manager.ProcessTimers();

    // Check if callback fired
    sandbox.LoadString("assert(fired == true)", "check");

    return true;
}

Test 2: SetInterval Fires Multiple Times

bool Test_SetIntervalFires(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    sandbox.LoadString(R"(
        count = 0
        setInterval(function() count = count + 1 end, 30)
    )", "interval_test");

    // Process multiple times
    for (int i = 0; i < 5; i++) {
        std::this_thread::sleep_for(std::chrono::milliseconds(40));
        manager.ProcessTimers();
    }

    // Should have fired multiple times
    sandbox.LoadString("assert(count >= 3)", "check");

    return true;
}

Test 3: ClearTimeout Cancels

bool Test_ClearTimeoutCancels(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    sandbox.LoadString(R"(
        fired = false
        local id = setTimeout(function() fired = true end, 100)
        clearTimeout(id)
    )", "clear_test");

    std::this_thread::sleep_for(std::chrono::milliseconds(150));
    manager.ProcessTimers();

    // Should NOT have fired
    sandbox.LoadString("assert(fired == false)", "check");

    return true;
}

Test 4: Timer Limit Enforced

bool Test_TimerLimitEnforced(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    // Try to create too many timers
    sandbox.LoadString(R"(
        for i = 1, 150 do
            setTimeout(function() end, 1000000)
        end
    )", "limit_test");

    // Should be capped at MAX_TIMERS_PER_APP
    EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) <= 100);

    return true;
}

Test 5: ClearAppTimers Cleanup

bool Test_ClearAppTimersCleanup(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    sandbox.LoadString(R"(
        for i = 1, 10 do
            setTimeout(function() end, 1000000)
        end
    )", "cleanup_test");

    EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 10);

    // Clear all timers for app
    manager.ClearAppTimers(ctx.app_id);

    EXPECT_TRUE(manager.GetTimerCount(ctx.app_id) == 0);

    return true;
}

Test 6: Minimum Interval Enforced

bool Test_MinIntervalEnforced(std::string& error_msg) {
    mosis::TimerManager manager;
    SandboxContext ctx = TestContext();
    LuaSandbox sandbox(ctx);

    mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id);

    // Try to set interval less than minimum
    sandbox.LoadString(R"(
        count = 0
        setInterval(function() count = count + 1 end, 1)  -- 1ms, should be clamped to 10ms
    )", "min_interval_test");

    // With 1ms interval, in 50ms we'd get 50 callbacks
    // With 10ms minimum, we should get ~5
    std::this_thread::sleep_for(std::chrono::milliseconds(55));
    for (int i = 0; i < 10; i++) {
        manager.ProcessTimers();
    }

    sandbox.LoadString("assert(count <= 10)", "check");

    return true;
}

Acceptance Criteria

All tests must pass:

  • Test_SetTimeoutFires - Timeout fires after delay
  • Test_SetIntervalFires - Interval fires repeatedly
  • Test_ClearTimeoutCancels - Cancelled timeout doesn't fire
  • Test_ClearIntervalCancels - Cancelled interval stops
  • Test_TimerLimitEnforced - Max 100 timers per app
  • Test_ClearAppTimersCleanup - All app timers cleared on stop
  • Test_MinIntervalEnforced - Interval clamped to 10ms minimum

Next Steps

After Milestone 5 passes:

  1. Milestone 6: JSON & Crypto APIs
  2. Milestone 7: Virtual Filesystem