Copilot commented on code in PR #14034: URL: https://github.com/apache/apisix/pull/14034#discussion_r4268497136
########## apisix/plugins/prometheus/process_metrics.lua: ########## @@ -0,0 +1,385 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- Per-process OpenResty internals: Lua timers, connection slots and Lua VM +-- memory. These values only exist inside the process they describe, while the +-- exposition is rendered by a single process, so every process writes its own +-- gauges into the shared metric dict from a timer, and the process rendering +-- the exposition refreshes its own values right before it does. +local core = require("apisix.core") +local ffi = require("ffi") +local process = require("ngx.process") +local C = ffi.C +local ngx = ngx +local timer_at = ngx.timer.at +local collectgarbage = collectgarbage +local tonumber = tonumber +local tostring = tostring +local pcall = pcall +local ipairs = ipairs +local select = select + + +-- The prefix of struct ngx_cycle_s up to write_events. Its layout is the same +-- in every nginx core shipped by the supported runtimes (1.21.4 to 1.29.x); +-- the self check in check_cycle_layout() is what keeps a future change from +-- turning into a wrong number. +ffi.cdef[[ +typedef struct { + uintptr_t log_level; + void *file; + uintptr_t connection; + long disk_full_time; + void *handler; + void *data; + void *writer; + void *wdata; + char *action; + void *next; +} apisix_prom_ngx_log_t; + +typedef struct { + void *elts; + uintptr_t nelts; + size_t size; + uintptr_t nalloc; + void *pool; +} apisix_prom_ngx_array_t; + +typedef struct { + void *elts; + uintptr_t nelts; + void *next; +} apisix_prom_ngx_list_part_t; + +typedef struct { + void *last; + apisix_prom_ngx_list_part_t part; + size_t size; + uintptr_t nalloc; + void *pool; +} apisix_prom_ngx_list_t; + +typedef struct { + uintptr_t key; + void *left; + void *right; + void *parent; + unsigned char color; + unsigned char data; +} apisix_prom_ngx_rbtree_node_t; + +typedef struct { + void *root; + void *sentinel; + void *insert; +} apisix_prom_ngx_rbtree_t; + +typedef struct { + void *data; + void *read; + void *write; +} apisix_prom_ngx_connection_head_t; + +typedef struct { + void *conf_ctx; + void *pool; + void *log; + apisix_prom_ngx_log_t new_log; + uintptr_t log_use_stderr; + void *files; + void *free_connections; + uintptr_t free_connection_n; + void *modules; + uintptr_t modules_n; + uintptr_t modules_used; + void *reusable_connections_queue[2]; + uintptr_t reusable_connections_n; + long connections_reuse_time; + apisix_prom_ngx_array_t listening; + apisix_prom_ngx_array_t paths; + apisix_prom_ngx_array_t config_dump; + apisix_prom_ngx_rbtree_t config_dump_rbtree; + apisix_prom_ngx_rbtree_node_t config_dump_sentinel; + apisix_prom_ngx_list_t open_files; + apisix_prom_ngx_list_t shared_memory; + uintptr_t connection_n; + uintptr_t files_n; + apisix_prom_ngx_connection_head_t *connections; + void *read_events; + void *write_events; +} apisix_prom_ngx_cycle_t; + +extern apisix_prom_ngx_cycle_t *ngx_cycle; +]] + + +-- nginx's own defaults for lua_max_running_timers / lua_max_pending_timers, +-- in effect when the configuration leaves them out +local DEFAULT_MAX_RUNNING_TIMERS = 256 +local DEFAULT_MAX_PENDING_TIMERS = 1024 +local DEFAULT_INTERVAL = 15 + +-- Shared by every process, so a layout mismatch found by one worker keeps the +-- connection gauges out of the exposition everywhere, including the +-- privileged agent, whose connection count cannot be checked against config. +local CONNECTIONS_VERDICT_KEY = "process_metrics:connections_readable" + +local _M = {} + +local gauges +local dict +local process_label +local no_extra_labels = {} +local extra_label_names = no_extra_labels +local extra_label_values +local subsystem = ngx.config.subsystem +local verdict_logged = false + +local timer_running = false +local stopped = true +local interval = DEFAULT_INTERVAL + + +local function get_process_label() + if process.type() == "privileged agent" then + return "privileged-agent" + end + + return "worker-" .. tostring(ngx.worker.id() or 0) +end + + +local function timers_max() + local nginx_config = core.config.local_conf().nginx_config or {} + return nginx_config.max_running_timers or DEFAULT_MAX_RUNNING_TIMERS, + nginx_config.max_pending_timers or DEFAULT_MAX_PENDING_TIMERS +end + + +local function read_cycle() + local cycle = C.ngx_cycle + local connection_n = tonumber(cycle.connection_n) + local free_n = tonumber(cycle.free_connection_n) + + -- every connection slot is wired to its own pair of events at startup, so + -- this pins down the offsets right after connection_n as well + local conns = cycle.connections + if connection_n == 0 or free_n > connection_n or conns == nil + or conns[0].read ~= cycle.read_events or conns[0].write ~= cycle.write_events + then + return nil + end + + return connection_n, free_n +end + + +local function check_cycle_layout() + local ok, connection_n = pcall(read_cycle) + if not ok or not connection_n then + return false, "unexpected nginx cycle layout" + end + + local event = core.config.local_conf().nginx_config.event + local expected = event and tonumber(event.worker_connections) + if connection_n ~= expected then + return false, "worker connection slots read as " .. tostring(connection_n) + .. ", but worker_connections is " .. tostring(expected) + end Review Comment: `core.config.local_conf().nginx_config` can be `nil` (or missing `event`) depending on configuration and defaults, which would raise attempts to index `.event`. Make this consistent with `timers_max()` by guarding `nginx_config` first (e.g., `local nginx_config = core.config.local_conf().nginx_config or {}` then `local event = nginx_config.event`). Additionally, if `expected` is `nil`, this will currently force-disable connection metrics (since `connection_n ~= nil`), so handle the `nil` case explicitly with a clearer error/decision (e.g., treat as “cannot validate” and skip enabling/disabling based on this comparison). ########## t/cli/test_prometheus_stream.sh: ########## @@ -124,3 +124,26 @@ if ! echo "$out" | grep "apisix_node_info{hostname=" > /dev/null; then fi echo "passed: prometheus works when only stream is enabled" + +# the http subsystem exists only to export metrics here, yet every process +# still reports its own Lua VMs and connection slots +ok=0 +deadline=$(( $(date +%s) + 20 )) +{ set +x; } 2>/dev/null +while [ "$(date +%s)" -lt "$deadline" ]; do + out="$(curl -s --connect-timeout 1 --max-time 2 http://127.0.0.1:9091/apisix/prometheus/metrics || true)" + if echo "$out" | grep -qE 'apisix_nginx_timers\{process="worker-0",subsystem="stream",state="running"\} [1-9]' \ Review Comment: This assertion requires a non-zero `running` timer count, but `ngx.timer.running_count()` can be `0` even when the feature works correctly. This can cause flakes in CI; change the grep pattern to accept `0` as a valid value. ########## t/plugin/prometheus-process-metrics.t: ########## @@ -0,0 +1,210 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request && !defined $block->stream_request) { + $block->set_value("request", "GET /t"); + } + + # t::APISIX renders worker_connections 128 into nginx.conf, so the + # configuration has to say the same for the connection gauges to pass + # their self check + if (!defined $block->extra_yaml_config) { + $block->set_value("extra_yaml_config", <<_EOC_); +stream_plugins: + - prometheus +nginx_config: + max_running_timers: 1234 + max_pending_timers: 5678 + event: + worker_connections: 128 +plugin_attr: + prometheus: + refresh_interval: 0.1 +_EOC_ + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: pre-create public API route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/metrics', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/prometheus/metrics" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: every process reports its timers, connection slots and Lua memory +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.5) + local http = require("resty.http") + local httpc = http.new() + local res = assert(httpc:request_uri( + "http://127.0.0.1:1984/apisix/prometheus/metrics")) + local patterns = { + [[apisix_nginx_timers\{process="worker-0",subsystem="http",state="running"\} [1-9]\d*]], Review Comment: The test assumes `state="running"` timer count is non-zero (`[1-9]\d*`), but `ngx.timer.running_count()` can legitimately be `0` most of the time (it counts currently executing timers, not scheduled/pending timers). This can make TEST 2 flaky; update the regex to accept `0` as well (e.g., `\d+` or `(?:0|[1-9]\d*)`). ########## t/plugin/prometheus-process-metrics.t: ########## @@ -0,0 +1,210 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request && !defined $block->stream_request) { + $block->set_value("request", "GET /t"); + } + + # t::APISIX renders worker_connections 128 into nginx.conf, so the + # configuration has to say the same for the connection gauges to pass + # their self check + if (!defined $block->extra_yaml_config) { + $block->set_value("extra_yaml_config", <<_EOC_); +stream_plugins: + - prometheus +nginx_config: + max_running_timers: 1234 + max_pending_timers: 5678 + event: + worker_connections: 128 +plugin_attr: + prometheus: + refresh_interval: 0.1 +_EOC_ + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: pre-create public API route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/metrics', + ngx.HTTP_PUT, + [[{ + "plugins": { + "public-api": {} + }, + "uri": "/apisix/prometheus/metrics" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: every process reports its timers, connection slots and Lua memory +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.5) + local http = require("resty.http") + local httpc = http.new() + local res = assert(httpc:request_uri( + "http://127.0.0.1:1984/apisix/prometheus/metrics")) + local patterns = { + [[apisix_nginx_timers\{process="worker-0",subsystem="http",state="running"\} [1-9]\d*]], + [[apisix_nginx_timers\{process="worker-0",subsystem="http",state="pending"\} \d+]], + [[apisix_nginx_timers\{process="privileged-agent",subsystem="http",state="running"\} \d+]], + [[apisix_nginx_timers_max\{state="running"\} 1234\n]], + [[apisix_nginx_timers_max\{state="pending"\} 5678\n]], + [[apisix_nginx_process_connections\{process="worker-0"\} [1-9]\d*\n]], + [[apisix_nginx_process_connections\{process="privileged-agent"\} [1-9]\d*\n]], + [[apisix_nginx_process_connections_max\{process="worker-0"\} 128\n]], + [[apisix_nginx_process_connections_max\{process="privileged-agent"\} 512\n]], + [[apisix_nginx_lua_memory_bytes\{process="worker-0",subsystem="http"\} [1-9]\d*]], + [[apisix_nginx_lua_memory_bytes\{process="privileged-agent",subsystem="http"\} [1-9]\d*]], + } + for _, p in ipairs(patterns) do + if not ngx.re.find(res.body, p, "jo") then + ngx.say("missing ", p) + end + end + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 3: connection slots held by cosockets show up in the worker's gauge +--- config + location /t { + content_by_lua_block { + local http = require("resty.http") + + local function worker_connections() + ngx.sleep(0.5) + local httpc = http.new() + local res = assert(httpc:request_uri( + "http://127.0.0.1:1984/apisix/prometheus/metrics")) + local m = ngx.re.match(res.body, + [[apisix_nginx_process_connections\{process="worker-0"\} (\d+)]], "jo") + return tonumber(m[1]) + end + + local before = worker_connections() + + local socks = {} + for i = 1, 20 do + local sock = ngx.socket.tcp() + assert(sock:connect("127.0.0.1", 1980)) + socks[i] = sock + end + + local held = worker_connections() + + for _, sock in ipairs(socks) do + sock:close() + end + + local released = worker_connections() + + ngx.say("held: ", held - before >= 20) + ngx.say("released: ", held - released >= 20) + } + } +--- response_body +held: true +released: true + + + +=== TEST 4: connection gauges stay out when the slots do not match worker_connections +--- extra_yaml_config +plugin_attr: + prometheus: + refresh_interval: 0.1 +--- config + location /t { + content_by_lua_block { + ngx.sleep(0.5) + local http = require("resty.http") + local httpc = http.new() + local res = assert(httpc:request_uri( + "http://127.0.0.1:1984/apisix/prometheus/metrics")) + ngx.say(ngx.re.find(res.body, "apisix_nginx_process_connections", "jo") == nil) + ngx.say(ngx.re.find(res.body, + [[apisix_nginx_timers\{process="worker-0",subsystem="http",state="pending"\}]], + "jo") ~= nil) + } + } +--- response_body +true +true +--- error_log +process connection metrics are disabled: worker connection slots read as 128, but worker_connections is 10620 + + + +=== TEST 5: the stream Lua VM of every process reports as well +--- stream_server_config + content_by_lua_block { + ngx.req.socket():receive() + ngx.sleep(0.5) + local http = require("resty.http") + local httpc = http.new() + local res = assert(httpc:request_uri( + "http://127.0.0.1:1984/apisix/prometheus/metrics")) + local patterns = { + [[apisix_nginx_timers\{process="worker-0",subsystem="stream",state="running"\} [1-9]\d*]], Review Comment: Same issue as TEST 2: the stream VM `running` timer count can be `0`, so requiring `[1-9]` risks intermittent failures. Adjust the pattern to allow `0` for `state="running"`. ########## apisix/plugins/prometheus/exporter.lua: ########## @@ -1186,6 +1203,9 @@ end local function collect(yieldable) + -- the other processes report their own values on a timer + process_metrics.refresh() + Review Comment: `process_metrics.refresh()` runs on every collection/scrape in the rendering process, which can become hot if Prometheus scrapes frequently (it calls `collectgarbage("count")`, timer counters, and may do FFI reads). Consider throttling refresh to at most once per `refresh_interval` (e.g., track last-refresh time in-module and skip if called too soon), while still keeping the timer-based updates for non-rendering processes. ########## apisix/plugins/prometheus/process_metrics.lua: ########## @@ -0,0 +1,385 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- Per-process OpenResty internals: Lua timers, connection slots and Lua VM +-- memory. These values only exist inside the process they describe, while the +-- exposition is rendered by a single process, so every process writes its own +-- gauges into the shared metric dict from a timer, and the process rendering +-- the exposition refreshes its own values right before it does. +local core = require("apisix.core") +local ffi = require("ffi") +local process = require("ngx.process") +local C = ffi.C +local ngx = ngx +local timer_at = ngx.timer.at +local collectgarbage = collectgarbage +local tonumber = tonumber +local tostring = tostring +local pcall = pcall +local ipairs = ipairs +local select = select + + +-- The prefix of struct ngx_cycle_s up to write_events. Its layout is the same +-- in every nginx core shipped by the supported runtimes (1.21.4 to 1.29.x); +-- the self check in check_cycle_layout() is what keeps a future change from +-- turning into a wrong number. +ffi.cdef[[ +typedef struct { + uintptr_t log_level; + void *file; + uintptr_t connection; + long disk_full_time; + void *handler; + void *data; + void *writer; + void *wdata; + char *action; + void *next; +} apisix_prom_ngx_log_t; + +typedef struct { + void *elts; + uintptr_t nelts; + size_t size; + uintptr_t nalloc; + void *pool; +} apisix_prom_ngx_array_t; + +typedef struct { + void *elts; + uintptr_t nelts; + void *next; +} apisix_prom_ngx_list_part_t; + +typedef struct { + void *last; + apisix_prom_ngx_list_part_t part; + size_t size; + uintptr_t nalloc; + void *pool; +} apisix_prom_ngx_list_t; + +typedef struct { + uintptr_t key; + void *left; + void *right; + void *parent; + unsigned char color; + unsigned char data; +} apisix_prom_ngx_rbtree_node_t; + +typedef struct { + void *root; + void *sentinel; + void *insert; +} apisix_prom_ngx_rbtree_t; + +typedef struct { + void *data; + void *read; + void *write; +} apisix_prom_ngx_connection_head_t; + +typedef struct { + void *conf_ctx; + void *pool; + void *log; + apisix_prom_ngx_log_t new_log; + uintptr_t log_use_stderr; + void *files; + void *free_connections; + uintptr_t free_connection_n; + void *modules; + uintptr_t modules_n; + uintptr_t modules_used; + void *reusable_connections_queue[2]; + uintptr_t reusable_connections_n; + long connections_reuse_time; + apisix_prom_ngx_array_t listening; + apisix_prom_ngx_array_t paths; + apisix_prom_ngx_array_t config_dump; + apisix_prom_ngx_rbtree_t config_dump_rbtree; + apisix_prom_ngx_rbtree_node_t config_dump_sentinel; + apisix_prom_ngx_list_t open_files; + apisix_prom_ngx_list_t shared_memory; + uintptr_t connection_n; + uintptr_t files_n; + apisix_prom_ngx_connection_head_t *connections; + void *read_events; + void *write_events; +} apisix_prom_ngx_cycle_t; + +extern apisix_prom_ngx_cycle_t *ngx_cycle; +]] + + +-- nginx's own defaults for lua_max_running_timers / lua_max_pending_timers, +-- in effect when the configuration leaves them out +local DEFAULT_MAX_RUNNING_TIMERS = 256 +local DEFAULT_MAX_PENDING_TIMERS = 1024 +local DEFAULT_INTERVAL = 15 + +-- Shared by every process, so a layout mismatch found by one worker keeps the +-- connection gauges out of the exposition everywhere, including the +-- privileged agent, whose connection count cannot be checked against config. +local CONNECTIONS_VERDICT_KEY = "process_metrics:connections_readable" + +local _M = {} + +local gauges +local dict +local process_label +local no_extra_labels = {} +local extra_label_names = no_extra_labels +local extra_label_values +local subsystem = ngx.config.subsystem +local verdict_logged = false + +local timer_running = false +local stopped = true +local interval = DEFAULT_INTERVAL + + +local function get_process_label() + if process.type() == "privileged agent" then + return "privileged-agent" + end + + return "worker-" .. tostring(ngx.worker.id() or 0) +end + + +local function timers_max() + local nginx_config = core.config.local_conf().nginx_config or {} + return nginx_config.max_running_timers or DEFAULT_MAX_RUNNING_TIMERS, + nginx_config.max_pending_timers or DEFAULT_MAX_PENDING_TIMERS +end + + +local function read_cycle() + local cycle = C.ngx_cycle + local connection_n = tonumber(cycle.connection_n) + local free_n = tonumber(cycle.free_connection_n) + + -- every connection slot is wired to its own pair of events at startup, so + -- this pins down the offsets right after connection_n as well + local conns = cycle.connections + if connection_n == 0 or free_n > connection_n or conns == nil + or conns[0].read ~= cycle.read_events or conns[0].write ~= cycle.write_events + then + return nil + end + + return connection_n, free_n +end + + +local function check_cycle_layout() + local ok, connection_n = pcall(read_cycle) + if not ok or not connection_n then + return false, "unexpected nginx cycle layout" + end + + local event = core.config.local_conf().nginx_config.event + local expected = event and tonumber(event.worker_connections) + if connection_n ~= expected then + return false, "worker connection slots read as " .. tostring(connection_n) + .. ", but worker_connections is " .. tostring(expected) + end + + return true +end + + +-- Only a worker can tell whether the numbers read through FFI are right: its +-- connection slots must match worker_connections from the configuration. +local function connections_readable() + local readable = dict:get(CONNECTIONS_VERDICT_KEY) + if readable ~= nil then + return readable + end + + if process.type() == "privileged agent" then + -- no worker has checked yet + return false + end + + local ok, err = check_cycle_layout() + dict:set(CONNECTIONS_VERDICT_KEY, ok) + if not ok then + gauges.connections:reset() + gauges.connections_max:reset() + if not verdict_logged then + verdict_logged = true + core.log.warn("process connection metrics are disabled: ", err) + end + end + + return ok +end + + +local function with_extra_labels(labels) + for _, name in ipairs(extra_label_names) do + labels[#labels + 1] = name + end + return labels +end + + +local function label_values(extra, ...) + local values = {...} + local n = select("#", ...) + for i, value in ipairs(extra) do + values[n + i] = value + end + return values +end + + +-- extra_labels, when given, appends the same labels to every gauge here: +-- {names = {...}, values = function() return {...} end} +function _M.init(registry, extra_labels) + dict = registry.dict + if extra_labels then + extra_label_names = extra_labels.names + extra_label_values = extra_labels.values + end + + gauges = { + timers = registry:gauge("nginx_timers", + "Number of Lua timers per process and subsystem, by state", + with_extra_labels({"process", "subsystem", "state"})), + timers_max = registry:gauge("nginx_timers_max", + "Configured maximum number of Lua timers per process, by state", + with_extra_labels({"state"})), + connections = registry:gauge("nginx_process_connections", + "Number of connection slots in use per nginx process", + with_extra_labels({"process"})), + connections_max = registry:gauge("nginx_process_connections_max", + "Number of connection slots per nginx process", + with_extra_labels({"process"})), + lua_memory = registry:gauge("nginx_lua_memory_bytes", + "Memory allocated by the Lua VM per process and subsystem", + with_extra_labels({"process", "subsystem"})), + } + + -- the verdict describes the binary and the configuration of the processes + -- starting now, so each worker checks again rather than trusting the one + -- left behind in the shared dict by a reload + if subsystem == "http" and process.type() ~= "privileged agent" then + dict:delete(CONNECTIONS_VERDICT_KEY) + end +end + + +local function report() + process_label = process_label or get_process_label() + local extra = extra_label_values and extra_label_values() or no_extra_labels + + local timers = gauges.timers + timers:set(ngx.timer.running_count(), + label_values(extra, process_label, subsystem, "running")) + timers:set(ngx.timer.pending_count(), + label_values(extra, process_label, subsystem, "pending")) + + gauges.lua_memory:set(collectgarbage("count") * 1024, + label_values(extra, process_label, subsystem)) + + -- the remaining values are per process rather than per Lua VM, so only + -- the http VM reports them; it is present in every process that exports + if subsystem ~= "http" then + return + end + + local max_running, max_pending = timers_max() + gauges.timers_max:set(max_running, label_values(extra, "running")) + gauges.timers_max:set(max_pending, label_values(extra, "pending")) + + if not connections_readable() then + return + end + + local connection_n, free_n = read_cycle() + if not connection_n then + return + end + + gauges.connections:set(connection_n - free_n, label_values(extra, process_label)) + gauges.connections_max:set(connection_n, label_values(extra, process_label)) +end + + +-- Writes this process's values now. A failure here is logged rather than +-- raised, so it can never cost the caller the rest of the exposition. +function _M.refresh() + if not gauges then + return + end + + local ok, err = pcall(report) + if not ok then + core.log.error("failed to report process metrics: ", err) + end +end + + +local function tick(premature) + if premature or stopped then + timer_running = false + return + end + + _M.refresh() + + local ok, err = timer_at(interval, tick) + if not ok then + timer_running = false + core.log.error("failed to schedule process metrics: ", err) + end +end + + +-- Idempotent: calling it again while reporting only updates the interval. +function _M.start(report_interval) + if not gauges then + return + end + + interval = report_interval or DEFAULT_INTERVAL + stopped = false + if timer_running then + return + end + + timer_running = true + local ok, err = timer_at(0, tick) + if not ok then + timer_running = false + core.log.error("failed to start process metrics: ", err) + end +end + + +-- The pending timer sees this when it fires and does not schedule another. +function _M.stop() + stopped = true +end Review Comment: `stop()` does not allow an immediate restart if a timer is already pending: calling `start()` after `stop()` (but before the pending `tick` fires) will return early due to `timer_running == true`, delaying resumption until the old timer eventually runs. To avoid long “dead periods” after quick disable/enable cycles, consider adding a generation/token checked by `tick` (so old timers become no-ops) and allowing `start()` to schedule a new immediate tick safely. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
