master
h 145 lines 6.79 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #ifndef NETDATA_RRDSET_PLUGINSD_ARRAY_H
4 #define NETDATA_RRDSET_PLUGINSD_ARRAY_H
5
6 // This header must be included AFTER rrddim.h to get the full struct pluginsd_rrddim definition
7
8 #include "rrddim.h"
9
10 // --------------------------------------------------------------------------------------------------------------------
11 // Reference-counted array for pluginsd dimension caching
12 //
13 // This structure provides thread-safe access to the dimension cache array used by the pluginsd protocol.
14 // The reference counting ensures that the array is not freed while any thread is still using it.
15 //
16 // THREAD SAFETY - LIFECYCLE SEPARATION:
17 // -------------------------------------
18 // The design relies on collector and cleanup never running concurrently on the same chart:
19 //
20 // 1. collector_tid: Primary synchronization mechanism
21 // - Collector sets collector_tid BEFORE accessing the array
22 // - Collector clears collector_tid AFTER all operations are complete
23 // - Cleanup code checks collector_tid and SKIPS if non-zero
24 // - This allows the collector to use lock-free operations (get_unsafe, replace, release)
25 //
26 // 2. spinlock + refcount: Coordinates concurrent cleanup operations
27 // - prd_array_acquire(): Takes spinlock, loads pointer, increments refcount
28 // - Used by cleanup code when collector is NOT active
29 // - Prevents races between multiple cleanup threads
30 //
31 // 3. Lifecycle guarantee: In production, cleanup only runs when:
32 // - Stream receiver is stopped (collector thread terminated)
33 // - collector_tid is explicitly cleared before cleanup
34 // - Therefore, collector's replace+release never races with cleanup's acquire
35 //
36 // HOT PATH (collector active, collector_tid set): Lock-free
37 // CLEANUP PATH (collector stopped, collector_tid == 0): Uses spinlock
38 // --------------------------------------------------------------------------------------------------------------------
39
40 typedef struct pluginsd_rrddim_array {
41 int32_t refcount; // Reference count (atomic)
42 size_t size; // Number of entries in the array
43 struct pluginsd_rrddim entries[]; // Flexible array member
44 } PRD_ARRAY;
45
46 // --------------------------------------------------------------------------------------------------------------------
47 // API Functions
48 // --------------------------------------------------------------------------------------------------------------------
49
50 // Create a new array with the specified size and refcount=1
51 // size is expected to be bounded by the caller: pluginsd slot input is capped at
52 // the parser (PLUGINSD_DIMENSION_SLOT_MAX) and the no-slots path uses the dimension
53 // count, so the size multiplication below cannot overflow. The check documents and
54 // guards that invariant against any future unbounded caller (debug builds only).
55 static inline PRD_ARRAY *prd_array_create(size_t size) {
56 internal_fatal(size > (SIZE_MAX - sizeof(PRD_ARRAY)) / sizeof(struct pluginsd_rrddim),
57 "PRD_ARRAY: requested size %zu would overflow the allocation", size);
58 PRD_ARRAY *arr = callocz(1, sizeof(PRD_ARRAY) + size * sizeof(struct pluginsd_rrddim));
59 arr->refcount = 1;
60 arr->size = size;
61 rrd_slot_memory_added(sizeof(PRD_ARRAY) + size * sizeof(struct pluginsd_rrddim));
62 return arr;
63 }
64
65 // Acquire a reference to the array when spinlock is ALREADY HELD
66 // Returns NULL if no array exists
67 // The caller MUST call prd_array_release() when done
68 // Use this when you need to do additional checks (e.g., collector_tid) under the same spinlock
69 static inline PRD_ARRAY *prd_array_acquire_locked(PRD_ARRAY **array_ptr) {
70 PRD_ARRAY *arr = *array_ptr;
71 if (arr) {
72 __atomic_fetch_add(&arr->refcount, 1, __ATOMIC_ACQ_REL);
73 }
74 return arr;
75 }
76
77 // Acquire a reference to the array stored in the atomic pointer location
78 // Returns NULL if no array exists
79 // The caller MUST call prd_array_release() when done
80 //
81 // IMPORTANT: Only call this when collector_tid == 0 (collector not active).
82 // Uses spinlock to coordinate with other cleanup operations.
83 static inline PRD_ARRAY *prd_array_acquire(PRD_ARRAY **array_ptr, SPINLOCK *spinlock) {
84 spinlock_lock(spinlock);
85 PRD_ARRAY *arr = prd_array_acquire_locked(array_ptr);
86 spinlock_unlock(spinlock);
87 return arr;
88 }
89
90 // Release a reference to the array
91 // If this was the last reference (refcount becomes 0), the array is freed
92 // Safe to call with NULL
93 static inline void prd_array_release(PRD_ARRAY *arr) {
94 if (!arr)
95 return;
96
97 int32_t old_refcount = __atomic_load_n(&arr->refcount, __ATOMIC_ACQUIRE);
98 while(true) {
99 if(unlikely(old_refcount <= 0)) {
100 // Keep the object stable and avoid driving refcount further negative on
101 // repeated misuse. Log in all builds; internal_fatal adds extra checks.
102 nd_log_limit_static_global_var(erl_prd_refcount_underflow, 1, 0);
103 nd_log_limit(&erl_prd_refcount_underflow, NDLS_DAEMON, NDLP_WARNING,
104 "PRD_ARRAY: refcount underflow (was %d) - double release detected",
105 old_refcount);
106 internal_fatal(true,
107 "PRD_ARRAY: refcount underflow (was %d) - double release detected", old_refcount);
108 return;
109 }
110
111 if(__atomic_compare_exchange_n(&arr->refcount, &old_refcount, old_refcount - 1,
112 false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE))
113 break;
114 }
115
116 if(old_refcount == 1) {
117 // We were the last reference - free the array
118 // Note: The caller is responsible for releasing any RRDDIM_ACQUIRED references
119 // in the entries before the final release
120 rrd_slot_memory_removed(sizeof(PRD_ARRAY) + arr->size * sizeof(struct pluginsd_rrddim));
121 freez(arr);
122 }
123 }
124
125 // Atomically replace the array pointer with a new array
126 // Returns the old array (caller must release it) or NULL if there was no old array
127 // The new_arr can be NULL to clear the array
128 //
129 // Thread safety depends on context:
130 // - Collector (collector_tid set): No spinlock needed - cleanup will skip
131 // - Cleanup (collector_tid == 0): Should hold spinlock to coordinate with other cleanup
132 static inline PRD_ARRAY *prd_array_replace(PRD_ARRAY **array_ptr, PRD_ARRAY *new_arr) {
133 return __atomic_exchange_n(array_ptr, new_arr, __ATOMIC_ACQ_REL);
134 }
135
136 // Get the current array without acquiring a reference (for quick NULL checks or
137 // when external synchronization guarantees the array won't be freed)
138 // WARNING: The returned pointer may become invalid at any time unless:
139 // - The caller holds the spinlock, OR
140 // - The caller is the collector thread with collector_tid set (preventing cleanup)
141 static inline PRD_ARRAY *prd_array_get_unsafe(PRD_ARRAY **array_ptr) {
142 return __atomic_load_n(array_ptr, __ATOMIC_ACQUIRE);
143 }
144
145 #endif // NETDATA_RRDSET_PLUGINSD_ARRAY_H