Fix sum on incremental dimensions for archived dims (#22580)
fix sum on incremental dimensions for archived dims query_target.c read the algorithm from the live RRDDIM, so SUM grouping on incremental dims whose RRDDIM was no longer in memory (host disconnected, chart obsoleted, parent-side child history) skipped the rate->volume scaling and returned raw rate sums, off by source_update_every. Persist RRD_ALGORITHM on RRDMETRIC, populate it from the live RRDDIM, the SQL dimension row at startup, and rrddim_set_algorithm at runtime. Switch query_target.c to read it atomically from RRDMETRIC, no live RRDDIM required. No schema migration; the dimension.algorithm column already exists.
Stelios Fragkakis committed
Jun 8, 2026 at 10:59 UTC
211914432845e855c08e525270068b9e2a1a1515
8 files changed
+74
-3
src/daemon/unit_test.c
+38
@@ -2,6 +2,7 @@
2
3
#include "common.h"
4
#include "web/api/formatters/rrd2json.h"
5
+#include "database/contexts/rrdcontext-internal.h"
6
7
#if defined(OS_LINUX)
8
#include "collectors/proc.plugin/plugin_proc.h"
@@ -1522,6 +1523,40 @@ static int test_incremental_sum_lookup_respects_update_every(void) {
1523
return rc;
1524
}
1525
1526
+static int test_rrdmetric_algorithm_follows_rrddim(void) {
1527
+ fprintf(stderr, "%s() running...\n", __FUNCTION__);
1528
+
1529
+ RRD_DB_MODE old_default_rrd_memory_mode = default_rrd_memory_mode;
1530
+ default_rrd_memory_mode = RRD_DB_MODE_ALLOC;
1531
+
1532
+ RRDSET *st = rrdset_create_localhost(
1533
+ "netdata", "unittest-algo-track", "unittest-algo-track", "netdata", NULL,
1534
+ "Unit Testing", "x", "unittest", NULL, 1,
1535
+ nd_profile.update_every, RRDSET_TYPE_LINE);
1536
+ RRDDIM *rd = rrddim_add(st, "d", NULL, 1, 1, RRD_ALGORITHM_INCREMENTAL);
1537
+
1538
+ int rc = 0;
1539
+ RRDMETRIC *rm = rrdmetric_acquired_value(rd->rrdcontexts.rrdmetric);
1540
+ if(!rm || rrdmetric_algorithm_atomic_load(rm) != RRD_ALGORITHM_INCREMENTAL) {
1541
+ fprintf(stderr, "%s: after rrddim_add INCREMENTAL, rm->algorithm = %d (want %d)\n",
1542
+ __FUNCTION__, rm ? (int)rrdmetric_algorithm_atomic_load(rm) : -1,
1543
+ (int)RRD_ALGORITHM_INCREMENTAL);
1544
+ rc = 1;
1545
+ }
1546
+
1547
+ rrddim_set_algorithm(st, rd, RRD_ALGORITHM_ABSOLUTE);
1548
+ rm = rrdmetric_acquired_value(rd->rrdcontexts.rrdmetric);
1549
+ if(!rm || rrdmetric_algorithm_atomic_load(rm) != RRD_ALGORITHM_ABSOLUTE) {
1550
+ fprintf(stderr, "%s: after rrddim_set_algorithm ABSOLUTE, rm->algorithm = %d (want %d)\n",
1551
+ __FUNCTION__, rm ? (int)rrdmetric_algorithm_atomic_load(rm) : -1,
1552
+ (int)RRD_ALGORITHM_ABSOLUTE);
1553
+ rc = 1;
1554
+ }
1555
+
1556
+ default_rrd_memory_mode = old_default_rrd_memory_mode;
1557
+ return rc;
1558
+}
1559
+
1560
int run_all_mockup_tests(void)
1561
{
1562
fprintf(stderr, "%s() running...\n", __FUNCTION__ );
@@ -1541,6 +1576,9 @@ int run_all_mockup_tests(void)
1576
if(test_incremental_sum_lookup_respects_update_every())
1577
return 1;
1578
1579
+ if(test_rrdmetric_algorithm_follows_rrddim())
1580
+ return 1;
1581
+
1582
if(!test_variable_renames())
1583
return 1;
1584
src/database/contexts/query_target.c
+1
-1
@@ -270,7 +270,7 @@ static bool query_metric_add(QUERY_TARGET_LOCALS *qtl, QUERY_NODE *qn, QUERY_CON
270
} tier_retention[nd_profile.storage_tiers];
271
272
RRDDIM *rd = rrdmetric_rrddim_get_and_lock(rm);
273
- bool values_stored_as_rates = rd && rd->algorithm == RRD_ALGORITHM_INCREMENTAL;
273
+ bool values_stored_as_rates = rrdmetric_algorithm_atomic_load(rm) == RRD_ALGORITHM_INCREMENTAL;
274
275
for (size_t tier = 0; tier < nd_profile.storage_tiers; tier++) {
276
STORAGE_ENGINE *eng = qn->rrdhost->db[tier].eng;
src/database/contexts/rrdcontext-internal.h
+10
@@ -220,6 +220,7 @@ typedef struct rrdmetric {
220
STRING *name;
221
222
RRDDIM *rrddim;
223
+ RRD_ALGORITHM algorithm; // atomic load/store; survives RRDDIM archive so query paths don't need a live rrddim
224
225
time_t first_time_s;
226
time_t last_time_s;
@@ -235,6 +236,14 @@ static ALWAYS_INLINE void rrdmetric_rrddim_atomic_store(RRDMETRIC *rm, RRDDIM *r
236
__atomic_store_n(&rm->rrddim, rd, __ATOMIC_RELEASE);
237
}
238
239
+static ALWAYS_INLINE RRD_ALGORITHM rrdmetric_algorithm_atomic_load(RRDMETRIC *rm) {
240
+ return __atomic_load_n(&rm->algorithm, __ATOMIC_ACQUIRE);
241
+}
242
+
243
+static ALWAYS_INLINE void rrdmetric_algorithm_atomic_store(RRDMETRIC *rm, RRD_ALGORITHM algorithm) {
244
+ __atomic_store_n(&rm->algorithm, algorithm, __ATOMIC_RELEASE);
245
+}
246
+
247
static ALWAYS_INLINE RRDDIM *rrdmetric_rrddim_get_and_lock(RRDMETRIC *rm) {
248
for(size_t retries = 0; retries < 5; retries++) {
249
RRDDIM *rd = rrdmetric_rrddim_atomic_load(rm);
@@ -437,6 +446,7 @@ static ALWAYS_INLINE void rrdmetric_release(RRDMETRIC_ACQUIRED *rma) {
446
447
void rrdmetric_rrddim_is_freed(RRDDIM *rd);
448
void rrdmetric_updated_rrddim_flags(RRDDIM *rd);
449
+void rrdmetric_updated_rrddim_algorithm(RRDDIM *rd);
450
void rrdmetric_collected_rrddim(RRDDIM *rd);
451
void rrdmetric_not_collected_rrddim(RRDDIM *rd);
452
src/database/contexts/rrdcontext-loading.c
+1
@@ -60,6 +60,7 @@ static void rrdinstance_load_dimension_callback(SQL_DIMENSION_DATA *sd, void *da
60
.id = string_strdupz(sd->id),
61
.name = string_strdupz(sd->name),
62
.flags = RRD_FLAG_ARCHIVED | RRD_FLAG_UPDATE_REASON_LOAD_SQL, // no need for atomic
63
+ .algorithm = (RRD_ALGORITHM)sd->algorithm,
64
};
65
if(sd->hidden) trm.flags |= RRD_FLAG_HIDDEN;
66
src/database/contexts/rrdcontext-metric.c
+20
@@ -170,6 +170,11 @@ static bool rrdmetric_conflict_callback(const DICTIONARY_ITEM *item __maybe_unus
170
rrd_flag_set_updated(rm, RRD_FLAG_UPDATE_REASON_CHANGED_METADATA);
171
}
172
173
+ if(rrdmetric_algorithm_atomic_load(rm) != rm_new->algorithm) {
174
+ rrdmetric_algorithm_atomic_store(rm, rm_new->algorithm);
175
+ rrd_flag_set_updated(rm, RRD_FLAG_UPDATE_REASON_CHANGED_METADATA);
176
+ }
177
+
178
if(!rm->first_time_s || (rm_new->first_time_s && rm_new->first_time_s < rm->first_time_s)) {
179
rm->first_time_s = rm_new->first_time_s;
180
rrd_flag_set_updated(rm, RRD_FLAG_UPDATE_REASON_CHANGED_FIRST_TIME_T);
@@ -256,6 +261,7 @@ void rrdmetric_from_rrddim(RRDDIM *rd) {
261
.name = string_dup(rd->name),
262
.flags = RRD_FLAG_NONE, // no need for atomics
263
.rrddim = rd,
264
+ .algorithm = rd->algorithm,
265
};
266
267
RRDMETRIC_ACQUIRED *rma = (RRDMETRIC_ACQUIRED *)dictionary_set_and_acquire_item(ri->rrdmetrics, string2str(trm.id), &trm, sizeof(trm));
@@ -314,6 +320,20 @@ inline void rrdmetric_updated_rrddim_flags(RRDDIM *rd) {
320
rrdmetric_trigger_updates(rm, __FUNCTION__ );
321
}
322
323
+inline void rrdmetric_updated_rrddim_algorithm(RRDDIM *rd) {
324
+ rrdmetric_not_collected_rrddim(rd);
325
+
326
+ RRDMETRIC *rm = rrddim_get_rrdmetric(rd);
327
+ if(unlikely(!rm)) return;
328
+
329
+ if(rrdmetric_algorithm_atomic_load(rm) != rd->algorithm) {
330
+ rrdmetric_algorithm_atomic_store(rm, rd->algorithm);
331
+ rrd_flag_set_updated(rm, RRD_FLAG_UPDATE_REASON_CHANGED_METADATA);
332
+ }
333
+
334
+ rrdmetric_trigger_updates(rm, __FUNCTION__ );
335
+}
336
+
337
ALWAYS_INLINE void rrdmetric_collected_rrddim(RRDDIM *rd) {
338
if(rd->rrdcontexts.collected)
339
return;
src/database/contexts/rrdcontext.c
+1
-1
@@ -50,7 +50,7 @@ ALWAYS_INLINE void rrdcontext_removed_rrddim(RRDDIM *rd) {
50
}
51
52
ALWAYS_INLINE void rrdcontext_updated_rrddim_algorithm(RRDDIM *rd) {
53
- rrdmetric_updated_rrddim_flags(rd);
53
+ rrdmetric_updated_rrddim_algorithm(rd);
54
}
55
56
ALWAYS_INLINE void rrdcontext_updated_rrddim_multiplier(RRDDIM *rd) {
src/database/sqlite/sqlite_context.c
+2
-1
@@ -130,7 +130,7 @@ done:
130
}
131
132
// Dimension list
133
-#define CTX_GET_DIMENSION_LIST "SELECT d.dim_id, d.id, d.name, CASE WHEN INSTR(d.options,\"hidden\") > 0 THEN 1 ELSE 0 END, c.type||'.'||c.id, c.context " \
133
+#define CTX_GET_DIMENSION_LIST "SELECT d.dim_id, d.id, d.name, CASE WHEN INSTR(d.options,\"hidden\") > 0 THEN 1 ELSE 0 END, c.type||'.'||c.id, c.context, d.algorithm " \
134
"FROM dimension d, chart c WHERE c.host_id = @host_id AND d.chart_id = c.chart_id AND d.dim_id IS NOT NULL ORDER BY d.rowid ASC"
135
void ctx_get_dimension_list(nd_uuid_t *host_uuid, void (*dict_cb)(SQL_DIMENSION_DATA *, void *), void *data)
136
{
@@ -162,6 +162,7 @@ void ctx_get_dimension_list(nd_uuid_t *host_uuid, void (*dict_cb)(SQL_DIMENSION_
162
dimension_data.hidden = sqlite3_column_int(res, 3);
163
dimension_data.chart_id = (char *) sqlite3_column_text(res, 4);
164
dimension_data.context = (char *) sqlite3_column_text(res, 5);
165
+ dimension_data.algorithm = sqlite3_column_int(res, 6);
166
dict_cb(&dimension_data, data);
167
}
168
src/database/sqlite/sqlite_context.h
+1
@@ -25,6 +25,7 @@ typedef struct ctx_dimension {
25
char *id;
26
char *name;
27
bool hidden;
28
+ int algorithm;
29
30
char *context;
31
char *chart_id;