Skip to content

Request timings stay per-thread: getStats() cannot report latency for a pool #169

Description

@EdmondDantes

What happens

getStats() aggregates across pool workers; the request timings do not reach it, and getTelemetry() reads them from the thread it runs on.

The counters live in the shared slab (src/core/stats_registry.c), so any worker that answers a scrape reports for the whole pool. The timing fields are plain members of http_server_objectsojourn_sum_ns, service_sum_ns, sojourn_max_ns, sojourn_samples (src/http_server_class.c:389-392), written by http_server_aggregate_sample() (:996-1001) and read by getTelemetry() (:5264-5273). Under setWorkers(4) each worker keeps its own four numbers and no API sums them.

Why it matters

A metrics endpoint written in PHP is served by whichever worker took the request, so consecutive scrapes read different workers: 7 ms, then 30 ms, then 7 ms again, with nothing in the response saying which worker answered. One Prometheus series drawn from four unrelated averages carries no trend and cannot carry an alert.

The exporter added to laravel-spawn (YanGusik/laravel-spawn#56) therefore emits counters and gauges only and leaves latency() out of the exposition — the single number an operator asks for first is the one that cannot be exported.

Naming the worker instead of summing does not close it either: a worker cannot report its own slot id, so a PHP caller cannot even label the series it gets.

Reproduction

$config = (new HttpServerConfig())
    ->addListener('127.0.0.1', 8080)
    ->setWorkers(4)
    ->setStatsEnabled(true)
    ->setTelemetryEnabled(true);   /* without this the timings stay zero */

$server = new HttpServer($config);

$server->addHttpHandler(function ($req, $res) use ($server) {
    if ($req->getPath() === '/t') {
        $t = $server->getTelemetry();
        $res->setBody("{$t['sojourn_samples']} samples, {$t['service_avg_ms']} ms\n")->end();
        return;
    }

    Async\delay(random_int(1, 50));
    $res->setBody("ok\n")->end();
});

$server->start();

Drive some load, then scrape /t repeatedly: the sample count jumps between four small independent tallies rather than growing with the traffic, and the average follows whichever worker answered.

Suggested fix

Move the four fields into http_server_counters_t and add them to HTTP_SERVER_COUNTER_TABLE (include/php_http_server.h:898):

X(sojourn_sum_ns,   SUM)
X(service_sum_ns,   SUM)
X(sojourn_samples,  SUM)
X(sojourn_max_ns,   MAX)

The combine semantics the registry needs already exist — http_stats_registry_totals() sums SUM fields and takes the maximum of MAX ones (src/core/stats_registry.c:214-232) — and the retired-totals block keeps a reload from resetting them. getStats() would then carry pool-wide sums, from which an average is one division, while workers[] keeps the per-worker breakdown a caller may still want. Two constraints go with the move: the static assert requires the table and the struct to stay in step, and resetTelemetry() currently zeroes these fields directly (:5370-5373), which would become a write into the worker's slot.

Percentiles are a separate matter and stay out of scope here (stage A6 of #5): this is about the sum, the maximum and the sample count that already exist being unreadable for a pool.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions