walkor/workerman · error · RuntimeException

Event::addTimer($interval) failed

Error message

Event::addTimer($interval) failed

What it means

The repeat() half of the same libevent adapter: it arms a PERSIST \Event timer with addTimer($interval) and throws when libevent refuses the value. The overwhelming cause is a negative or otherwise invalid interval passed through Workerman\Timer::add($interval, $func, $args, true) (the default persistent mode). Because repeat() timers drive heartbeats and periodic tasks, an exception here aborts the callback registration at runtime.

Source

Thrown at src/Events/Event.php:147

     * {@inheritdoc}
     */
    public function offRepeat(int $timerId): bool
    {
        return $this->offDelay($timerId);
    }

    /**
     * {@inheritdoc}
     */
    public function repeat(float $interval, callable $func, array $args = []): int
    {
        $className = $this->eventClassName;
        $timerId = $this->timerId++;
        $event = new $className($this->eventBase, -1, $className::TIMEOUT | $className::PERSIST, function () use ($func, $args) {
            $this->safeCall($func, $args);
        });
        if (!$event->addTimer($interval)) {
            throw new \RuntimeException("Event::addTimer($interval) failed");
        }
        $this->eventTimer[$timerId] = $event;
        return $timerId;
    }

    /**
     * {@inheritdoc}
     */
    public function onReadable($stream, callable $func): void
    {
        $className = $this->eventClassName;
        $fdKey = (int)$stream;
        $event = new $className($this->eventBase, $stream, $className::READ | $className::PERSIST, $func);
        if ($event->add()) {
            $this->readEvents[$fdKey] = $event;
        }
    }

View on GitHub (pinned to 1391112a61)

Solutions

  1. Validate/clamp before registering: `$interval = max(0.001, (float)$interval); Timer::add($interval, $fn);`.
  2. Reject non-positive config values at load time instead of letting them reach the timer (fail fast with a clear config error).
  3. If positive intervals also fail, check ext-event health (php --ri event) or switch loop implementations via Worker::$eventLoopClass.

Example fix

// before: interval from config can be 0 or negative
Timer::add((float)$config['heartbeat_interval'], [$this, 'ping']);
// RuntimeException: Event::addTimer(0) failed / Event::addTimer(-1) failed

// after: validate at load time, clamp at use time
$interval = (float)($config['heartbeat_interval'] ?? 0);
if ($interval <= 0) {
    throw new InvalidArgumentException('heartbeat_interval must be > 0');
}
Timer::add($interval, [$this, 'ping']);
Defensive patterns

Strategy: validation

Validate before calling

function addIntervalTimer(float $interval, callable $fn): int
{
    if ($interval <= 0) {
        throw new InvalidArgumentException('interval must be > 0, got ' . var_export($interval, true));
    }
    return Workerman\Timer::add($interval, $fn); // persistent=true -> repeat()
}

Type guard

function isSchedulableInterval(mixed $interval): bool
{
    return is_numeric($interval) && (float)$interval > 0;
}

Try / catch

try {
    Timer::add($interval, $fn);
} catch (RuntimeException $e) {
    Worker::log('timer registration failed: ' . $e->getMessage());
    throw $e; // do not silently drop periodic tasks (heartbeats, flushers)
}

Prevention

When it happens

Trigger: Timer::add() with persistent=true (default) and a non-positive interval — e.g. interval read from config as 0/-1, an interval computed as a difference that went negative, or interval passed as a string like '-5'. Only with the ext-event Event loop selected.

Common situations: Heartbeat/keepalive tasks where the interval comes from per-connection settings and can be unset or zero; cron-like next-run calculations; unit-tested code running under a loop that behaves differently than the production libevent loop.

Related errors


AI-assisted analysis of walkor/workerman@1391112a61 (2026-08-21). Data as JSON: /api/errors/d3edbaf76e295824. Report an issue: GitHub.