#23896·netdata

dbengine: pre-existing defects found while reviewing the engine/daemon boundary (#23904)

Author: vkalintirisCreated Sep 15, 2026Updated Sep 17, 2026

All of these exist on master (20350344f0) and are independent of #23904, which neither introduced nor fixed them; they were left out of that PR to keep its scope. Line numbers refer to 20350344f0. Found by static tracing during review; none has been reproduced on a live agent yet.

Crash / assert

  • Weights query crashes on a dbengine build running in ram/alloc mode. With two or more hosts and two or more CPUs the weights endpoint takes the parallel path (src/web/api/queries/weights.c:2529 is a compile-time gate only; the serial fallback at :2555 triggers only on host/thread count) and enqueues onto the engine command queue (:2583). That queue's allocator is created by rrdeng_cmd_queue_init() (src/database/engine/rrdengine.c:490) from dbengine_initialize_structures() (:2449, :2458), which runs only when the first tier spawns; without a dbengine tier aral_mallocz() at :547 dereferences NULL. A parent in ram mode whose stream.conf does not require dbengine for its children is the realistic trigger. Fix idea: take the serial path, or run the work inline, when no tier is up.
  • Engine spawn: a failed uv_timer_init() asserts on loop close. In rrdeng_dbengine_spawn() (src/database/engine/rrdengine.c:2466) both timer-failure paths (:2493-2498, :2501-2506) call uv_close() on the async handle and then uv_loop_close() immediately. Closing is asynchronous, so the handle is still in the loop's queue, uv_loop_close() returns UV_EBUSY and fatal_assert fires. The second path additionally never closes the first timer. Reachable only if libuv's uv_timer_init() fails, which the current implementation cannot; recorded for correctness of the error path. Fix idea: close every initialised handle, run the loop once so the close callbacks complete, then close the loop.

Wrong data

  • Gorilla tier-0 byte counters double-count in NETDATA_INTERNAL_CHECKS builds. pgd_disk_footprint() (src/database/engine/page.c:820) bumps the compression counters; the internal_fatal at :882-883 calls it twice more, so actual/optimal/original bytes are over-counted exactly in the builds where the pulse extended charts are on by default. Fix idea: compare against the size already computed instead of recomputing.
  • A tier whose directory creation failed is still counted as created and wired into hosts. In netdata_conf_dbengine_init() (src/daemon/config/netdata-conf-db.c:270-273) a failed mkdir logs and continues, leaving that tier's ret at 0, so the join loop (:305-310) counts it in created_tiers; src/database/rrdhost.c:568 and :587 then wire multidb_ctx[tier] (NULL) into every host's db[tier].si. Fix idea: mark the tier failed on mkdir failure and count only tiers whose rrdeng_init() returned 0.
  • The cache-efficiency statistics snapshot is not atomic. rrdeng_get_cache_efficiency_stats() (src/database/engine/rrdengineapi.c:1497-1500, with its FIXME) returns the struct by plain copy while workers update the fields with relaxed atomics; the 64-bit usec_t timing fields can tear on 32-bit targets. Fix idea: copy each field with __atomic_load_n.

Availability

  • A rotation that gives up strands the datafile. datafile_delete() bails after 30 attempts (src/database/engine/rrdengine.c:1714-1731) but datafile_acquire_for_deletion() has already set users.available = false (src/database/engine/datafile.c:130, :160), so queries can no longer acquire the file until the next quota/time breach retries the deletion. Fix idea: restore available on bail, or keep retrying from the timer without flipping availability.

Cosmetic

  • libuv worker threads register under the inherited thread name. register_libuv_worker_jobs_internal() (src/daemon/libuv_workers.c:6-9) calls worker_register("LIBUV") before the thread is named UV_WORKER[n] (:110-111), and worker_register() snapshots the OS thread name as the tag, so worker-utilisation charts label these threads with whatever name the pool thread inherited. Fix idea: name the thread first.
  • Gorilla pulse charts appear, empty, when dbengine is not running. pulse_gorilla_do() (src/daemon/pulse/pulse-gorilla.c:45) gates the two tier-0 gorilla charts on the configured page type, not on an active tier, so db = ram with pulse extended shows two flat charts. Fix idea: gate on dbengine_enabled.