@cryptotaxi247 / netdata / commits / fb1a1e13e

Reapply alert prototypes when host or chart labels change (#22570)

* Add support for detecting label changes and triggering health rechecks - Introduced `rrdlabels_add_changed` to check and indicate if label additions modify existing labels. - Updated `rrdlabels_migrate_to_these` and related functions to detect changes in label sets. - Added `RRDSET_FLAG_PENDING_LABEL_RECHECK` and `RRDHOST_FLAG_PENDING_LABEL_RECHECK` to signal health evaluation requirements based on label changes. - Enhanced health event loop to handle label-related rechecks alongside initialization. * Refine label migration logic and clarify health recheck behavior. - Updated comments for `rrdlabels_migrate_to_these` to explicitly describe label addition/removal behavior and edge cases. - Improved handling of `RRDHOST_FLAG_PENDING_LABEL_RECHECK` to streamline chart recheck logic during health evaluations. * Add unit tests for label change detection and improve pending flag handling - Introduced `rrdlabels_unittest_change_detection` to validate label modification scenarios. - Updated health event loop to use atomic snapshot-and-clear for pending recheck flags, ensuring consistency during concurrent operations. - Refined label migration logic to address edge cases in rechecks and removals.

Stelios Fragkakis committed May 28, 2026 at 22:18 UTC fb1a1e13e55e80548f407ddd8f7cc7832639af0c
8 files changed +262 -32
src/daemon/analytics.c
+4 -2
@@ -1298,9 +1298,11 @@ void refresh_system_timezone(const char *timezone, bool is_tzdb_name) {
1298 if (rrdhost_update_timezone(localhost, timezone, new_abbrev, new_offset)) {
1299 // Timezone changed — update the two labels directly, persist, and notify.
1300 if (localhost->rrdlabels) {
1301 - rrdlabels_add(localhost->rrdlabels, "_timezone", timezone, RRDLABEL_SRC_AUTO);
1302 - rrdlabels_add(localhost->rrdlabels, "_abbrev_timezone", new_abbrev, RRDLABEL_SRC_AUTO);
1301 + bool labels_changed = rrdlabels_add_changed(localhost->rrdlabels, "_timezone", timezone, RRDLABEL_SRC_AUTO);
1302 + labels_changed |= rrdlabels_add_changed(localhost->rrdlabels, "_abbrev_timezone", new_abbrev, RRDLABEL_SRC_AUTO);
1303 rrdhost_flag_set(localhost, RRDHOST_FLAG_METADATA_LABELS | RRDHOST_FLAG_METADATA_UPDATE);
1304 + if(labels_changed)
1305 + rrdhost_flag_set(localhost, RRDHOST_FLAG_PENDING_LABEL_RECHECK);
1306 stream_send_host_labels(localhost);
1307 }
1308 aclk_queue_node_info(localhost, false);
src/database/rrdhost-labels.c
+4 -2
@@ -9,7 +9,8 @@ void rrdhost_set_is_parent_label(void) {
9
10 if (count == 0 || count == 1) {
11 RRDLABELS *labels = localhost->rrdlabels;
12 - rrdlabels_add(labels, "_is_parent", (count) ? "true" : "false", RRDLABEL_SRC_AUTO);
12 + if(rrdlabels_add_changed(labels, "_is_parent", (count) ? "true" : "false", RRDLABEL_SRC_AUTO))
13 + rrdhost_flag_set(localhost, RRDHOST_FLAG_PENDING_LABEL_RECHECK);
14
15 // queue a node info
16 aclk_queue_node_info(localhost, false);
@@ -202,7 +203,8 @@ void reload_host_labels(void) {
203 // RRDLABEL_FLAG_DONT_DELETE entries are preserved.
204 rrdlabels_remove_all_unmarked(localhost->rrdlabels);
205
205 - rrdhost_flag_set(localhost,RRDHOST_FLAG_METADATA_LABELS | RRDHOST_FLAG_METADATA_UPDATE);
206 + rrdhost_flag_set(localhost,
207 + RRDHOST_FLAG_METADATA_LABELS | RRDHOST_FLAG_METADATA_UPDATE | RRDHOST_FLAG_PENDING_LABEL_RECHECK);
208
209 stream_send_host_labels(localhost);
210 }
src/database/rrdhost.h
+6
@@ -101,6 +101,12 @@ typedef enum __attribute__ ((__packed__)) rrdhost_flags {
101 // gates rrdhost_set_receiver() so a reconnect cannot attach
102 // mid-pass. Set under receiver_lock; the heavy work runs
103 // without holding receiver_lock so readers stay unblocked.
104 +
105 + RRDHOST_FLAG_PENDING_LABEL_RECHECK = (1U << 31), // host labels changed since the last health prototype
106 + // evaluation; on its next pass the health thread treats
107 + // every chart of this host as needing a recheck, in
108 + // addition to charts that have RRDSET_FLAG_PENDING_LABEL_RECHECK
109 + // set individually (no per-chart flag fan-out).
110 } RRDHOST_FLAGS;
111
112 #define rrdhost_flag_get(host) atomic_flags_get(&((host)->flags))
src/database/rrdlabels.c
+175 -11
@@ -252,9 +252,9 @@ static RRDLABEL *rrdlabels_find_label_with_key_unsafe(RRDLABELS *labels, RRDLABE
252 // ----------------------------------------------------------------------------
253 // rrdlabels_add()
254
255 -static void labels_add_already_sanitized(RRDLABELS *labels, const char *key, const char *value, RRDLABEL_SRC ls)
255 +static bool labels_add_already_sanitized(RRDLABELS *labels, const char *key, const char *value, RRDLABEL_SRC ls)
256 {
257 - if (unlikely(!labels || !key)) return;
257 + if (unlikely(!labels || !key)) return false;
258
259 RRDLABEL *new_label = add_label_name_value(key, value);
260
@@ -271,11 +271,13 @@ static void labels_add_already_sanitized(RRDLABELS *labels, const char *key, con
271 int64_t judy_mem = JudyAllocThreadPulseGetAndReset();
272 RRDLABELS_MEMORY_DELTA(&dictionary_stats_category_rrdlabels, judy_mem, 0);
273
274 + bool changed;
275 if(*PValue) {
276 new_ls |= RRDLABEL_FLAG_OLD;
277 *((RRDLABEL_SRC *)PValue) = new_ls;
278
279 delete_label(new_label);
280 + changed = false;
281 }
282 else {
283 new_ls |= RRDLABEL_FLAG_NEW;
@@ -289,6 +291,7 @@ static void labels_add_already_sanitized(RRDLABELS *labels, const char *key, con
291 RRDLABELS_MEMORY_DELTA(&dictionary_stats_category_rrdlabels, judy_mem, 0);
292 delete_label(old_label_with_same_key);
293 }
294 + changed = true;
295 }
296
297 labels->version++;
@@ -297,13 +300,20 @@ static void labels_add_already_sanitized(RRDLABELS *labels, const char *key, con
300 // RRDLABELS_MEMORY_DELTA(&dictionary_stats_category_rrdlabels, judy_mem, 0);
301
302 spinlock_unlock(&labels->spinlock);
303 +
304 + return changed;
305 }
306
307 void rrdlabels_add(RRDLABELS *labels, const char *name, const char *value, RRDLABEL_SRC ls)
308 +{
309 + (void)rrdlabels_add_changed(labels, name, value, ls);
310 +}
311 +
312 +bool rrdlabels_add_changed(RRDLABELS *labels, const char *name, const char *value, RRDLABEL_SRC ls)
313 {
314 if(!labels) {
315 netdata_log_error("%s(): called with NULL dictionary.", __FUNCTION__ );
306 - return;
316 + return false;
317 }
318
319 char n[RRDLABELS_MAX_NAME_LENGTH + 1], v[RRDLABELS_MAX_VALUE_LENGTH + 1];
@@ -312,10 +322,10 @@ void rrdlabels_add(RRDLABELS *labels, const char *name, const char *value, RRDLA
322
323 if(!*n) {
324 netdata_log_error("%s: cannot add name '%s' (value '%s') which is sanitized as empty string", __FUNCTION__, name, value);
315 - return;
325 + return false;
326 }
327
318 - labels_add_already_sanitized(labels, n, v, ls);
328 + return labels_add_already_sanitized(labels, n, v, ls);
329 }
330
331 bool rrdlabels_exist(RRDLABELS *labels, const char *key)
@@ -550,11 +560,12 @@ void rrdlabels_unmark_all(RRDLABELS *labels)
560 spinlock_unlock(&labels->spinlock);
561 }
562
553 -static void rrdlabels_remove_all_unmarked_unsafe(RRDLABELS *labels)
563 +static size_t rrdlabels_remove_all_unmarked_unsafe(RRDLABELS *labels)
564 {
565 Pvoid_t *PValue;
566 Word_t Index = 0;
567 bool first_then_next = true;
568 + size_t removed = 0;
569
570 while ((PValue = JudyLFirstThenNext(labels->JudyL, &Index, &first_then_next))) {
571 if (!((*((RRDLABEL_SRC *)PValue)) & (RRDLABEL_FLAG_INTERNAL))) {
@@ -566,18 +577,21 @@ static void rrdlabels_remove_all_unmarked_unsafe(RRDLABELS *labels)
577 RRDLABELS_MEMORY_DELTA(&dictionary_stats_category_rrdlabels, judy_mem, 0);
578
579 delete_label((RRDLABEL *)Index);
580 + removed++;
581 if (labels->JudyL != (Pvoid_t) NULL) {
582 Index = 0;
583 first_then_next = true;
584 }
585 }
586 }
587 +
588 + return removed;
589 }
590
591 void rrdlabels_remove_all_unmarked(RRDLABELS *labels)
592 {
593 spinlock_lock(&labels->spinlock);
580 - rrdlabels_remove_all_unmarked_unsafe(labels);
594 + (void)rrdlabels_remove_all_unmarked_unsafe(labels);
595 spinlock_unlock(&labels->spinlock);
596 }
597
@@ -599,6 +613,28 @@ void rrdlabels_mark_source_as_old(RRDLABELS *labels, RRDLABEL_SRC src_match)
613 spinlock_unlock(&labels->spinlock);
614 }
615
616 +bool rrdlabels_remove_all_unmarked_and_changed(RRDLABELS *labels)
617 +{
618 + if(!labels) return false;
619 +
620 + spinlock_lock(&labels->spinlock);
621 +
622 + size_t added = 0;
623 + Pvoid_t *PValue;
624 + Word_t Index = 0;
625 + bool first_then_next = true;
626 + while ((PValue = JudyLFirstThenNext(labels->JudyL, &Index, &first_then_next))) {
627 + if ((*((RRDLABEL_SRC *)PValue)) & RRDLABEL_FLAG_NEW)
628 + added++;
629 + }
630 +
631 + size_t removed = rrdlabels_remove_all_unmarked_unsafe(labels);
632 +
633 + spinlock_unlock(&labels->spinlock);
634 +
635 + return (added > 0) || (removed > 0);
636 +}
637 +
638 // ----------------------------------------------------------------------------
639 // rrdlabels_walkthrough_read()
640
@@ -663,9 +699,9 @@ static SIMPLE_PATTERN_RESULT rrdlabels_walkthrough_read_sp(RRDLABELS *labels, SI
699 // rrdlabels_migrate_to_these()
700 // migrate an existing label list to a new list
701
666 -void rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src) {
702 +bool rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src) {
703 if (!dst || !src || (dst == src))
668 - return;
704 + return false;
705
706 spinlock_lock(&dst->spinlock);
707 spinlock_lock(&src->spinlock);
@@ -674,6 +710,7 @@ void rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src) {
710
711 RRDLABEL *label;
712 Pvoid_t *PValue;
713 + size_t added = 0;
714
715 RRDLABEL_SRC ls;
716 lfe_start_nolock(src, label, ls)
@@ -690,6 +727,7 @@ void rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src) {
727 dup_label(label);
728 int64_t judy_mem = JudyAllocThreadPulseGetAndReset();
729 RRDLABELS_MEMORY_DELTA(&dictionary_stats_category_rrdlabels, judy_mem, 0);
730 + added++;
731 }
732 else
733 flag = RRDLABEL_FLAG_OLD;
@@ -698,11 +736,13 @@ void rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src) {
736 }
737 lfe_done_nolock();
738
701 - rrdlabels_remove_all_unmarked_unsafe(dst);
739 + size_t removed = rrdlabels_remove_all_unmarked_unsafe(dst);
740 dst->version = src->version;
741
742 spinlock_unlock(&src->spinlock);
743 spinlock_unlock(&dst->spinlock);
744 +
745 + return (added > 0) || (removed > 0);
746 }
747
748 //
@@ -1011,15 +1051,22 @@ uint32_t rrdlabels_version(RRDLABELS *labels __maybe_unused)
1051 }
1052
1053 void rrdset_update_rrdlabels(RRDSET *st, RRDLABELS *new_rrdlabels) {
1054 + bool labels_changed = false;
1055 +
1056 if(!st->rrdlabels)
1057 st->rrdlabels = rrdlabels_create();
1058
1059 if (new_rrdlabels)
1018 - rrdlabels_migrate_to_these(st->rrdlabels, new_rrdlabels);
1060 + labels_changed = rrdlabels_migrate_to_these(st->rrdlabels, new_rrdlabels);
1061
1062 rrdset_flag_set(st, RRDSET_FLAG_METADATA_UPDATE);
1063 rrdhost_flag_set(st->rrdhost, RRDHOST_FLAG_METADATA_UPDATE);
1064 rrdset_metadata_updated(st);
1065 +
1066 + if(labels_changed) {
1067 + rrdset_flag_set(st, RRDSET_FLAG_PENDING_LABEL_RECHECK);
1068 + rrdhost_flag_set(st->rrdhost, RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION);
1069 + }
1070 }
1071
1072 // ----------------------------------------------------------------------------
@@ -1533,6 +1580,122 @@ static int rrdlabels_unittest_mark_source_as_old(void) {
1580 return errors;
1581 }
1582
1583 +#define UT_EXPECT(_cond, _msg) do { \
1584 + if (!(_cond)) { \
1585 + fprintf(stderr, " FAIL: %s\n", (_msg)); \
1586 + errors++; \
1587 + } \
1588 +} while (0)
1589 +
1590 +static int rrdlabels_unittest_change_detection(void) {
1591 + fprintf(stderr, "\n%s() tests\n", __FUNCTION__);
1592 + int errors = 0;
1593 +
1594 + // ---- rrdlabels_add_changed: new vs same vs value-change ----
1595 + RRDLABELS *l = rrdlabels_create();
1596 + UT_EXPECT(rrdlabels_add_changed(l, "k1", "v1", RRDLABEL_SRC_CONFIG) == true,
1597 + "add of new label should return true");
1598 + UT_EXPECT(rrdlabels_add_changed(l, "k1", "v1", RRDLABEL_SRC_CONFIG) == false,
1599 + "re-add of identical key+value should return false");
1600 + UT_EXPECT(rrdlabels_add_changed(l, "k1", "v2", RRDLABEL_SRC_CONFIG) == true,
1601 + "value change for an existing key should return true");
1602 + UT_EXPECT(rrdlabels_add_changed(l, "k2", "v2", RRDLABEL_SRC_CONFIG) == true,
1603 + "add of a different new key should return true");
1604 + rrdlabels_destroy(l);
1605 +
1606 + // ---- rrdlabels_migrate_to_these: empty/identical/diff/DONT_DELETE ----
1607 + RRDLABELS *dst = rrdlabels_create();
1608 + RRDLABELS *src = rrdlabels_create();
1609 +
1610 + // empty dst, empty src => no add, no remove => false
1611 + UT_EXPECT(rrdlabels_migrate_to_these(dst, src) == false,
1612 + "migrate of two empty label sets should return false");
1613 +
1614 + // populate src; dst empty => added > 0 => true
1615 + rrdlabels_add(src, "k1", "v1", RRDLABEL_SRC_CONFIG);
1616 + rrdlabels_add(src, "k2", "v2", RRDLABEL_SRC_CONFIG);
1617 + UT_EXPECT(rrdlabels_migrate_to_these(dst, src) == true,
1618 + "migrate to empty dst should return true when src has labels");
1619 +
1620 + // identical content now (dst was populated from src) => false
1621 + UT_EXPECT(rrdlabels_migrate_to_these(dst, src) == false,
1622 + "migrate with identical key/value sets should return false");
1623 +
1624 + // src adds one => true
1625 + rrdlabels_add(src, "k3", "v3", RRDLABEL_SRC_CONFIG);
1626 + UT_EXPECT(rrdlabels_migrate_to_these(dst, src) == true,
1627 + "migrate that adds a label should return true");
1628 +
1629 + // src drops one => true (removed > 0)
1630 + {
1631 + RRDLABELS *trimmed = rrdlabels_create();
1632 + rrdlabels_add(trimmed, "k1", "v1", RRDLABEL_SRC_CONFIG);
1633 + rrdlabels_add(trimmed, "k2", "v2", RRDLABEL_SRC_CONFIG);
1634 + UT_EXPECT(rrdlabels_migrate_to_these(dst, trimmed) == true,
1635 + "migrate that removes a label should return true");
1636 + rrdlabels_destroy(trimmed);
1637 + }
1638 + rrdlabels_destroy(dst);
1639 + rrdlabels_destroy(src);
1640 +
1641 + // DONT_DELETE: a label that is not in src remains in dst; nothing added or
1642 + // removed, so the function returns false even though dst != src.
1643 + dst = rrdlabels_create();
1644 + src = rrdlabels_create();
1645 + rrdlabels_add(dst, "k1", "v1", RRDLABEL_SRC_CONFIG | RRDLABEL_FLAG_DONT_DELETE);
1646 + UT_EXPECT(rrdlabels_migrate_to_these(dst, src) == false,
1647 + "migrate where the only diff is a DONT_DELETE label should return false");
1648 + UT_EXPECT(rrdlabels_entries(dst) == 1,
1649 + "DONT_DELETE label should be preserved after migrate");
1650 + rrdlabels_destroy(dst);
1651 + rrdlabels_destroy(src);
1652 +
1653 + // ---- rrdlabels_remove_all_unmarked_and_changed: CLABEL-commit semantics ----
1654 + // (1) unmark + re-add identical set => no change => false
1655 + l = rrdlabels_create();
1656 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1657 + rrdlabels_add(l, "k2", "v2", RRDLABEL_SRC_CONFIG);
1658 + rrdlabels_unmark_all(l);
1659 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1660 + rrdlabels_add(l, "k2", "v2", RRDLABEL_SRC_CONFIG);
1661 + UT_EXPECT(rrdlabels_remove_all_unmarked_and_changed(l) == false,
1662 + "remove_all_unmarked_and_changed: identical re-commit should return false");
1663 + rrdlabels_destroy(l);
1664 +
1665 + // (2) unmark + add a new label => added > 0 => true
1666 + l = rrdlabels_create();
1667 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1668 + rrdlabels_unmark_all(l);
1669 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1670 + rrdlabels_add(l, "k2", "v2", RRDLABEL_SRC_CONFIG);
1671 + UT_EXPECT(rrdlabels_remove_all_unmarked_and_changed(l) == true,
1672 + "remove_all_unmarked_and_changed: adding a label should return true");
1673 + rrdlabels_destroy(l);
1674 +
1675 + // (3) unmark + commit a subset => removed > 0 => true
1676 + l = rrdlabels_create();
1677 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1678 + rrdlabels_add(l, "k2", "v2", RRDLABEL_SRC_CONFIG);
1679 + rrdlabels_unmark_all(l);
1680 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1681 + UT_EXPECT(rrdlabels_remove_all_unmarked_and_changed(l) == true,
1682 + "remove_all_unmarked_and_changed: dropping a label should return true");
1683 + rrdlabels_destroy(l);
1684 +
1685 + // (4) unmark + change a value => true (the changed entry is NEW)
1686 + l = rrdlabels_create();
1687 + rrdlabels_add(l, "k1", "v1", RRDLABEL_SRC_CONFIG);
1688 + rrdlabels_unmark_all(l);
1689 + rrdlabels_add(l, "k1", "v2", RRDLABEL_SRC_CONFIG);
1690 + UT_EXPECT(rrdlabels_remove_all_unmarked_and_changed(l) == true,
1691 + "remove_all_unmarked_and_changed: value change should return true");
1692 + rrdlabels_destroy(l);
1693 +
1694 + return errors;
1695 +}
1696 +
1697 +#undef UT_EXPECT
1698 +
1699 struct pattern_array *trim_and_add_key_to_values(struct pattern_array *pa, const char *key, STRING *input);
1700 static int rrdlabels_unittest_check_pattern_list(RRDLABELS *labels, const char *pattern, bool expected) {
1701 fprintf(stderr, "rrdlabels_match_simple_pattern(labels, \"%s\") ... ", pattern);
@@ -1716,6 +1879,7 @@ int rrdlabels_unittest(void) {
1879 errors += rrdlabels_unittest_double_check();
1880 errors += rrdlabels_unittest_migrate_check();
1881 errors += rrdlabels_unittest_mark_source_as_old();
1882 + errors += rrdlabels_unittest_change_detection();
1883 errors += rrdlabels_unittest_pattern_check();
1884
1885 fprintf(stderr, "%d errors found\n", errors);
src/database/rrdlabels.h
+17 -1
@@ -32,6 +32,10 @@ RRDLABELS *rrdlabels_create(void);
32 void rrdlabels_destroy(RRDLABELS *labels_dict);
33 void rrdlabels_flush(RRDLABELS *labels);
34 void rrdlabels_add(RRDLABELS *labels, const char *name, const char *value, RRDLABEL_SRC ls);
35 +// like rrdlabels_add() but returns true when the call actually changed the
36 +// label set (new key, or value changed for an existing key); false when the
37 +// key already existed with the same value.
38 +bool rrdlabels_add_changed(RRDLABELS *labels, const char *name, const char *value, RRDLABEL_SRC ls);
39 void rrdlabels_add_pair(RRDLABELS *labels, const char *string, RRDLABEL_SRC ls);
40 void rrdlabels_value_to_buffer_array_item_or_null(RRDLABELS *labels, BUFFER *wb, const char *key);
41 void rrdlabels_key_to_buffer_array_item(RRDLABELS *labels, BUFFER *wb);
@@ -72,7 +76,19 @@ int rrdlabels_to_buffer(RRDLABELS *labels, BUFFER *wb, const char *before_each,
76 void (*value_sanitizer)(char *dst, const char *src, size_t dst_size));
77 void rrdlabels_to_buffer_json_members(RRDLABELS *labels, BUFFER *wb);
78
75 -void rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src);
79 +// migrate dst toward src by key/value: labels present in src but missing from
80 +// dst are added, labels present in dst but missing from src are removed
81 +// (entries flagged RRDLABEL_FLAG_DONT_DELETE are preserved). The RRDLABEL_SRC
82 +// bits on entries that are already present in both are NOT reconciled to src.
83 +// Returns true when at least one label was added or removed, false otherwise.
84 +// A false return does not imply dst equals src bit-for-bit (preserved
85 +// DONT_DELETE entries and unchanged RRDLABEL_SRC bits can still differ).
86 +bool rrdlabels_migrate_to_these(RRDLABELS *dst, RRDLABELS *src);
87 +
88 +// finalize a CLABEL stream commit: remove unmarked entries and report whether
89 +// the resulting label set differs from the pre-commit set (added, removed, or
90 +// value-changed entries).
91 +bool rrdlabels_remove_all_unmarked_and_changed(RRDLABELS *labels);
92 void rrdlabels_copy(RRDLABELS *dst, RRDLABELS *src);
93 size_t rrdlabels_common_count(RRDLABELS *labels1, RRDLABELS *labels2);
94
src/database/rrdset.h
+4
@@ -63,6 +63,10 @@ typedef enum __attribute__ ((__packed__)) rrdset_flags {
63 RRDSET_FLAG_COLLECTION_FINISHED = (1 << 25), // when set, data collection is not available for this chart
64
65 RRDSET_FLAG_HAS_RRDCALC_LINKED = (1 << 26), // this chart has at least one rrdcal linked
66 +
67 + RRDSET_FLAG_PENDING_LABEL_RECHECK = (1 << 27), // chart labels changed since the last health
68 + // prototype evaluation; the health thread must
69 + // detach + reattach alerts for this chart.
70 } RRDSET_FLAGS;
71
72 // --------------------------------------------------------------------------------------------------------------------
src/health/health_event_loop.c
+29 -6
@@ -208,18 +208,41 @@ static void health_execute_delayed_initializations(RRDHOST *host) {
208
209 RRDSET *st;
210
211 - if (!rrdhost_flag_check(host, RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION)) return;
212 - rrdhost_flag_clear(host, RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION);
211 + // Atomically snapshot + clear the host pending flags. A separate check-then-clear
212 + // would race with concurrent setters (e.g. label updaters in plugins/streaming),
213 + // and a flag set between check and clear would be lost.
214 + RRDHOST_FLAGS old_host_flags = rrdhost_flag_set_and_clear(
215 + host, 0,
216 + RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION | RRDHOST_FLAG_PENDING_LABEL_RECHECK);
217 +
218 + bool host_pending_init = old_host_flags & RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION;
219 + bool host_pending_recheck = old_host_flags & RRDHOST_FLAG_PENDING_LABEL_RECHECK;
220 +
221 + if (!host_pending_init && !host_pending_recheck) return;
222
223 rrdset_foreach_reentrant(st, host) {
215 - if (!rrdset_flag_check(st, RRDSET_FLAG_PENDING_HEALTH_INITIALIZATION))
216 - continue;
224 + // Same race applies per-chart: snapshot + clear atomically so a concurrent
225 + // CLABEL stream commit or rrdset_update_rrdlabels() cannot have its
226 + // pending-recheck request swallowed by a separate clear.
227 + RRDSET_FLAGS old_st_flags = rrdset_flag_set_and_clear(
228 + st, 0,
229 + RRDSET_FLAG_PENDING_HEALTH_INITIALIZATION | RRDSET_FLAG_PENDING_LABEL_RECHECK);
230
218 - rrdset_flag_clear(st, RRDSET_FLAG_PENDING_HEALTH_INITIALIZATION);
231 + bool needs_init = old_st_flags & RRDSET_FLAG_PENDING_HEALTH_INITIALIZATION;
232 + bool needs_recheck = host_pending_recheck || (old_st_flags & RRDSET_FLAG_PENDING_LABEL_RECHECK);
233 +
234 + if (!needs_init && !needs_recheck)
235 + continue;
236
237 worker_is_busy(WORKER_HEALTH_JOB_DELAYED_INIT_RRDSET);
238
222 - health_prototype_alerts_for_rrdset_incrementally(st);
239 + // recheck path subsumes init: reset detaches all current alerts on the
240 + // chart and reattaches by re-evaluating every prototype against the
241 + // current labels, which also picks up first-attach matches.
242 + if (needs_recheck)
243 + health_prototype_reset_alerts_for_rrdset(st);
244 + else
245 + health_prototype_alerts_for_rrdset_incrementally(st);
246
247 if (!service_running(SERVICE_HEALTH))
248 break;
src/plugins.d/pluginsd_parser.c
+23 -10
@@ -183,7 +183,8 @@ static inline PARSER_RC pluginsd_host_labels(char **words, size_t num_words, PAR
183 PLUGINSD_KEYWORD_HOST_LABEL);
184 }
185
186 -static inline void pluginsd_update_host_ephemerality(RRDHOST *host) {
186 +// returns true when the _is_ephemeral label was added or its value changed.
187 +static inline bool pluginsd_update_host_ephemerality(RRDHOST *host) {
188 char value[64];
189 rrdlabels_get_value_strcpyz(host->rrdlabels, value, sizeof(value), HOST_LABEL_IS_EPHEMERAL);
190 if(value[0] && inicfg_test_boolean_value(value)) {
@@ -196,7 +197,7 @@ static inline void pluginsd_update_host_ephemerality(RRDHOST *host) {
197 }
198
199 // Set or replace current label as needed
199 - rrdlabels_add(host->rrdlabels, HOST_LABEL_IS_EPHEMERAL, value, RRDLABEL_SRC_CONFIG);
200 + return rrdlabels_add_changed(host->rrdlabels, HOST_LABEL_IS_EPHEMERAL, value, RRDLABEL_SRC_CONFIG);
201 }
202
203 #define VNODE_BASE_EPOCH (1704067200L) // Jan 1, 2024 00:00:00 UTC
@@ -241,22 +242,27 @@ static inline PARSER_RC pluginsd_host_define_end(char **words __maybe_unused, si
242 ml_host_start(host);
243 pulse_host_status(host, 0, 0); // this will detect the receiver status
244
245 + bool labels_changed;
246 if(host->rrdlabels) {
245 - rrdlabels_migrate_to_these(host->rrdlabels, parser->user.host_define.rrdlabels);
247 + labels_changed = rrdlabels_migrate_to_these(host->rrdlabels, parser->user.host_define.rrdlabels);
248 }
249 else {
250 host->rrdlabels = parser->user.host_define.rrdlabels;
251 parser->user.host_define.rrdlabels = NULL;
252 + labels_changed = true;
253 }
254
255 if(SERVING_PLUGINSD(parser)) {
253 - rrdlabels_add(host->rrdlabels, "_collector_machine_guid",
256 + labels_changed |= rrdlabels_add_changed(host->rrdlabels, "_collector_machine_guid",
257 localhost->machine_guid, RRDLABEL_SRC_AUTO);
258 }
259
257 - pluginsd_update_host_ephemerality(host);
260 + labels_changed |= pluginsd_update_host_ephemerality(host);
261 pluginsd_host_define_cleanup(parser);
262
263 + if(labels_changed)
264 + rrdhost_flag_set(host, RRDHOST_FLAG_PENDING_LABEL_RECHECK);
265 +
266 parser->user.host = host;
267 pluginsd_clear_scope_chart(parser, PLUGINSD_KEYWORD_HOST_DEFINE_END, NULL);
268
@@ -725,16 +731,18 @@ static inline PARSER_RC pluginsd_overwrite(char **words __maybe_unused, size_t n
731 if(unlikely(!host->rrdlabels))
732 host->rrdlabels = rrdlabels_create();
733
728 - rrdlabels_migrate_to_these(host->rrdlabels, parser->user.new_host_labels);
729 - pluginsd_update_host_ephemerality(host);
734 + bool labels_changed = rrdlabels_migrate_to_these(host->rrdlabels, parser->user.new_host_labels);
735 + labels_changed |= pluginsd_update_host_ephemerality(host);
736
737 if(!rrdlabels_exist(host->rrdlabels, "_os"))
732 - rrdlabels_add(host->rrdlabels, "_os", string2str(host->os), RRDLABEL_SRC_AUTO);
738 + labels_changed |= rrdlabels_add_changed(host->rrdlabels, "_os", string2str(host->os), RRDLABEL_SRC_AUTO);
739
740 if(!rrdlabels_exist(host->rrdlabels, "_hostname"))
735 - rrdlabels_add(host->rrdlabels, "_hostname", string2str(host->hostname), RRDLABEL_SRC_AUTO);
741 + labels_changed |= rrdlabels_add_changed(host->rrdlabels, "_hostname", string2str(host->hostname), RRDLABEL_SRC_AUTO);
742
743 rrdhost_flag_set(host, RRDHOST_FLAG_METADATA_LABELS | RRDHOST_FLAG_METADATA_UPDATE);
744 + if(labels_changed)
745 + rrdhost_flag_set(host, RRDHOST_FLAG_PENDING_LABEL_RECHECK);
746
747 rrdlabels_destroy(parser->user.new_host_labels);
748 parser->user.new_host_labels = NULL;
@@ -776,12 +784,17 @@ static inline PARSER_RC pluginsd_clabel_commit(char **words __maybe_unused, size
784 return PLUGINSD_DISABLE_PLUGIN(parser, NULL, NULL);
785 }
786
779 - rrdlabels_remove_all_unmarked(st->rrdlabels);
787 + bool labels_changed = rrdlabels_remove_all_unmarked_and_changed(st->rrdlabels);
788
789 rrdset_flag_set(st, RRDSET_FLAG_METADATA_UPDATE);
790 rrdhost_flag_set(st->rrdhost, RRDHOST_FLAG_METADATA_UPDATE);
791 rrdset_metadata_updated(st);
792
793 + if(labels_changed) {
794 + rrdset_flag_set(st, RRDSET_FLAG_PENDING_LABEL_RECHECK);
795 + rrdhost_flag_set(st->rrdhost, RRDHOST_FLAG_PENDING_HEALTH_INITIALIZATION);
796 + }
797 +
798 parser->user.clabel_count = 0;
799
800 return PARSER_RC_OK;